Merge cmp-rewrite and complete Greek translations

Resolve the values-el conflict in favor of accurate Greek playback
labels, add translations for 39 new upstream strings (library filters,
P2P cache/profile, torrent status, Upcoming), and fix leftover typos.

Co-authored-by: Nasos V. <nvasedis@gmail.com>
This commit is contained in:
Cursor Agent 2026-07-24 17:25:09 +00:00
commit e1811f0e5e
No known key found for this signature in database
107 changed files with 4576 additions and 1257 deletions

193
.github/workflows/android-release.yml vendored Normal file
View file

@ -0,0 +1,193 @@
name: Build Android Release
on:
workflow_dispatch:
inputs:
mode:
description: Build only, create a draft, or publish the release
required: true
type: choice
options:
- dry-run
- draft
- publish
default: dry-run
exclude_commits:
description: Optional comma-separated commit hashes to omit from the notes
required: false
type: string
permissions:
contents: write
concurrency:
group: android-release-${{ github.ref }}
cancel-in-progress: false
jobs:
release:
name: ${{ inputs.mode }} Android release
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
RELEASE_PROPERTIES_BASE64: ${{ secrets.NUVIO_LOCAL_PROPERTIES_BASE64 }}
RELEASE_KEYSTORE_BASE64: ${{ secrets.NUVIO_RELEASE_KEYSTORE_BASE64 }}
steps:
- name: Check out full history
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Read release metadata
id: release
run: ./scripts/release-metadata.sh "${GITHUB_SHA}" >> "${GITHUB_OUTPUT}"
- name: Validate release state
env:
RELEASE_TAG: ${{ steps.release.outputs.tag }}
CURRENT_BUMP: ${{ steps.release.outputs.current_bump }}
RELEASE_COMMIT: ${{ steps.release.outputs.release_commit }}
RELEASE_BRANCH: ${{ github.ref_name }}
RELEASE_REF_TYPE: ${{ github.ref_type }}
RELEASE_MODE: ${{ inputs.mode }}
run: |
if [[ "${RELEASE_REF_TYPE}" != "branch" ]]; then
echo "Releases must be dispatched from a branch, not ${RELEASE_REF_TYPE}." >&2
exit 1
fi
if git rev-parse --verify --quiet "refs/tags/${RELEASE_TAG}"; then
echo "Tag ${RELEASE_TAG} already exists." >&2
exit 1
fi
if gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}" >/dev/null 2>&1; then
echo "A GitHub release for ${RELEASE_TAG} already exists." >&2
exit 1
fi
unexpected_changes="$(
git diff --name-only "${CURRENT_BUMP}..${RELEASE_COMMIT}" \
| grep -Ev '^(\.github/workflows/android-release\.yml|scripts/(generate-release-notes|release-metadata)\.sh)$' \
|| true
)"
if [[ -n "${unexpected_changes}" ]]; then
echo "The version bump must be the final application change before release." >&2
echo "Unexpected files changed after the bump:" >&2
echo "${unexpected_changes}" >&2
exit 1
fi
if [[ "${RELEASE_MODE}" != "dry-run" ]]; then
required_secrets=(
RELEASE_PROPERTIES_BASE64
RELEASE_KEYSTORE_BASE64
)
missing=()
for secret_name in "${required_secrets[@]}"; do
if [[ -z "${!secret_name}" ]]; then
missing+=("${secret_name}")
fi
done
if (( ${#missing[@]} > 0 )); then
printf 'Missing required release secrets: %s\n' "${missing[*]}" >&2
exit 1
fi
fi
- name: Generate release notes
env:
PREVIOUS_BUMP: ${{ steps.release.outputs.previous_bump }}
CURRENT_BUMP: ${{ steps.release.outputs.current_bump }}
EXCLUDE_COMMITS: ${{ inputs.exclude_commits }}
run: |
./scripts/generate-release-notes.sh \
--from "${PREVIOUS_BUMP}" \
--to "${CURRENT_BUMP}" \
--repository "${GITHUB_REPOSITORY}" \
--exclude "${EXCLUDE_COMMITS}" \
> release-notes.md
if [[ ! -s release-notes.md ]]; then
echo "No release-note commits were found." >&2
exit 1
fi
{
echo "## ${{ steps.release.outputs.version }} release notes"
echo
cat release-notes.md
} >> "${GITHUB_STEP_SUMMARY}"
- name: Set up Java
if: inputs.mode != 'dry-run'
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
- name: Set up Gradle
if: inputs.mode != 'dry-run'
uses: gradle/actions/setup-gradle@v6
- name: Configure release properties
if: inputs.mode != 'dry-run'
run: |
keystore_path="${RUNNER_TEMP}/nuvio-release.jks"
printf '%s' "${RELEASE_KEYSTORE_BASE64}" | base64 --decode > "${keystore_path}"
printf '%s' "${RELEASE_PROPERTIES_BASE64}" | base64 --decode > local.properties
# These paths only apply to the machine where local.properties was encoded.
sed -i -E '/^[[:space:]]*(sdk\.dir|NUVIO_RELEASE_STORE_FILE)[[:space:]]*=/d' local.properties
printf '\nNUVIO_RELEASE_STORE_FILE=%s\n' "${keystore_path}" >> local.properties
required_properties=(
NUVIO_RELEASE_STORE_PASSWORD
NUVIO_RELEASE_KEY_ALIAS
NUVIO_RELEASE_KEY_PASSWORD
TRAKT_CLIENT_ID
TRAKT_CLIENT_SECRET
)
for property_name in "${required_properties[@]}"; do
if ! grep -Eq "^${property_name}=.+" local.properties; then
echo "Missing required property: ${property_name}" >&2
exit 1
fi
done
- name: Build full release APKs
if: inputs.mode != 'dry-run'
run: ./gradlew :androidApp:assembleFullRelease --stacktrace
- name: Upload build artifacts
if: inputs.mode != 'dry-run'
uses: actions/upload-artifact@v6
with:
name: nuvio-${{ steps.release.outputs.version }}-full-release
path: androidApp/build/outputs/apk/full/release/*.apk
if-no-files-found: error
- name: Create GitHub release
if: inputs.mode != 'dry-run'
env:
RELEASE_MODE: ${{ inputs.mode }}
RELEASE_TAG: ${{ steps.release.outputs.tag }}
RELEASE_VERSION: ${{ steps.release.outputs.version }}
RELEASE_BRANCH: ${{ github.ref_name }}
run: |
mapfile -t release_apks < <(find androidApp/build/outputs/apk/full/release -maxdepth 1 -type f -name '*.apk' -print | sort)
if (( ${#release_apks[@]} == 0 )); then
echo "No release APKs were produced." >&2
exit 1
fi
release_args=(
"${RELEASE_TAG}"
--target "${RELEASE_BRANCH}"
--title "${RELEASE_VERSION}"
--notes-file release-notes.md
)
if [[ "${RELEASE_MODE}" == "draft" ]]; then
release_args+=(--draft)
fi
gh release create "${release_args[@]}" "${release_apks[@]}"

View file

@ -226,6 +226,7 @@ val iosDistributionSourceDir = if (iosDistribution == "full") {
"src/iosAppStore/kotlin"
}
val iosFrameworkBundleId = "com.nuvio.media"
val nuvioEngineAppleFramework = rootProject.file("../nuvio-engine/platform/apple/NuvioEngine.xcframework")
val fullCommonSourceDir = project.file("src/fullCommonMain/kotlin")
val generatedRuntimeConfigDir = layout.buildDirectory.dir("generated/runtime-config/kotlin")
val requestedGradleTasks = gradle.startParameter.taskNames.map { taskName ->
@ -329,12 +330,28 @@ kotlin {
)
iosTargets.forEach { iosTarget ->
val nuvioEngineSlice = if (iosTarget.name == "iosArm64") {
"ios-arm64"
} else {
"ios-arm64_x86_64-simulator"
}
val nuvioEngineSliceDirectory = nuvioEngineAppleFramework.resolve(nuvioEngineSlice)
iosTarget.compilations.getByName("main") {
cinterops {
create("commoncrypto") {
defFile(project.file("src/nativeInterop/cinterop/commoncrypto.def"))
compilerOpts("-I${project.projectDir}/src/nativeInterop/cinterop")
}
if (iosDistribution == "full") {
check(nuvioEngineSliceDirectory.resolve("libCNuvioEngine.a").isFile) {
"Build the local Nuvio Engine Apple XCFramework before compiling iOS Full."
}
create("nuvioengine") {
defFile(project.file("src/nativeInterop/cinterop/nuvioengine.def"))
compilerOpts("-I${nuvioEngineSliceDirectory.resolve("Headers").absolutePath}")
extraOpts("-libraryPath", nuvioEngineSliceDirectory.absolutePath)
}
}
}
if (iosDistribution == "full") {
@ -354,6 +371,14 @@ kotlin {
baseName = "ComposeApp"
isStatic = true
freeCompilerArgs += listOf("-Xbinary=bundleId=$iosFrameworkBundleId")
if (iosDistribution == "full") {
linkerOpts(
"-lc++",
"-framework", "Security",
"-framework", "SystemConfiguration",
"-framework", "CoreFoundation",
)
}
}
}
@ -392,6 +417,7 @@ kotlin {
implementation(libs.androidx.media3.container)
implementation(libs.androidx.media3.extractor)
implementation(libs.mpv.android.lib)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("lib-*.aar"))))
if (androidDistribution == "full") {
implementation(files("libs/quickjs-kt-android-1.0.5-nuvio.aar"))

Binary file not shown.

View file

@ -39,8 +39,11 @@
-keep class com.dokar.quickjs.** { *; }
-keep class com.nuvio.app.features.plugins.** { *; }
# TorrServer based P2P streaming.
# P2P runtime and Nuvio Engine JNI bridge. Native libraries are not processed
# by R8, but their Kotlin/JNI wrapper classes and method names must stay stable.
-keep class com.nuvio.app.features.p2p.** { *; }
-keep class com.nuvio.engine.** { *; }
-keep interface com.nuvio.engine.** { *; }
-keep class androidx.work.impl.WorkDatabase_Impl { *; }

View file

@ -11,12 +11,16 @@ internal object PlatformPlaybackDataSourceFactory {
defaultRequestHeaders: Map<String, String>,
defaultResponseHeaders: Map<String, String>,
useYoutubeChunkedPlayback: Boolean,
useLongReadTimeout: Boolean = false,
externalSubtitles: List<com.nuvio.app.features.streams.StreamSubtitle> = emptyList(),
): DataSource.Factory {
val networkFactory: DataSource.Factory = if (useYoutubeChunkedPlayback) {
YoutubeChunkedDataSourceFactory(defaultRequestHeaders = defaultRequestHeaders)
} else {
PlayerPlaybackNetworking.createHttpDataSourceFactory(defaultRequestHeaders)
PlayerPlaybackNetworking.createHttpDataSourceFactory(
defaultRequestHeaders,
useLongReadTimeout,
)
}
val subtitleHeaderFactory = SubtitleRequestHeaderDataSourceFactory(
upstreamFactory = networkFactory,
@ -32,4 +36,4 @@ internal object PlatformPlaybackDataSourceFactory {
)
}
}
}
}

View file

@ -0,0 +1,92 @@
package com.nuvio.app.features.p2p
import com.nuvio.engine.NuvioUploadMode
import com.nuvio.engine.NuvioTorrentProfile
import java.io.File
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
class P2pStreamingEngineAndroidTest {
@Test
fun appOwnedRoutesDisableAutomaticInactivityExpiry() {
val stateDirectory = File("state")
val cacheDirectory = File("cache")
val uploading = buildNuvioEngineConfig(
stateDirectory = stateDirectory,
cacheDirectory = cacheDirectory,
uploadEnabled = true,
torrentProfile = P2pTorrentProfile.FAST,
diskCacheCapacityBytes = P2pCacheSize.GB_5.bytes,
)
val downloadOnly = buildNuvioEngineConfig(
stateDirectory = stateDirectory,
cacheDirectory = cacheDirectory,
uploadEnabled = false,
torrentProfile = P2pTorrentProfile.SOFT,
diskCacheCapacityBytes = P2pCacheSize.NONE.bytes,
)
assertEquals(0, uploading.streamInactivityTimeoutMilliseconds)
assertEquals(NuvioUploadMode.Unlimited, uploading.uploadMode)
assertEquals(NuvioTorrentProfile.Fast, uploading.torrentProfile)
assertEquals(P2pCacheSize.GB_5.bytes, uploading.diskCacheCapacityBytes)
assertEquals(0, downloadOnly.streamInactivityTimeoutMilliseconds)
assertEquals(NuvioUploadMode.Disabled, downloadOnly.uploadMode)
assertEquals(NuvioTorrentProfile.Soft, downloadOnly.torrentProfile)
assertEquals(0L, downloadOnly.diskCacheCapacityBytes)
}
@Test
fun matchingUnsolicitedStopBecomesTerminalError() {
val error = unexpectedStreamStopError(
requestId = 0L,
eventStreamId = "stream",
currentStreamId = "stream",
message = "stream expired after inactivity",
fallbackMessage = "unknown",
)
assertEquals(
P2pStreamingState.Error("stream expired after inactivity"),
error,
)
}
@Test
fun explicitAndStaleStopsAreIgnored() {
assertNull(
unexpectedStreamStopError(
requestId = 7L,
eventStreamId = "stream",
currentStreamId = "stream",
message = "stopped",
fallbackMessage = "unknown",
)
)
assertNull(
unexpectedStreamStopError(
requestId = 0L,
eventStreamId = "old-stream",
currentStreamId = "stream",
message = "stopped",
fallbackMessage = "unknown",
)
)
}
@Test
fun blankStopMessageUsesFallback() {
assertEquals(
P2pStreamingState.Error("unknown"),
unexpectedStreamStopError(
requestId = 0L,
eventStreamId = "stream",
currentStreamId = "stream",
message = " ",
fallbackMessage = "unknown",
),
)
}
}

View file

@ -21,6 +21,7 @@ import com.nuvio.app.features.debrid.DebridSettingsStorage
import com.nuvio.app.features.downloads.DownloadsLiveStatusPlatform
import com.nuvio.app.features.downloads.DownloadsPlatformDownloader
import com.nuvio.app.features.downloads.DownloadsStorage
import com.nuvio.app.features.library.LibraryDisplaySettingsStorage
import com.nuvio.app.features.library.LibraryStorage
import com.nuvio.app.features.details.MetaScreenSettingsStorage
import com.nuvio.app.features.home.HomeCatalogSettingsStorage
@ -103,6 +104,7 @@ class MainActivity : AppCompatActivity() {
TraktCommentsStorage.initialize(applicationContext)
TraktLibraryStorage.initialize(applicationContext)
TraktSettingsStorage.initialize(applicationContext)
LibraryDisplaySettingsStorage.initialize(applicationContext)
ContinueWatchingPreferencesStorage.initialize(applicationContext)
ResumePromptStorage.initialize(applicationContext)
ContinueWatchingEnrichmentStorage.initialize(applicationContext)

View file

@ -6,6 +6,7 @@ internal actual object PlatformLocalAccountDataCleaner {
private val preferenceNames = listOf(
"nuvio_addons",
"nuvio_library",
"nuvio_library_display_settings",
"nuvio_home_catalog_settings",
"nuvio_player_settings",
"torrent_settings",

View file

@ -0,0 +1,26 @@
package com.nuvio.app.features.library
import android.content.Context
import android.content.SharedPreferences
import com.nuvio.app.core.storage.ProfileScopedKey
actual object LibraryDisplaySettingsStorage {
private const val preferencesName = "nuvio_library_display_settings"
private const val payloadKey = "library_display_settings_payload"
private var preferences: SharedPreferences? = null
fun initialize(context: Context) {
preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
}
actual fun loadPayload(): String? =
preferences?.getString(ProfileScopedKey.of(payloadKey), null)
actual fun savePayload(payload: String) {
preferences
?.edit()
?.putString(ProfileScopedKey.of(payloadKey), payload)
?.apply()
}
}

View file

@ -22,6 +22,7 @@ actual object MdbListSettingsStorage {
private const val useTraktKey = "mdblist_use_trakt"
private const val useLetterboxdKey = "mdblist_use_letterboxd"
private const val useAudienceKey = "mdblist_use_audience"
private const val useMalKey = "mdblist_use_mal"
private val syncKeys = listOf(
enabledKey,
apiKey,
@ -32,6 +33,7 @@ actual object MdbListSettingsStorage {
useTraktKey,
useLetterboxdKey,
useAudienceKey,
useMalKey,
)
private var preferences: SharedPreferences? = null
@ -98,6 +100,12 @@ actual object MdbListSettingsStorage {
saveBoolean(useAudienceKey, enabled)
}
actual fun loadUseMal(): Boolean? = loadBoolean(useMalKey)
actual fun saveUseMal(enabled: Boolean) {
saveBoolean(useMalKey, enabled)
}
private fun loadBoolean(key: String): Boolean? =
preferences?.let { sharedPreferences ->
val scopedKey = ProfileScopedKey.of(key)
@ -125,6 +133,7 @@ actual object MdbListSettingsStorage {
loadUseTrakt()?.let { put(useTraktKey, encodeSyncBoolean(it)) }
loadUseLetterboxd()?.let { put(useLetterboxdKey, encodeSyncBoolean(it)) }
loadUseAudience()?.let { put(useAudienceKey, encodeSyncBoolean(it)) }
loadUseMal()?.let { put(useMalKey, encodeSyncBoolean(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
@ -141,5 +150,6 @@ actual object MdbListSettingsStorage {
payload.decodeSyncBoolean(useTraktKey)?.let(::saveUseTrakt)
payload.decodeSyncBoolean(useLetterboxdKey)?.let(::saveUseLetterboxd)
payload.decodeSyncBoolean(useAudienceKey)?.let(::saveUseAudience)
payload.decodeSyncBoolean(useMalKey)?.let(::saveUseMal)
}
}

View file

@ -9,6 +9,8 @@ internal actual object P2pSettingsStorage {
private const val p2pEnabledKey = "p2p_enabled"
private const val enableUploadKey = "enable_upload"
private const val hideTorrentStatsKey = "hide_torrent_stats"
private const val torrentProfileKey = "torrent_profile"
private const val cacheSizeKey = "cache_size"
private var preferences: SharedPreferences? = null
@ -37,6 +39,18 @@ internal actual object P2pSettingsStorage {
saveBoolean(hideTorrentStatsKey, enabled)
}
actual fun loadTorrentProfile(): String? = loadString(torrentProfileKey)
actual fun saveTorrentProfile(profile: String) {
saveString(torrentProfileKey, profile)
}
actual fun loadCacheSize(): String? = loadString(cacheSizeKey)
actual fun saveCacheSize(size: String) {
saveString(cacheSizeKey, size)
}
private fun loadBoolean(keyBase: String): Boolean? =
preferences?.let { sharedPreferences ->
val key = ProfileScopedKey.of(keyBase)
@ -53,4 +67,14 @@ internal actual object P2pSettingsStorage {
?.putBoolean(ProfileScopedKey.of(keyBase), value)
?.apply()
}
private fun loadString(keyBase: String): String? =
preferences?.getString(ProfileScopedKey.of(keyBase), null)
private fun saveString(keyBase: String, value: String) {
preferences
?.edit()
?.putString(ProfileScopedKey.of(keyBase), value)
?.apply()
}
}

View file

@ -9,6 +9,7 @@ import android.util.Log
import android.util.TypedValue
import android.graphics.Typeface
import android.os.Build
import android.os.SystemClock
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.util.AttributeSet
import androidx.compose.runtime.getValue
@ -79,6 +80,12 @@ import java.net.HttpURLConnection
import java.net.URL
private const val TAG = "NuvioPlayer"
private const val PLAYER_DIAGNOSTIC_TAG = "NuvioPlayerDiag"
private class PlaybackDiagnostics {
var prepareStartedAtMs: Long = 0L
var attempt: Int = 0
}
@androidx.annotation.OptIn(UnstableApi::class)
@Composable
@ -92,8 +99,11 @@ actual fun PlatformPlayerSurface(
useYoutubeChunkedPlayback: Boolean,
modifier: Modifier,
playWhenReady: Boolean,
initialPositionMs: Long?,
initialPositionRequestKey: String?,
resizeMode: PlayerResizeMode,
useNativeController: Boolean,
onInitialPositionHandled: (key: String, handled: Boolean) -> Unit,
onControllerReady: (PlayerEngineController) -> Unit,
onSnapshot: (PlayerPlaybackSnapshot) -> Unit,
onError: (String?) -> Unit,
@ -109,6 +119,7 @@ actual fun PlatformPlayerSurface(
sanitizePlaybackResponseHeaders(sourceResponseHeaders),
normalizeStreamType(streamType).orEmpty(),
useYoutubeChunkedPlayback,
initialPositionRequestKey.orEmpty(),
)
var activeEngine by remember(playerSourceKey, playerSettings.androidPlaybackEngine) {
mutableStateOf(playerSettings.androidPlaybackEngine.initialAndroidEngine())
@ -125,13 +136,19 @@ actual fun PlatformPlayerSurface(
useYoutubeChunkedPlayback = useYoutubeChunkedPlayback,
modifier = modifier,
playWhenReady = playWhenReady,
initialPositionMs = initialPositionMs,
initialPositionRequestKey = initialPositionRequestKey,
resizeMode = resizeMode,
useNativeController = useNativeController,
onInitialPositionHandled = onInitialPositionHandled,
onControllerReady = onControllerReady,
onSnapshot = onSnapshot,
onError = { message ->
if (message != null && playerSettings.androidPlaybackEngine == AndroidPlaybackEngine.Auto) {
Log.w(TAG, "ExoPlayer failed; falling back to libmpv: $message")
initialPositionRequestKey?.let { key ->
onInitialPositionHandled(key, false)
}
activeEngine = ResolvedAndroidPlaybackEngine.Libmpv
onError(null)
} else {
@ -139,21 +156,28 @@ actual fun PlatformPlayerSurface(
}
},
)
ResolvedAndroidPlaybackEngine.Libmpv -> LibmpvPlayerSurface(
sourceUrl = sourceUrl,
sourceAudioUrl = sourceAudioUrl,
sourceHeaders = sourceHeaders,
externalSubtitles = externalSubtitles,
modifier = modifier,
playWhenReady = playWhenReady,
resizeMode = resizeMode,
videoOutput = playerSettings.androidLibmpvVideoOutput,
hardwareDecodingEnabled = playerSettings.androidLibmpvHardwareDecodingEnabled,
yuv420pEnabled = playerSettings.androidLibmpvYuv420pEnabled,
onControllerReady = onControllerReady,
onSnapshot = onSnapshot,
onError = onError,
)
ResolvedAndroidPlaybackEngine.Libmpv -> {
LaunchedEffect(initialPositionRequestKey) {
initialPositionRequestKey?.let { key ->
onInitialPositionHandled(key, false)
}
}
LibmpvPlayerSurface(
sourceUrl = sourceUrl,
sourceAudioUrl = sourceAudioUrl,
sourceHeaders = sourceHeaders,
externalSubtitles = externalSubtitles,
modifier = modifier,
playWhenReady = playWhenReady,
resizeMode = resizeMode,
videoOutput = playerSettings.androidLibmpvVideoOutput,
hardwareDecodingEnabled = playerSettings.androidLibmpvHardwareDecodingEnabled,
yuv420pEnabled = playerSettings.androidLibmpvYuv420pEnabled,
onControllerReady = onControllerReady,
onSnapshot = onSnapshot,
onError = onError,
)
}
}
}
@ -181,8 +205,11 @@ private fun ExoPlayerSurface(
useYoutubeChunkedPlayback: Boolean,
modifier: Modifier,
playWhenReady: Boolean,
initialPositionMs: Long?,
initialPositionRequestKey: String?,
resizeMode: PlayerResizeMode,
useNativeController: Boolean,
onInitialPositionHandled: (key: String, handled: Boolean) -> Unit,
onControllerReady: (PlayerEngineController) -> Unit,
onSnapshot: (PlayerPlaybackSnapshot) -> Unit,
onError: (String?) -> Unit,
@ -191,6 +218,7 @@ private fun ExoPlayerSurface(
val lifecycleOwner = LocalLifecycleOwner.current
val latestOnSnapshot = rememberUpdatedState(onSnapshot)
val latestOnError = rememberUpdatedState(onError)
val latestOnInitialPositionHandled = rememberUpdatedState(onInitialPositionHandled)
val latestPlayWhenReady = rememberUpdatedState(playWhenReady)
val coroutineScope = rememberCoroutineScope()
@ -219,7 +247,9 @@ private fun ExoPlayerSurface(
sanitizedSourceResponseHeaders,
normalizedStreamType.orEmpty(),
useYoutubeChunkedPlayback,
initialPositionRequestKey.orEmpty(),
)
val playbackDiagnostics = remember(playerSourceKey) { PlaybackDiagnostics() }
var subtitleDelayMs by remember(playerSourceKey) { mutableStateOf(0) }
var selectedExternalSubtitleMimeType by remember(playerSourceKey) { mutableStateOf<String?>(null) }
val latestSubtitleDelayMs = rememberUpdatedState(subtitleDelayMs)
@ -262,6 +292,7 @@ private fun ExoPlayerSurface(
}
val dataSourceFactory = remember(
context,
sourceUrl,
sanitizedSourceHeaders,
sanitizedSourceResponseHeaders,
useYoutubeChunkedPlayback,
@ -272,6 +303,7 @@ private fun ExoPlayerSurface(
defaultRequestHeaders = sanitizedSourceHeaders,
defaultResponseHeaders = sanitizedSourceResponseHeaders,
useYoutubeChunkedPlayback = useYoutubeChunkedPlayback,
useLongReadTimeout = isLoopbackPlaybackSource(sourceUrl),
externalSubtitles = externalSubtitles,
)
}
@ -302,6 +334,7 @@ private fun ExoPlayerSurface(
normalizedStreamType,
useYoutubeChunkedPlayback,
effectiveDecoderPriority,
initialPositionRequestKey,
) {
val renderersFactory = SubtitleOffsetRenderersFactory(
context = context,
@ -389,9 +422,28 @@ private fun ExoPlayerSurface(
onDispose { nowPlayingController.release() }
}
LaunchedEffect(exoPlayer, resolvedMediaItem) {
LaunchedEffect(exoPlayer, resolvedMediaItem, initialPositionRequestKey) {
val mediaItem = resolvedMediaItem ?: return@LaunchedEffect
exoPlayer.setPlaybackMediaItem(mediaItem, fallbackStartPositionMs)
val requestedStartPositionMs = fallbackStartPositionMs
?: initialPositionMs?.takeIf { it > 0L }
playbackDiagnostics.attempt += 1
playbackDiagnostics.prepareStartedAtMs = SystemClock.elapsedRealtime()
Log.i(
PLAYER_DIAGNOSTIC_TAG,
"prepare begin attempt=${playbackDiagnostics.attempt} " +
"source=${diagnosticPlaybackSource(sourceUrl)} audioSource=${!sourceAudioUrl.isNullOrBlank()} " +
"mime=${mediaItem.localConfiguration?.mimeType ?: "auto"} " +
"startPositionMs=${requestedStartPositionMs ?: 0L}",
)
exoPlayer.setPlaybackMediaItem(mediaItem, requestedStartPositionMs)
if (fallbackStartPositionMs == null) {
initialPositionRequestKey?.let { key ->
latestOnInitialPositionHandled.value(
key,
requestedStartPositionMs != null,
)
}
}
exoPlayer.prepare()
}
@ -451,6 +503,14 @@ private fun ExoPlayerSurface(
val listener = object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
syncPlayerViewKeepScreenOn()
Log.e(
PLAYER_DIAGNOSTIC_TAG,
"error attempt=${playbackDiagnostics.attempt} " +
"elapsedMs=${diagnosticElapsedSince(playbackDiagnostics.prepareStartedAtMs)} " +
"code=${error.errorCodeName} cause=${error.cause?.javaClass?.simpleName ?: "none"} " +
"message=${diagnosticPlayerMessage(error.message)}",
error,
)
val isSourceError = error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW ||
error.errorCode == PlaybackException.ERROR_CODE_IO_UNSPECIFIED ||
@ -503,6 +563,14 @@ private fun ExoPlayerSurface(
else -> "UNKNOWN($playbackState)"
}
Log.d(TAG, "onPlaybackStateChanged: $stateName")
Log.i(
PLAYER_DIAGNOSTIC_TAG,
"state=$stateName attempt=${playbackDiagnostics.attempt} " +
"elapsedMs=${diagnosticElapsedSince(playbackDiagnostics.prepareStartedAtMs)} " +
"positionMs=${exoPlayer.currentPosition.coerceAtLeast(0L)} " +
"bufferedMs=${exoPlayer.bufferedPosition.coerceAtLeast(0L)} " +
"bufferedPercent=${exoPlayer.bufferedPercentage} playWhenReady=${exoPlayer.playWhenReady}",
)
if (playbackState == Player.STATE_READY) {
fallbackStartPositionMs = null
latestOnError.value(null)
@ -517,10 +585,25 @@ private fun ExoPlayerSurface(
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
Log.i(
PLAYER_DIAGNOSTIC_TAG,
"isPlaying=$isPlaying attempt=${playbackDiagnostics.attempt} " +
"elapsedMs=${diagnosticElapsedSince(playbackDiagnostics.prepareStartedAtMs)} " +
"positionMs=${exoPlayer.currentPosition.coerceAtLeast(0L)}",
)
syncPlayerViewKeepScreenOn()
dispatchExoPlayerSnapshot()
}
override fun onRenderedFirstFrame() {
Log.i(
PLAYER_DIAGNOSTIC_TAG,
"firstFrame attempt=${playbackDiagnostics.attempt} " +
"elapsedMs=${diagnosticElapsedSince(playbackDiagnostics.prepareStartedAtMs)} " +
"positionMs=${exoPlayer.currentPosition.coerceAtLeast(0L)}",
)
}
override fun onPlaybackParametersChanged(playbackParameters: androidx.media3.common.PlaybackParameters) {
dispatchExoPlayerSnapshot()
}
@ -1895,6 +1978,26 @@ private fun guessSubtitleMime(url: String): String {
}
}
private fun diagnosticElapsedSince(startedAtMs: Long): Long =
if (startedAtMs <= 0L) -1L else (SystemClock.elapsedRealtime() - startedAtMs).coerceAtLeast(0L)
private fun diagnosticPlaybackSource(value: String): String = runCatching {
val uri = Uri.parse(value)
val host = uri.host.orEmpty()
val isLoopback = host == "127.0.0.1" || host == "localhost" || host == "::1"
"scheme=${uri.scheme ?: "none"},host=${host.ifBlank { "none" }},port=${uri.port},loopback=$isLoopback"
}.getOrDefault("unparseable")
private fun isLoopbackPlaybackSource(value: String): Boolean = runCatching {
when (Uri.parse(value).host.orEmpty().lowercase()) {
"127.0.0.1", "localhost", "::1" -> true
else -> false
}
}.getOrDefault(false)
private fun diagnosticPlayerMessage(value: String?): String =
value?.replace('\n', ' ')?.replace('\r', ' ')?.take(160) ?: "none"
internal class SubtitleRequestHeaderDataSourceFactory(
private val upstreamFactory: DataSource.Factory,
private val externalSubtitles: List<com.nuvio.app.features.streams.StreamSubtitle>,

View file

@ -90,7 +90,10 @@ internal class AndroidPlayerNowPlayingController(
private var metadata: AndroidNowPlayingMetadata? = null
private var snapshot = PlayerPlaybackSnapshot()
private var artworkBitmap: Bitmap? = null
private var artworkArt: Bitmap? = null
private var artworkAlbumArt: Bitmap? = null
private var artworkDisplayIcon: Bitmap? = null
private var artworkNotificationIcon: Bitmap? = null
private var released = false
private var lastPublishedPositionMs = Long.MIN_VALUE
private var lastPublishedDurationMs = Long.MIN_VALUE
@ -126,7 +129,10 @@ internal class AndroidPlayerNowPlayingController(
mediaSession.isActive = true
if (artworkChanged) {
artworkBitmap = null
artworkArt = null
artworkAlbumArt = null
artworkDisplayIcon = null
artworkNotificationIcon = null
loadArtwork(normalized.artworkUrl)
}
@ -184,7 +190,10 @@ internal class AndroidPlayerNowPlayingController(
artworkGeneration.incrementAndGet()
metadata = null
snapshot = PlayerPlaybackSnapshot()
artworkBitmap = null
artworkArt = null
artworkAlbumArt = null
artworkDisplayIcon = null
artworkNotificationIcon = null
resetPublishedPlaybackState()
mediaSession.setMetadata(null)
mediaSession.setPlaybackState(
@ -209,13 +218,23 @@ internal class AndroidPlayerNowPlayingController(
snapshot.durationMs.takeIf { it > 0L }?.let { durationMs ->
builder.putLong(MediaMetadata.METADATA_KEY_DURATION, durationMs)
}
artworkBitmap?.let { artwork ->
builder.putBitmap(MediaMetadata.METADATA_KEY_ART, artwork)
builder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, artwork)
builder.putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, artwork)
val base = builder.build()
val withArtwork = artworkArt?.takeIf { !it.isRecycled }?.let { art ->
MediaMetadata.Builder(base)
.putBitmap(MediaMetadata.METADATA_KEY_ART, art)
.apply {
artworkAlbumArt?.takeIf { !it.isRecycled }
?.let { putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, it) }
artworkDisplayIcon?.takeIf { !it.isRecycled }
?.let { putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, it) }
}
.build()
}
mediaSession.setMetadata(builder.build())
if (withArtwork == null || runCatching { mediaSession.setMetadata(withArtwork) }.isFailure) {
runCatching { mediaSession.setMetadata(base) }
}
}
private fun publishPlaybackState(force: Boolean) {
@ -275,7 +294,7 @@ internal class AndroidPlayerNowPlayingController(
sessionToken = mediaSession.sessionToken,
metadata = currentMetadata,
snapshot = snapshot,
artwork = artworkBitmap,
artwork = artworkNotificationIcon?.takeIf { !it.isRecycled },
)
PlayerNowPlayingService.publish(appContext, notification)
}
@ -291,10 +310,12 @@ internal class AndroidPlayerNowPlayingController(
mainHandler.post {
if (released || generation != artworkGeneration.get() || metadata?.artworkUrl != urlString) {
bitmap?.recycle()
return@post
}
artworkBitmap = bitmap
artworkArt = bitmap
artworkAlbumArt = bitmap?.let(::copyArtwork)
artworkDisplayIcon = bitmap?.let(::copyArtwork)
artworkNotificationIcon = bitmap?.let(::copyArtwork)
publishMetadata()
publishNotification()
}
@ -533,6 +554,9 @@ private fun downloadArtwork(urlString: String): Bitmap? {
}
}
private fun copyArtwork(bitmap: Bitmap): Bitmap? =
if (bitmap.isRecycled) null else runCatching { bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, false) }.getOrNull()
private fun decodeSampledBitmap(bytes: ByteArray, maxEdgePx: Int): Bitmap? {
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds)

View file

@ -60,10 +60,28 @@ internal object PlayerPlaybackNetworking {
.build()
}
fun createHttpDataSourceFactory(defaultHeaders: Map<String, String> = emptyMap()): DataSource.Factory {
private val loopbackPlaybackHttpClient: OkHttpClient by lazy {
playbackHttpClient.newBuilder()
.addInterceptor { chain ->
val request = chain.request()
val requestChain = if (isLoopbackHost(request.url.host)) {
chain.withReadTimeout(65, TimeUnit.SECONDS)
} else {
chain
}
requestChain.proceed(request)
}
.build()
}
fun createHttpDataSourceFactory(
defaultHeaders: Map<String, String> = emptyMap(),
useLongReadTimeout: Boolean = false,
): DataSource.Factory {
val requestHeaders = sanitizeHeaders(defaultHeaders)
val baseClient = if (useLongReadTimeout) loopbackPlaybackHttpClient else playbackHttpClient
val client = requestHeaders.headerValue("Authorization")?.let { authorization ->
playbackHttpClient.newBuilder()
baseClient.newBuilder()
.addNetworkInterceptor { chain ->
val request = chain.request()
if (request.header("Authorization") == null) {
@ -77,7 +95,7 @@ internal object PlayerPlaybackNetworking {
}
}
.build()
} ?: playbackHttpClient
} ?: baseClient
return OkHttpDataSource.Factory(client).apply {
setDefaultRequestProperties(requestHeaders)
@ -90,8 +108,12 @@ internal object PlayerPlaybackNetworking {
fun createDataSourceFactory(
context: Context,
defaultHeaders: Map<String, String> = emptyMap(),
useLongReadTimeout: Boolean = false,
): DataSource.Factory {
return DefaultDataSource.Factory(context, createHttpDataSourceFactory(defaultHeaders))
return DefaultDataSource.Factory(
context,
createHttpDataSourceFactory(defaultHeaders, useLongReadTimeout),
)
}
fun openConnection(
@ -133,6 +155,11 @@ internal object PlayerPlaybackNetworking {
}
}.toMap()
private fun isLoopbackHost(host: String): Boolean = when (host.lowercase()) {
"127.0.0.1", "localhost", "::1" -> true
else -> false
}
private fun withDefaultUserAgent(headers: Map<String, String>): Map<String, String> {
val sanitized = sanitizeHeaders(headers)
if (sanitized.headerValue("User-Agent") != null) return sanitized

View file

@ -14,13 +14,13 @@ internal actual object TraktAuthStorage {
preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
}
actual fun loadPayload(): String? =
preferences?.getString(ProfileScopedKey.of(payloadKey), null)
actual fun loadPayload(profileId: Int): String? =
preferences?.getString(ProfileScopedKey.of(payloadKey, profileId), null)
actual fun savePayload(payload: String) {
actual fun savePayload(profileId: Int, payload: String) {
preferences
?.edit()
?.putString(ProfileScopedKey.of(payloadKey), payload)
?.putString(ProfileScopedKey.of(payloadKey, profileId), payload)
?.apply()
}
}

View file

@ -10,9 +10,13 @@ internal object PlatformPlaybackDataSourceFactory {
defaultRequestHeaders: Map<String, String>,
defaultResponseHeaders: Map<String, String>,
useYoutubeChunkedPlayback: Boolean,
useLongReadTimeout: Boolean = false,
externalSubtitles: List<com.nuvio.app.features.streams.StreamSubtitle> = emptyList(),
): DataSource.Factory {
val httpFactory = PlayerPlaybackNetworking.createHttpDataSourceFactory(defaultRequestHeaders)
val httpFactory = PlayerPlaybackNetworking.createHttpDataSourceFactory(
defaultRequestHeaders,
useLongReadTimeout,
)
val subtitleHeaderFactory = SubtitleRequestHeaderDataSourceFactory(
upstreamFactory = httpFactory,
externalSubtitles = externalSubtitles
@ -27,4 +31,4 @@ internal object PlatformPlaybackDataSourceFactory {
)
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -415,12 +415,14 @@
<string name="compose_settings_page_licenses_attributions">Лицензи и авторство</string>
<string name="compose_settings_page_mdblist_ratings">MDBList оценки</string>
<string name="compose_settings_page_meta_screen">Страница с детайли</string>
<string name="settings_appearance_section_detail_page">СТРАНИЦА С ДЕТАЙЛИ</string>
<string name="compose_settings_page_notifications">Известия</string>
<string name="compose_settings_page_playback">Възпроизвеждане</string>
<string name="compose_settings_page_plugins">Плъгини</string>
<string name="compose_settings_page_poster_customization">Стил на картичката с постер</string>
<string name="compose_settings_page_root">Настройки</string>
<string name="compose_settings_page_streams">Потоци</string>
<string name="settings_appearance_section_streams">ПОТОЦИ</string>
<string name="compose_settings_page_supporters_contributors">Поддръжници и сътрудници</string>
<string name="compose_settings_page_tmdb_enrichment">TMDB обогатяване</string>
<string name="compose_settings_page_trakt">Trakt</string>
@ -561,8 +563,8 @@
<string name="settings_homescreen_section_catalogs">КАТАЛОЗИ</string>
<string name="settings_homescreen_section_catalogs_collections">КАТАЛОЗИ И КОЛЕКЦИИ</string>
<string name="settings_homescreen_section_collections">КОЛЕКЦИИ</string>
<string name="settings_homescreen_section_hero">Оформление на началото</string>
<string name="settings_homescreen_section_hero_sources">Основни каталози</string>
<string name="settings_homescreen_section_hero">ОФОРМЛЕНИЕ НА НАЧАЛОТО</string>
<string name="settings_homescreen_section_hero_sources">ОСНОВНИ КАТАЛОЗИ</string>
<string name="settings_homescreen_selected_count">%1$d от %2$d избрани</string>
<string name="settings_homescreen_show_hero">Покажи основната секция</string>
<string name="settings_homescreen_show_hero_description">Покажи въртележка в горната част на началото.</string>
@ -576,7 +578,7 @@
<string name="settings_hide_secret">Скрий стойността</string>
<string name="settings_playback_subtitle">Плейър, субтитри и автоматично възпроизвеждане</string>
<string name="settings_poster_card_radius">Радиус на ъглите</string>
<string name="settings_poster_card_style">Стил на картичката с постер</string>
<string name="settings_poster_card_style">СТИЛ НА КАРТИЧКАТА С ПОСТЕР</string>
<string name="settings_poster_card_width">Ширина</string>
<string name="settings_poster_custom">Персонализиран</string>
<string name="settings_poster_description">Настрой ширината на картичката и радиуса на ъглите.</string>
@ -611,7 +613,7 @@
<string name="settings_continue_watching_sort_mode_default_desc">Сортирай всички елементи по актуалност</string>
<string name="settings_continue_watching_sort_mode_streaming"> потокинг стил</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">Излезлите елементи първо, предстоящите в края</string>
<string name="settings_continue_watching_section_card_style">Стил на картичката с постер</string>
<string name="settings_continue_watching_section_card_style">СТИЛ НА КАРТИЧКАТА С ПОСТЕР</string>
<string name="settings_continue_watching_section_on_launch">ПРИ СТАРТИРАНЕ</string>
<string name="settings_continue_watching_section_up_next_behavior">ПОВЕДЕНИЕ НА „СЛЕДВАЩО"</string>
<string name="settings_continue_watching_section_visibility">ВИДИМОСТ</string>
@ -634,11 +636,11 @@
<string name="settings_content_discovery_homescreen_description">Настрой оформлението на началото, видимостта на съдържанието и поведението на постерите</string>
<string name="settings_content_discovery_meta_screen_description">Настройки за екраните с детайли и епизоди.</string>
<string name="settings_content_discovery_collections_description">Създавай персонализирани групи от каталози с папки, показвани на началото.</string>
<string name="settings_integrations_section_title">Интеграции</string>
<string name="settings_integrations_section_title">ИНТЕГРАЦИИ</string>
<string name="settings_integrations_tmdb_description">Контроли за обогатяване на метаданни</string>
<string name="settings_integrations_mdblist_description">Доставчици на външни оценки</string>
<string name="settings_integrations_debrid_description">Свързвай акаунти за връзки и достъп до библиотека</string>
<string name="settings_debrid_section_title">Свързани услуги</string>
<string name="settings_debrid_section_title">СВЪРЗАНИ УСЛУГИ</string>
<string name="settings_debrid_experimental_notice">Тези интеграции са експериментални и могат да бъдат запазени, променени или премахнати по-късно.</string>
<string name="settings_debrid_cloud_library">Облачна библиотека</string>
<string name="settings_debrid_cloud_library_description">Разглеждай и пускай файлове вече в свързаните акаунти.</string>
@ -647,7 +649,7 @@
<string name="settings_debrid_resolve_with">Разреши с</string>
<string name="settings_debrid_resolve_with_description">Избери кой свързан акаунт обработва пускащи се връзки.</string>
<string name="settings_debrid_add_key_first">Първо свържете акаунт.</string>
<string name="settings_debrid_section_providers">Акаунти</string>
<string name="settings_debrid_section_providers">АКАУНТИ</string>
<string name="settings_debrid_provider_description">Свържете вашия акаунт в %1$s.</string>
<string name="settings_debrid_provider_device_description">Свържете вашия акаунт в %1$s чрез браузъра.</string>
<string name="settings_debrid_dialog_title">%1$s API ключ</string>
@ -667,26 +669,26 @@
<string name="settings_debrid_device_auth_failed">Не можа да се стартира влизането.</string>
<string name="settings_debrid_device_auth_missing_configuration">Този метод за влизане не е конфигуриран в тази версия.</string>
<string name="settings_debrid_device_auth_expired">Кодът изтече. Опитайте отново.</string>
<string name="settings_debrid_section_instant_playback">Подготовка на връзки</string>
<string name="settings_debrid_section_instant_playback">ПОДГОТОВКА НА ВРЪЗКИ</string>
<string name="settings_debrid_prepare_instant_playback">Подготви връзки</string>
<string name="settings_debrid_prepare_instant_playback_description">Разреши пускащи се връзки преди да започне възпроизвеждането.</string>
<string name="settings_debrid_prepare_stream_count">Връзки за подготовка</string>
<string name="settings_debrid_prepare_stream_count_warning">Използвайте по-малък брой, когато е възможно. Свързаните услуги може да ограничат броя на връзките, които могат да бъдат разрешени за период от време. Отварянето на филм или епизод може да се брои за тези ограничения, дори ако не натиснете Гледай, тъй като връзките се подготвят предварително.</string>
<string name="settings_debrid_prepare_count_one">1 връзка</string>
<string name="settings_debrid_prepare_count_many">%1$d връзки</string>
<string name="settings_debrid_section_formatting">Форматиране</string>
<string name="settings_debrid_section_formatting">ФОРМАТИРАНЕ</string>
<string name="settings_debrid_name_template">Шаблон за ime</string>
<string name="settings_debrid_name_template_description">Контролира как се показват имената на резултатите. Оставете празно, за да използвате оригиналното ime на резултата.</string>
<string name="settings_debrid_description_template">Шаблон за описание</string>
<string name="settings_debrid_description_template_description">Контролира метаданните, показани под всеки резултат. Оставете празно, за да използвате оригиналните детайли на резултата.</string>
<string name="settings_debrid_formatter_reset_title">Нулирай форматирането</string>
<string name="settings_debrid_formatter_reset_subtitle">Възстанови форматирането на резултатите по подразбиране.</string>
<string name="settings_stream_badges_section">Fusion стил</string>
<string name="settings_stream_badges_section">FUSION СТИЛ</string>
<string name="settings_stream_size_badges_title">Значки за размер</string>
<string name="settings_stream_size_badges_description">Покажи значки за размер на файла в резултатите от потоци и панелите с източници на плейъра.</string>
<string name="settings_stream_addon_logo_title">Лого на добавка</string>
<string name="settings_stream_addon_logo_description">Покажи логото и името на добавката до изворите на потоци.</string>
<string name="settings_stream_display_section">Показване</string>
<string name="settings_stream_display_section">ПОКАЗВАНЕ</string>
<string name="settings_stream_badge_position_title">Позиция на значката</string>
<string name="settings_stream_badge_position_description">Изберете дали Fusion и значките за размер да се показват над или под картичките с потоци.</string>
<string name="settings_stream_badge_position_dialog_title">Позиция на значката</string>
@ -717,9 +719,9 @@
<string name="settings_mdb_api_key_title">API ключ</string>
<string name="settings_mdb_enable_ratings">Активирай MDBList оценки</string>
<string name="settings_mdb_enable_ratings_description">Извличай оценки от външни доставчици в екрана с детайли на метаданните</string>
<string name="settings_mdb_section_api_key">API ключ</string>
<string name="settings_mdb_section_rating_providers">Доставчици на външни оценки</string>
<string name="settings_mdb_section_title">MDBList оценки</string>
<string name="settings_mdb_section_api_key">API КЛЮЧ</string>
<string name="settings_mdb_section_rating_providers">ДОСТАВЧИЦИ НА ВЪНШНИ ОЦЕНКИ</string>
<string name="settings_mdb_section_title">MDBList ОЦЕНКИ</string>
<string name="settings_meta_actions">Действия</string>
<string name="settings_meta_actions_description">Контроли за пускане и запазване.</string>
<string name="settings_meta_cast">Актьорски състав</string>
@ -987,7 +989,7 @@
<string name="settings_tmdb_section_credentials">ИДЕНТИФИКАЦИОННИ ДАННИ</string>
<string name="settings_tmdb_section_localization">ЛОКАЛИЗАЦИЯ</string>
<string name="settings_tmdb_section_modules">МОДУЛИ</string>
<string name="settings_tmdb_section_title">TMDB обогатяване</string>
<string name="settings_tmdb_section_title">TMDB ОБОГАТЯВАНЕ</string>
<string name="settings_trakt_approval_redirect">След одобрение, ще бъдете пренасочени автоматично.</string>
<string name="settings_trakt_authentication">УДОСТОВЕРЯВАНЕ</string>
<string name="settings_trakt_comments">Коментари</string>
@ -1577,7 +1579,7 @@
<string name="player_ios_preset_custom_label">Персонализиран</string>
<string name="player_ios_preset_custom_desc">Използвай разширените стойности по-долу.</string>
<string name="player_ios_hardware_decoder_off">Изкл.</string>
<string name="settings_playback_ios_video_output">iOS видео изход</string>
<string name="settings_playback_ios_video_output">iOS ВИДЕО ИЗХОД</string>
<string name="settings_playback_ios_hardware_decoder">Хардуерен декодер</string>
<string name="settings_playback_ios_extended_dynamic_range">Разширен динамичен обхват</string>
<string name="settings_playback_ios_extended_dynamic_range_desc">Режим на Metal изход по подразбиране за нови сесии на възпроизвеждане.</string>
@ -1585,12 +1587,12 @@
<string name="settings_playback_ios_display_color_hint_desc">Нека mpv насочва цветовото пространство на активния дисплей по подразбиране.</string>
<string name="settings_playback_ios_target_primaries">Целеви примари</string>
<string name="settings_playback_ios_target_transfer">Целево пренасяне</string>
<string name="settings_playback_ios_audio_output_section">iOS аудио изход</string>
<string name="settings_playback_ios_audio_output_section">iOS АУДИО ИЗХОД</string>
<string name="settings_playback_ios_audio_output">Аудио изход</string>
<string name="settings_playback_ios_audio_output_auto_desc">Опитай AVFoundation първо, след това се върни към AudioUnit.</string>
<string name="settings_playback_ios_audio_output_avfoundation_desc">Експериментална поддръжка за Spatial Audio и многоканален изход.</string>
<string name="settings_playback_ios_audio_output_audiounit_desc">Използвай стария AudioUnit изход.</string>
<string name="settings_debrid_section_result_management">Управление на резултатите</string>
<string name="settings_debrid_section_result_management">УПРАВЛЕНИЕ НА РЕЗУЛТАТИТЕ</string>
<string name="settings_debrid_max_results">Макс. резултати</string>
<string name="settings_debrid_max_results_desc">Ограничи колко резултата се показват.</string>
<string name="settings_debrid_sort_results">Сортирай резултатите</string>
@ -1834,6 +1836,7 @@
<string name="p2p_consent_enable">Активирай P2P</string>
<string name="p2p_consent_cancel">Отказ</string>
<string name="settings_p2p_title">P2P потокинг</string>
<string name="settings_playback_section_p2p">P2P ПОТОКИНГ</string>
<string name="settings_p2p_subtitle">Разрешете peer-to-peer (торент) потоци</string>
<string name="settings_p2p_hide_stats_title">Скрий статистиките на торента</string>
<string name="settings_p2p_hide_stats_subtitle">Скрий буфера, сийдовете, пиъровете и скоростта на изтегляне по време на зареждане и възпроизвеждане</string>

View file

@ -426,12 +426,14 @@
<string name="compose_settings_page_licenses_attributions">Licence &amp; autorství</string>
<string name="compose_settings_page_mdblist_ratings">Hodnocení MDBList</string>
<string name="compose_settings_page_meta_screen">Stránka podrobností</string>
<string name="settings_appearance_section_detail_page">STRÁNKA PODROBNOSTÍ</string>
<string name="compose_settings_page_notifications">Oznámení</string>
<string name="compose_settings_page_playback">Přehrávání</string>
<string name="compose_settings_page_plugins">Pluginy</string>
<string name="compose_settings_page_poster_customization">Styl karty plakátu</string>
<string name="compose_settings_page_root">Nastavení</string>
<string name="compose_settings_page_streams">Streamy</string>
<string name="settings_appearance_section_streams">STREAMY</string>
<string name="compose_settings_page_supporters_contributors">Podporovatelé a přispěvatelé</string>
<string name="compose_settings_page_tmdb_enrichment">Obohacení TMDB</string>
<string name="compose_settings_page_trakt">Trakt</string>
@ -573,8 +575,8 @@
<string name="settings_homescreen_section_catalogs">KATALOGY</string>
<string name="settings_homescreen_section_catalogs_collections">KATALOGY A SBÍRKY</string>
<string name="settings_homescreen_section_collections">SBÍRKY</string>
<string name="settings_homescreen_section_hero">Rozvržení domů</string>
<string name="settings_homescreen_section_hero_sources">Katalogy pro Hero</string>
<string name="settings_homescreen_section_hero">ROZVRŽENÍ DOMŮ</string>
<string name="settings_homescreen_section_hero_sources">KATALOGY PRO HERO</string>
<string name="settings_homescreen_selected_count">%1$d z %2$d vybráno</string>
<string name="settings_homescreen_show_hero">Zobrazit Hero sekci</string>
<string name="settings_homescreen_show_hero_description">Zobrazit hlavní karusel v horní části domovské stránky.</string>
@ -588,7 +590,7 @@
<string name="settings_hide_secret">Skrýt hodnotu</string>
<string name="settings_playback_subtitle">Přehrávač, titulky a automatické přehrávání</string>
<string name="settings_poster_card_radius">Poloměr rohů</string>
<string name="settings_poster_card_style">Styl karty plakátu</string>
<string name="settings_poster_card_style">STYL KARTY PLAKÁTU</string>
<string name="settings_poster_card_width">Šířka</string>
<string name="settings_poster_custom">Vlastní</string>
<string name="settings_poster_description">Vyladit šířku karty a poloměr rohů.</string>
@ -623,7 +625,7 @@
<string name="settings_continue_watching_sort_mode_default_desc">Seřadit všechny položky podle nejnovějších</string>
<string name="settings_continue_watching_sort_mode_streaming">Styl streamování</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">Nejprve vydané položky, nadcházející na konci</string>
<string name="settings_continue_watching_section_card_style">Styl karty plakátu</string>
<string name="settings_continue_watching_section_card_style">STYL KARTY PLAKÁTU</string>
<string name="settings_continue_watching_section_on_launch">PŘI SPUŠTĚNÍ</string>
<string name="settings_continue_watching_section_up_next_behavior">CHOVÁNÍ "DALŠÍ NA ŘADĚ"</string>
<string name="settings_continue_watching_section_visibility">VIDITELNOST</string>
@ -647,11 +649,11 @@
<string name="settings_content_discovery_homescreen_description">Upravit rozvržení domovské obrazovky, viditelnost obsahu a chování plakátů.</string>
<string name="settings_content_discovery_meta_screen_description">Nastavení pro obrazovku detailu a epizody.</string>
<string name="settings_content_discovery_collections_description">Vytvářet vlastní seskupení katalogů se složkami na domovské obrazovce.</string>
<string name="settings_integrations_section_title">Integrace</string>
<string name="settings_integrations_section_title">INTEGRACE</string>
<string name="settings_integrations_tmdb_description">Ovládání pro obohacování metadat</string>
<string name="settings_integrations_mdblist_description">Externí poskytovatelé hodnocení</string>
<string name="settings_integrations_debrid_description">Připojit účty pro odkazy a přístup ke knihovně</string>
<string name="settings_debrid_section_title">Připojené služby</string>
<string name="settings_debrid_section_title">PŘIPOJENÉ SLUŽBY</string>
<string name="settings_debrid_experimental_notice">Tyto integrace jsou experimentální a mohou být zachovány, změněny nebo později odstraněny.</string>
<string name="settings_debrid_cloud_library">Cloudová knihovna</string>
<string name="settings_debrid_cloud_library_description">Procházet a přehrávat soubory, které už jsou ve vašich připojených účtech.</string>
@ -660,7 +662,7 @@
<string name="settings_debrid_resolve_with">Zpracovávat pomocí</string>
<string name="settings_debrid_resolve_with_description">Vyberte, který připojený účet bude zpracovávat přehrávatelné odkazy.</string>
<string name="settings_debrid_add_key_first">Nejprve připojte účet.</string>
<string name="settings_debrid_section_providers">Účty</string>
<string name="settings_debrid_section_providers">ÚČTY</string>
<string name="settings_debrid_provider_description">Připojte svůj účet na %1$s.</string>
<string name="settings_debrid_provider_device_description">Propojte svůj %1$s účet v prohlížeči.</string>
<string name="settings_debrid_dialog_title">API klíč k %1$s</string>
@ -680,26 +682,26 @@
<string name="settings_debrid_device_auth_failed">Nepodařilo se zahájit přihlášení.</string>
<string name="settings_debrid_device_auth_missing_configuration">Tato metoda přihlášení není v tomto buildu nakonfigurována.</string>
<string name="settings_debrid_device_auth_expired">Tento kód vypršel. Zkuste to znovu.</string>
<string name="settings_debrid_section_instant_playback">Příprava odkazů</string>
<string name="settings_debrid_section_instant_playback">PŘÍPRAVA ODKAZŮ</string>
<string name="settings_debrid_prepare_instant_playback">Připravit odkazy</string>
<string name="settings_debrid_prepare_instant_playback_description">Zpracovat přehrávatelné odkazy dříve, než začne přehrávání.</string>
<string name="settings_debrid_prepare_stream_count">Odkazy k přípravě</string>
<string name="settings_debrid_prepare_stream_count_warning">Kdykoli je to možné, použijte nižší počet. Připojené služby mohou omezovat, kolik odkazů lze vyhodnotit za určité časové období. Otevření filmu nebo epizody se může započítat do těchto limitů, i když nestisknete Přehrát, protože odkazy se připravují předem.</string>
<string name="settings_debrid_prepare_count_one">1 odkaz</string>
<string name="settings_debrid_prepare_count_many">%1$d odkazů</string>
<string name="settings_debrid_section_formatting">Formátování</string>
<string name="settings_debrid_section_formatting">FORMÁTOVÁNÍ</string>
<string name="settings_debrid_name_template">Šablona názvu</string>
<string name="settings_debrid_name_template_description">Ovládá, jak se budou názvy výsledků zobrazovat. Ponechte prázdné pro použití původního názvu výsledku.</string>
<string name="settings_debrid_description_template">Šablona popisu</string>
<string name="settings_debrid_description_template_description">Ovládá metadata zobrazená pod každým výsledkem. Ponechte prázdné pro použití původních detailů výsledku.</string>
<string name="settings_debrid_formatter_reset_title">Obnovit formátování</string>
<string name="settings_debrid_formatter_reset_subtitle">Obnovit výchozí formátování výsledků.</string>
<string name="settings_stream_badges_section">Styl Fusion</string>
<string name="settings_stream_badges_section">STYL FUSION</string>
<string name="settings_stream_size_badges_title">Odznaky velikosti</string>
<string name="settings_stream_size_badges_description">Zobrazovat odznaky velikosti souboru ve výsledcích streamu a na panelech zdroje v přehrávači.</string>
<string name="settings_stream_addon_logo_title">Logo doplňku</string>
<string name="settings_stream_addon_logo_description">Zobrazovat logo a název doplňku vedle zdrojů streamů.</string>
<string name="settings_stream_display_section">Zobrazení</string>
<string name="settings_stream_display_section">ZOBRAZENÍ</string>
<string name="settings_stream_badge_position_title">Pozice odznaku</string>
<string name="settings_stream_badge_position_description">Vyberte si, zda se budou odznaky Fusion a velikosti zobrazovat nad nebo pod kartami streamu.</string>
<string name="settings_stream_badge_position_dialog_title">Pozice odznaku</string>
@ -734,9 +736,9 @@
<string name="settings_mdb_api_key_title">API klíč</string>
<string name="settings_mdb_enable_ratings">Povolit hodnocení MDBList</string>
<string name="settings_mdb_enable_ratings_description">Načítat hodnocení od externích poskytovatelů na obrazovce podrobností metadat</string>
<string name="settings_mdb_section_api_key">API klíč</string>
<string name="settings_mdb_section_rating_providers">Externí poskytovatelé hodnocení</string>
<string name="settings_mdb_section_title">Hodnocení MDBList</string>
<string name="settings_mdb_section_api_key">API KLÍČ</string>
<string name="settings_mdb_section_rating_providers">EXTERNÍ POSKYTOVATELÉ HODNOCENÍ</string>
<string name="settings_mdb_section_title">HODNOCENÍ MDBList</string>
<string name="settings_meta_actions">Akce</string>
<string name="settings_meta_actions_description">Ovládání přehrávání a ukládání.</string>
<string name="settings_meta_cast">Obsazení</string>
@ -1024,7 +1026,7 @@
<string name="settings_tmdb_section_credentials">POVĚŘENÍ</string>
<string name="settings_tmdb_section_localization">LOKALIZACE</string>
<string name="settings_tmdb_section_modules">MODULY</string>
<string name="settings_tmdb_section_title">Obohacení TMDB</string>
<string name="settings_tmdb_section_title">OBOHACENÍ TMDB</string>
<string name="settings_trakt_approval_redirect">Po schválení budete automaticky přesměrováni zpět.</string>
<string name="settings_trakt_authentication">OVĚŘENÍ (AUTHENTICATION)</string>
<string name="settings_trakt_comments">Komentáře</string>
@ -1633,7 +1635,7 @@
<string name="player_ios_preset_custom_desc">Použije vaše pokročilé hodnoty níže.</string>
<string name="player_ios_hardware_decoder_off">Vyp</string>
<!-- Playback settings iOS video output -->
<string name="settings_playback_ios_video_output">Výstup videa pro iOS</string>
<string name="settings_playback_ios_video_output">VÝSTUP VIDEA PRO iOS</string>
<string name="settings_playback_ios_hardware_decoder">Hardwarový dekodér</string>
<string name="settings_playback_ios_extended_dynamic_range">Rozšířený dynamický rozsah</string>
<string name="settings_playback_ios_extended_dynamic_range_desc">Výchozí režim výstupu Metal pro nové relace přehrávání.</string>
@ -1642,13 +1644,13 @@
<string name="settings_playback_ios_target_primaries">Cílové základní barvy</string>
<string name="settings_playback_ios_target_transfer">Cílový přenos</string>
<!-- Playback settings iOS audio output -->
<string name="settings_playback_ios_audio_output_section">Výstup zvuku pro iOS</string>
<string name="settings_playback_ios_audio_output_section">VÝSTUP ZVUKU PRO iOS</string>
<string name="settings_playback_ios_audio_output">Výstup zvuku</string>
<string name="settings_playback_ios_audio_output_auto_desc">Použít AudioUnit, dokud je výstup AVFoundation dočasně zakázán.</string>
<string name="settings_playback_ios_audio_output_avfoundation_desc">Experimentální podpora pro prostorový zvuk (Spatial Audio) a vícekanálový výstup.</string>
<string name="settings_playback_ios_audio_output_audiounit_desc">Použít starší výstup AudioUnit.</string>
<!-- Debrid Result Management section -->
<string name="settings_debrid_section_result_management">Správa výsledků</string>
<string name="settings_debrid_section_result_management">SPRÁVA VÝSLEDKŮ</string>
<string name="settings_debrid_max_results">Max výsledků</string>
<string name="settings_debrid_max_results_desc">Omezit, kolik výsledků se zobrazí.</string>
<string name="settings_debrid_sort_results">Seřadit výsledky</string>
@ -1916,6 +1918,7 @@
<string name="p2p_consent_cancel">Zrušit</string>
<string name="p2p_error_unknown">Neznámá chyba torrentu</string>
<string name="settings_p2p_title">P2P Streamování</string>
<string name="settings_playback_section_p2p">P2P STREAMOVÁNÍ</string>
<string name="settings_p2p_subtitle">Povolit peer-to-peer (torrent) streamy</string>
<string name="settings_p2p_hide_stats_title">Skrýt statistiky torrentu</string>
<string name="settings_p2p_hide_stats_subtitle">Skrýt načítání (buffer), seedery, peery a rychlost stahování během načítání a přehrávání</string>

View file

@ -375,6 +375,7 @@
<string name="compose_settings_page_integrations">Integrationen</string>
<string name="compose_settings_page_mdblist_ratings">MDBList-Bewertungen</string>
<string name="compose_settings_page_meta_screen">Meta-Bildschirm</string>
<string name="settings_appearance_section_detail_page">META-BILDSCHIRM</string>
<string name="compose_settings_page_notifications">Benachrichtigungen</string>
<string name="compose_settings_page_playback">Wiedergabe</string>
<string name="compose_settings_page_plugins">Plugins</string>

View file

@ -218,6 +218,7 @@
<string name="compose_settings_page_integrations">Ενσωματώσεις</string>
<string name="compose_settings_page_mdblist_ratings">Αξιολογήσεις MDBList</string>
<string name="compose_settings_page_meta_screen">Οθόνη μεταδεδομένων</string>
<string name="settings_appearance_section_detail_page">ΟΘΌΝΗ ΜΕΤΑΔΕΔΟΜΈΝΩΝ</string>
<string name="compose_settings_page_notifications">Ειδοποιήσεις</string>
<string name="compose_settings_page_playback">Αναπαραγωγή</string>
<string name="compose_settings_page_plugins">Πρόσθετα εφαρμογών</string>
@ -355,7 +356,7 @@
<string name="settings_homescreen_summary_hint">Ανοίξτε έναν κατάλογο μόνο όταν χρειάζεστε να τον μετονομάσετε ή να τον αναδιατάξετε.</string>
<string name="settings_homescreen_visible">Ορατό</string>
<string name="settings_playback_subtitle">Αναπαραγωγή, υπότιτλοι και αυτόματη εκκίνηση</string>
<string name="settings_card_depth_title">Εφέ βάθους κάρτας</string>
<string name="settings_card_depth_title">ΕΦΈ ΒΆΘΟΥΣ ΚΆΡΤΑΣ</string>
<string name="settings_card_depth_description">Προσθέτει φωτεινό επάνω άκρο και απαλή λάμψη στις κάρτες εικόνας για αίσθηση βάθους.</string>
<string name="settings_card_depth_enabled">Ενεργοποίηση εφέ βάθους</string>
<string name="settings_card_depth_edge">Λάμψη άκρου</string>
@ -693,6 +694,7 @@
<string name="source_rotten_tomatoes">Rotten Tomatoes</string>
<string name="source_tmdb">TMDB</string>
<string name="source_trakt">Trakt</string>
<string name="source_mal">MyAnimeList</string>
<string name="subtitle_language_unknown">Άγνωστο</string>
<string name="theme_amber">Κεχριμπάρι</string>
<string name="theme_crimson">Κόκκινο</string>
@ -1078,8 +1080,19 @@
<string name="home_catalog_default_title">%1$s - %2$s</string>
<string name="library_empty_message">Οι αποθηκευμένοι τίτλοι θα εμφανιστούν εδώ αφού πατήσετε Αποθήκευση σε μια οθόνη λεπτομερειών.</string>
<string name="library_empty_title">Η βιβλιοθήκη σας είναι κενή</string>
<string name="library_filter_all_types">Όλοι οι τύποι</string>
<string name="library_filter_list">Επιλογή λίστας</string>
<string name="library_filter_sort">Ταξινόμηση βιβλιοθήκης</string>
<string name="library_filter_type">Επιλογή τύπου</string>
<string name="library_layout_show_horizontal">Εμφάνιση οριζόντιων ραφιών</string>
<string name="library_layout_show_vertical">Εμφάνιση κατακόρυφου πλέγματος</string>
<string name="library_load_failed">Αδυναμία φόρτωσης βιβλιοθήκης</string>
<string name="library_other">Άλλο</string>
<string name="library_sort_added_asc">Παλαιότερη προσθήκη</string>
<string name="library_sort_added_desc">Πρόσφατη προσθήκη</string>
<string name="library_sort_title_asc">Τίτλος Α–Ω</string>
<string name="library_sort_title_desc">Τίτλος Ω–Α</string>
<string name="library_sort_trakt_order">Σειρά Trakt</string>
<string name="library_title">Βιβλιοθήκη</string>
<string name="library_trakt_empty_message">Συνδέστε το Trakt και αποθηκεύστε τίτλους στη λίστα παρακολούθησης ή σε προσωπικές λίστες.</string>
<string name="library_trakt_empty_title">Η βιβλιοθήκη Trakt σας είναι κενή</string>
@ -1144,7 +1157,7 @@
<string name="cloud_library_empty_title">Τίποτα εδώ ακόμα</string>
<string name="cloud_library_file_picker_title">Επιλέξτε ένα αρχείο για αναπαραγωγή</string>
<string name="cloud_library_load_failed">Δεν ήταν δυνατή η φόρτωση της βιβλιοθήκης Cloud %1$s</string>
<string name="cloud_library_no_files_message">Αυτό το στοιχείο δεν εκθέτει ένα αναπαιγαίμο αρχείο βίντεο.</string>
<string name="cloud_library_no_files_message">Αυτό το στοιχείο δεν εκθέτει ένα αναπαίξιμο αρχείο βίντεο.</string>
<string name="cloud_library_no_files_title">Δεν υπάρχουν αναπαίξιμα αρχεία</string>
<string name="cloud_library_no_playable_files">Δεν υπάρχουν αναπαίξιμα αρχεία</string>
<string name="cloud_library_play_disabled">Η βιβλιοθήκη Cloud είναι απενεργοποιημένη.</string>
@ -1157,6 +1170,7 @@
<string name="cloud_library_provider_all">Όλα</string>
<string name="cloud_library_provider_unavailable">Η βιβλιοθήκη Cloud δεν είναι διαθέσιμη για %1$s.</string>
<string name="cloud_library_refresh">Ανανέωση βιβλιοθήκης Cloud</string>
<string name="cloud_library_search_label">Αναζήτηση βιβλιοθήκης Cloud</string>
<string name="cloud_library_select_provider">Επιλογή παρόχου</string>
<string name="cloud_library_select_type">Επιλογή τύπου</string>
<string name="cloud_library_status_ready">Έτοιμο για αναπαραγωγή</string>
@ -1419,6 +1433,7 @@
<string name="compose_settings_page_debrid">Συνδεδεμένος Services</string>
<string name="compose_settings_page_licenses_attributions">Άδειες και αποδόσεις</string>
<string name="compose_settings_page_streams">Ροές</string>
<string name="settings_appearance_section_streams">ΡΟΈΣ</string>
<string name="compose_settings_root_advanced_description">Συμπεριφορά εκκίνησης και προφίλ.</string>
<string name="compose_settings_root_advanced_section">ΓΙΑ ΠΡΟΧΩΡΗΜΕΝΟΥΣ</string>
<string name="compose_settings_root_streams_description">Εμφάνιση αποτελεσμάτων ροής και κανόνες URL badge.</string>
@ -1452,7 +1467,7 @@
<string name="external_player_unavailable">Δεν υπάρχει διαθέσιμος εξωτερικός player</string>
<string name="home_continue_watching_hours_minutes_left">απομένουν %1$dω %2$dλ</string>
<string name="home_continue_watching_minutes_left">απομένουν %1$dλ</string>
<string name="layout_hide_unreleased">Απόκρυψη Unreleased Content</string>
<string name="layout_hide_unreleased">Απόκρυψη μη κυκλοφορημένου περιεχομένου</string>
<string name="layout_hide_unreleased_sub">Απόκρυψη ταινιών και σειρών που δεν έχουν κυκλοφορήσει ακόμα.</string>
<string name="layout_catalog_type">Εμφάνιση τύπου καταλόγου</string>
<string name="layout_catalog_type_sub">Εμφάνιση επιθήματος τύπου δίπλα στο όνομα καταλόγου (Ταινία/Σειρές).</string>
@ -1489,7 +1504,7 @@
<string name="player_engine_unavailable_rebuild">Ο μηχανισμός αναπαραγωγής MPV δεν είναι διαθέσιμος. Παρακαλώ κάντε rebuild την εφαρμογή.</string>
<string name="player_error_failed_start_torrent">Αποτυχία εκκίνησης torrent: %1$s</string>
<string name="player_error_mpv_unavailable">Ο μηχανισμός αναπαραγωγής MPV δεν είναι διαθέσιμος. Παρακαλώ κάντε rebuild την εφαρμογή.</string>
<string name="player_error_torrent">Torrent Σφάλμα: %1$s</string>
<string name="player_error_torrent">Σφάλμα torrent: %1$s</string>
<string name="player_error_unable_to_play_stream">Δεν είναι δυνατή η αναπαραγωγή αυτής της ροής.</string>
<string name="player_external_downloading_subtitles">Λήψη υποτίτλων...</string>
<string name="player_external_loading_subtitles">Φόρτωση υποτίτλων από πρόσθετα...</string>
@ -1502,10 +1517,14 @@
<string name="player_ios_preset_native_edr_label">Εγγενές EDR</string>
<string name="player_ios_preset_sdr_tone_mapped_desc">Πιο προβλέψιμα λευκά και μαύρα σε έξοδο τύπου SDR.</string>
<string name="player_ios_preset_sdr_tone_mapped_label">Χαρτογράφηση τόνου SDR</string>
<string name="player_torrent_buffered_status">%1$s buffered · %2$s · %3$s</string>
<string name="player_torrent_buffered_status">%1$s στην προσωρινή μνήμη · %2$s · %3$s</string>
<string name="player_torrent_connecting_peers">Σύνδεση με peers…</string>
<string name="player_torrent_peer_info">%1$d seeds · %2$d peers</string>
<string name="player_torrent_starting_engine">Εκκίνηση μηχανισμού P2P…</string>
<string name="player_torrent_fetching_metadata">Λήψη μεταδεδομένων torrent…</string>
<string name="player_torrent_preparing_stream">Προετοιμασία ροής torrent…</string>
<string name="player_torrent_connecting_status">%1$s · %2$s · %3$s</string>
<string name="player_torrent_loading_status">%1$s ληφθέντα · %2$s · %3$s</string>
<string name="player_torrent_stats">%1$d peers · %2$d seeds · %3$d%%</string>
<string name="player_torrent_status">%1$s · %2$s</string>
<string name="player_video_settings_brightness">Φωτεινότητα</string>
@ -1584,7 +1603,7 @@
<string name="settings_advanced_clear_cw_cache_subtitle">Κατάργηση αποθηκευμένων δεδομένων Συνέχειας παρακολούθησης και ανανέωση προόδου παρακολούθησης</string>
<string name="settings_advanced_remember_last_profile">Απομνημόνευση τελευταίου προφίλ</string>
<string name="settings_advanced_remember_last_profile_description">Απομνημόνευση του τελευταίου επιλεγμένου προφίλ κατά την εκκίνηση</string>
<string name="settings_advanced_section_cache">Προσωρινή μνήμη</string>
<string name="settings_advanced_section_cache">ΠΡΟΣΩΡΙΝΉ ΜΝΉΜΗ</string>
<string name="settings_advanced_section_startup">ΕΚΚΙΝΗΣΗ</string>
<string name="settings_appearance_app_language_device">Γλώσσα συσκευής</string>
<string name="settings_appearance_liquid_glass">Υγρό γυαλί</string>
@ -1599,6 +1618,9 @@
<string name="settings_continue_watching_sort_mode_default_desc">Ταξινόμηση όλων των στοιχείων κατά πρόσφατη δραστηριότητα</string>
<string name="settings_continue_watching_sort_mode_streaming">Στυλ streaming</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">Πρώτα τα κυκλοφορημένα, τα επερχόμενα στο τέλος</string>
<string name="settings_continue_watching_sort_mode_split_upcoming">Ξεχωριστή σειρά επερχόμενων</string>
<string name="settings_continue_watching_sort_mode_split_upcoming_desc">Μετακίνηση επερχόμενων επεισοδίων σε ξεχωριστή σειρά Επερχόμενων</string>
<string name="upcoming_section_title">Επερχόμενα</string>
<string name="settings_continue_watching_sort_mode_title">Σειρά ταξινόμησης</string>
<string name="settings_continue_watching_style_card">Κάρτα</string>
<string name="settings_continue_watching_style_card_description">Οριζόντια κάρτα σε στυλ τηλεόρασης</string>
@ -1701,11 +1723,11 @@
<string name="settings_debrid_rule_required_resolutions_desc">Εμφάνιση μόνο των επιλεγμένων αναλύσεων.</string>
<string name="settings_debrid_rule_required_visual_tags">Απαιτούμενες οπτικές ετικέτες</string>
<string name="settings_debrid_rule_required_visual_tags_desc">Απαιτεί ετικέτες DV, HDR, 10bit, IMAX, SDR και παρόμοιες.</string>
<string name="settings_debrid_section_formatting">Μορφοποίηση</string>
<string name="settings_debrid_section_instant_playback">Προετοιμασία συνδέσμων</string>
<string name="settings_debrid_section_providers">Λογαριασμοί</string>
<string name="settings_debrid_section_result_management">Διαχείριση αποτελεσμάτων</string>
<string name="settings_debrid_section_title">Συνδεδεμένος Services</string>
<string name="settings_debrid_section_formatting">ΜΟΡΦΟΠΟΊΗΣΗ</string>
<string name="settings_debrid_section_instant_playback">ΠΡΟΕΤΟΙΜΑΣΊΑ ΣΥΝΔΈΣΜΩΝ</string>
<string name="settings_debrid_section_providers">ΛΟΓΑΡΙΑΣΜΟΊ</string>
<string name="settings_debrid_section_result_management">ΔΙΑΧΕΊΡΙΣΗ ΑΠΟΤΕΛΕΣΜΆΤΩΝ</string>
<string name="settings_debrid_section_title">ΣΥΝΔΕΔΕΜΈΝΟΣ SERVICES</string>
<string name="settings_debrid_selection_any">Οποιοδήποτε</string>
<string name="settings_debrid_selection_count">%1$d επιλεγμένα</string>
<string name="settings_debrid_size_min">%1$dGB+</string>
@ -1759,7 +1781,7 @@
<string name="settings_licenses_attributions_premiumize_title">Premiumize</string>
<string name="settings_licenses_attributions_section_app">ΑΔΕΙΑ ΕΦΑΡΜΟΓΗΣ</string>
<string name="settings_licenses_attributions_section_data">ΔΕΔΟΜΕΝΑ &amp; ΥΠΗΡΕΣΙΕΣ</string>
<string name="settings_licenses_attributions_section_playback">Αναπαραγωγή LICENSE</string>
<string name="settings_licenses_attributions_section_playback">ΑΝΑΠΑΡΑΓΩΓΉ LICENSE</string>
<string name="settings_licenses_attributions_tmdb_body">Το Nuvio χρησιμοποιεί το API του TMDB για μεταδεδομένα ταινιών και σειρών, εικαστικό υλικό, trailer, συμμετέχοντες, λεπτομέρειες παραγωγής, συλλογές και προτάσεις. Αυτό το προϊόν χρησιμοποιεί το API του TMDB αλλά δεν είναι εγκεκριμένο ούτε πιστοποιημένο από το TMDB.</string>
<string name="settings_licenses_attributions_tmdb_title">The Ταινία Database (TMDB)</string>
<string name="settings_licenses_attributions_torbox_body">Το Nuvio συνδέεται με το TorBox για ταυτοποίηση λογαριασμού, πρόσβαση σε βιβλιοθήκη Cloud, ελέγχους προσωρινής μνήμης και λειτουργίες αναπαραγωγής από το Cloud. Το Nuvio δεν σχετίζεται με το TorBox ούτε έχει την υποστήριξή του.</string>
@ -1779,9 +1801,29 @@
<string name="settings_meta_hero_trailer_playback">Hero Trailer Αναπαραγωγή</string>
<string name="settings_meta_hero_trailer_playback_description">Αναπαραγωγή προεπισκοπήσεων trailer στο κύριο πλαίσιο μεταδεδομένων όταν υπάρχει διαθέσιμο trailer.</string>
<string name="settings_p2p_hide_stats_subtitle">Απόκρυψη buffer, seeds, peers και ταχύτητας λήψης κατά τη φόρτωση και την αναπαραγωγή</string>
<string name="settings_p2p_hide_stats_title">Απόκρυψη torrent stats</string>
<string name="settings_p2p_profile_title">Προφίλ torrent</string>
<string name="settings_p2p_profile_soft">Ήπιο</string>
<string name="settings_p2p_profile_soft_description">Χρησιμοποιεί λιγότερες συνδέσεις peers για μείωση φόρτου μπαταρίας και δικτύου</string>
<string name="settings_p2p_profile_balanced">Ισορροπημένο</string>
<string name="settings_p2p_profile_balanced_description">Προτεινόμενη ισορροπία μεταξύ ταχύτητας εκκίνησης και σταθερότητας σύνδεσης</string>
<string name="settings_p2p_profile_fast">Γρήγορο</string>
<string name="settings_p2p_profile_fast_description">Χρησιμοποιεί περισσότερες συνδέσεις peers για δύσκολα ή υψηλού bitrate torrents</string>
<string name="settings_p2p_cache_size_title">Μέγεθος προσωρινής μνήμης torrent</string>
<string name="settings_p2p_cache_none">Χωρίς μόνιμη προσωρινή μνήμη</string>
<string name="settings_p2p_cache_2_gb">2 GB</string>
<string name="settings_p2p_cache_5_gb">5 GB</string>
<string name="settings_p2p_cache_10_gb">10 GB</string>
<string name="settings_p2p_clear_cache_title">Εκκαθάριση προσωρινής μνήμης torrent</string>
<string name="settings_p2p_clear_cache_usage">Χρησιμοποιούνται %1$s αυτή τη στιγμή</string>
<string name="settings_p2p_clear_cache_usage_pending">Η χρήση μετράται μετά την εκκίνηση του P2P· πατήστε για εκκαθάριση αποθηκευμένων δεδομένων torrent</string>
<string name="settings_p2p_clear_cache_clearing">Εκκαθάριση ανενεργών δεδομένων torrent…</string>
<string name="settings_p2p_clear_cache_playback_active">Διακόψτε την αναπαραγωγή P2P πριν εκκαθαρίσετε την προσωρινή μνήμη</string>
<string name="settings_p2p_clear_cache_done">Εκκαθαρίστηκαν %1$s ανενεργών δεδομένων torrent</string>
<string name="settings_p2p_clear_cache_failed">Δεν ήταν δυνατή η εκκαθάριση της προσωρινής μνήμης torrent</string>
<string name="settings_p2p_hide_stats_title">Απόκρυψη στατιστικών torrent</string>
<string name="settings_p2p_subtitle">Επιτρέψτε ροές peer-to-peer (torrent)</string>
<string name="settings_p2p_title">Ροές P2P</string>
<string name="settings_playback_section_p2p">ΡΟΈΣ P2P</string>
<string name="settings_playback_addon_subtitle_startup_all">Όλοι οι υπότιτλοι</string>
<string name="settings_playback_addon_subtitle_startup_all_description">Λήψη και εμφάνιση κάθε υπότιτλου πρόσθετου για το βίντεο.</string>
<string name="settings_playback_addon_subtitle_startup_fast">Γρήγορη εκκίνηση</string>
@ -1813,7 +1855,7 @@
<string name="settings_playback_ios_audio_output_auto_desc">Χρήση AudioUnit ενώ η έξοδος AVFoundation είναι προσωρινά απενεργοποιημένη.</string>
<string name="settings_playback_ios_audio_output_avfoundation_desc">Πειραματική υποστήριξη για Spatial Audio και πολυκάναλη έξοδο.</string>
<string name="settings_playback_ios_audio_output_dialog">Ήχος output</string>
<string name="settings_playback_ios_audio_output_section">iOS Ήχος output</string>
<string name="settings_playback_ios_audio_output_section">iOS ΉΧΟΣ OUTPUT</string>
<string name="settings_playback_ios_display_color_hint">Οθόνη color hint</string>
<string name="settings_playback_ios_display_color_hint_desc">Επιτρέψτε στο mpv να στοχεύει τον ενεργό χρωματικό χώρο της οθόνης από προεπιλογή.</string>
<string name="settings_playback_ios_extended_dynamic_range">Εκτεταμένο δυναμικό εύρος</string>
@ -1824,7 +1866,7 @@
<string name="settings_playback_ios_target_primaries_dialog">Στόχοι πρωτογενών χρωμάτων</string>
<string name="settings_playback_ios_target_transfer">Στόχος μεταφοράς</string>
<string name="settings_playback_ios_target_transfer_dialog">Στόχος μεταφοράς</string>
<string name="settings_playback_ios_video_output">iOS Βίντεο output</string>
<string name="settings_playback_ios_video_output">Έξοδος βίντεο iOS</string>
<string name="settings_playback_libmpv_hardware_decoding">Αποκωδικοποίηση υλικού libmpv</string>
<string name="settings_playback_libmpv_hardware_decoding_description">Χρήση αποκωδικοποίησης υλικού mpv όταν είναι διαθέσιμη.</string>
<string name="settings_playback_libmpv_video_output">Renderer libmpv</string>
@ -1876,8 +1918,8 @@
<string name="settings_stream_badge_urls_description">Εισαγωγή έως %1$d URL badge ροής τύπου Fusion σε μορφή JSON. Κάθε URL μπορεί να ενημερωθεί ή να διαγραφεί ξεχωριστά.</string>
<string name="settings_stream_badge_urls_search_description">Διαχείριση εισηγμένων URL badge ροής τύπου Fusion (JSON).</string>
<string name="settings_stream_badge_urls_title">URL badge Fusion</string>
<string name="settings_stream_badges_section">Στυλ Fusion</string>
<string name="settings_stream_display_section">Οθόνη</string>
<string name="settings_stream_badges_section">ΣΤΥΛ FUSION</string>
<string name="settings_stream_display_section">ΟΘΌΝΗ</string>
<string name="settings_stream_size_badges_description">Εμφάνιση badge μεγέθους αρχείου στα αποτελέσματα ροής και στα πλαίσια πηγών του player.</string>
<string name="settings_stream_size_badges_title">Badge μεγέθους</string>
<string name="streams_loading_skip_segments">Φόρτωση τμημάτων παράλειψης…</string>

View file

@ -426,12 +426,14 @@
<string name="compose_settings_page_licenses_attributions">Licencias y atribución</string>
<string name="compose_settings_page_mdblist_ratings">Calificaciones de MDBList</string>
<string name="compose_settings_page_meta_screen">Pantalla meta</string>
<string name="settings_appearance_section_detail_page">PANTALLA META</string>
<string name="compose_settings_page_notifications">Notificaciones</string>
<string name="compose_settings_page_playback">Reproducción</string>
<string name="compose_settings_page_plugins">Plugins</string>
<string name="compose_settings_page_poster_customization">Personalización de póster</string>
<string name="compose_settings_page_root">Configuración</string>
<string name="compose_settings_page_streams">Corrientes</string>
<string name="settings_appearance_section_streams">CORRIENTES</string>
<string name="compose_settings_page_supporters_contributors">Patrocinadores y colaboradores</string>
<string name="compose_settings_page_tmdb_enrichment">Enriquecimiento TMDB</string>
<string name="compose_settings_page_trakt">Trakt</string>
@ -651,7 +653,7 @@
<string name="settings_integrations_tmdb_description">Mejora las páginas de detalles con arte, créditos, metadatos de episodios y más desde TMDB.</string>
<string name="settings_integrations_mdblist_description">Añade calificaciones externas de IMDb, Rotten Tomatoes, Metacritic y otras a las páginas de detalles.</string>
<string name="settings_integrations_debrid_description">Conecte cuentas para enlaces y acceso a la biblioteca</string>
<string name="settings_debrid_section_title">Servicios conectados</string>
<string name="settings_debrid_section_title">SERVICIOS CONECTADOS</string>
<string name="settings_debrid_experimental_notice">Estas integraciones son experimentales y pueden conservarse, modificarse o eliminarse más adelante.</string>
<string name="settings_debrid_cloud_library">biblioteca en la nube</string>
<string name="settings_debrid_cloud_library_description">Explore y reproduzca archivos que ya estén en sus cuentas conectadas.</string>
@ -660,7 +662,7 @@
<string name="settings_debrid_resolve_with">Resolver con</string>
<string name="settings_debrid_resolve_with_description">Elija qué cuenta conectada maneja los enlaces reproducibles.</string>
<string name="settings_debrid_add_key_first">Primero conecte una cuenta.</string>
<string name="settings_debrid_section_providers">Cuentas</string>
<string name="settings_debrid_section_providers">CUENTAS</string>
<string name="settings_debrid_provider_description">Conecta tu cuenta de %1$s.</string>
<string name="settings_debrid_provider_device_description">Vincula tu cuenta de %1$s en el navegador.</string>
<string name="settings_debrid_dialog_title">%1$s clave API</string>
@ -680,26 +682,26 @@
<string name="settings_debrid_device_auth_failed">No se pudo iniciar el inicio de sesión.</string>
<string name="settings_debrid_device_auth_missing_configuration">Este método de inicio de sesión no está configurado en esta compilación.</string>
<string name="settings_debrid_device_auth_expired">Este código expiró. Intentar otra vez.</string>
<string name="settings_debrid_section_instant_playback">Preparación de enlaces</string>
<string name="settings_debrid_section_instant_playback">PREPARACIÓN DE ENLACES</string>
<string name="settings_debrid_prepare_instant_playback">Preparar enlaces</string>
<string name="settings_debrid_prepare_instant_playback_description">Resuelva los enlaces reproducibles antes de que comience la reproducción.</string>
<string name="settings_debrid_prepare_stream_count">Enlaces para preparar</string>
<string name="settings_debrid_prepare_stream_count_warning">Utilice un recuento más bajo cuando sea posible. Los servicios conectados pueden limitar la cantidad de enlaces que se pueden resolver en un período de tiempo. Abrir una película o un episodio puede contar para esos límites incluso si no presiona Ver, porque los enlaces están preparados con anticipación.</string>
<string name="settings_debrid_prepare_count_one">1 enlace</string>
<string name="settings_debrid_prepare_count_many">%1$d enlaces</string>
<string name="settings_debrid_section_formatting">Formato</string>
<string name="settings_debrid_section_formatting">FORMATO</string>
<string name="settings_debrid_name_template">Plantilla de nombre</string>
<string name="settings_debrid_name_template_description">Controla cómo aparecen los nombres de los resultados. Déjelo en blanco para utilizar el nombre del resultado original.</string>
<string name="settings_debrid_description_template">Plantilla de descripción</string>
<string name="settings_debrid_description_template_description">Controla los metadatos que se muestran debajo de cada resultado. Déjelo en blanco para utilizar los detalles del resultado original.</string>
<string name="settings_debrid_formatter_reset_title">Restablecer formato</string>
<string name="settings_debrid_formatter_reset_subtitle">Restaurar el formato de resultados predeterminado.</string>
<string name="settings_stream_badges_section">Estilo de fusión</string>
<string name="settings_stream_badges_section">ESTILO DE FUSIÓN</string>
<string name="settings_stream_size_badges_title">Insignias de tamaño</string>
<string name="settings_stream_size_badges_description">Muestre insignias de tamaño de archivo en los resultados de la transmisión y en los paneles de fuentes del reproductor.</string>
<string name="settings_stream_addon_logo_title">Logotipo del complemento</string>
<string name="settings_stream_addon_logo_description">Muestra el logotipo y el nombre del complemento junto a las fuentes de transmisión.</string>
<string name="settings_stream_display_section">Mostrar</string>
<string name="settings_stream_display_section">MOSTRAR</string>
<string name="settings_stream_badge_position_title">Posición de la insignia</string>
<string name="settings_stream_badge_position_description">Elija si las insignias de tamaño y Fusion aparecen encima o debajo de las tarjetas de transmisión.</string>
<string name="settings_stream_badge_position_dialog_title">Posición de la insignia</string>
@ -1633,7 +1635,7 @@
<string name="player_ios_preset_custom_desc">Utilice sus valores avanzados a continuación.</string>
<string name="player_ios_hardware_decoder_off">Apagado</string>
<!-- Playback settings iOS video output -->
<string name="settings_playback_ios_video_output">salida de vídeo iOS</string>
<string name="settings_playback_ios_video_output">SALIDA DE VÍDEO iOS</string>
<string name="settings_playback_ios_hardware_decoder">Decodificador de hardware</string>
<string name="settings_playback_ios_extended_dynamic_range">Rango dinámico extendido</string>
<string name="settings_playback_ios_extended_dynamic_range_desc">Modo de salida Metal predeterminado para nuevas sesiones de reproducción.</string>
@ -1642,13 +1644,13 @@
<string name="settings_playback_ios_target_primaries">Primarias objetivo</string>
<string name="settings_playback_ios_target_transfer">Transferencia de destino</string>
<!-- Playback settings iOS audio output -->
<string name="settings_playback_ios_audio_output_section">salida de audio de iOS</string>
<string name="settings_playback_ios_audio_output_section">SALIDA DE AUDIO DE iOS</string>
<string name="settings_playback_ios_audio_output">Salida de audio</string>
<string name="settings_playback_ios_audio_output_auto_desc">Utilice AudioUnit mientras la salida de AVFoundation esté temporalmente desactivada.</string>
<string name="settings_playback_ios_audio_output_avfoundation_desc">Soporte experimental para audio espacial y salida multicanal.</string>
<string name="settings_playback_ios_audio_output_audiounit_desc">Utilice la salida AudioUnit heredada.</string>
<!-- Debrid Result Management section -->
<string name="settings_debrid_section_result_management">Gestión de resultados</string>
<string name="settings_debrid_section_result_management">GESTIÓN DE RESULTADOS</string>
<string name="settings_debrid_max_results">Resultados máximos</string>
<string name="settings_debrid_max_results_desc">Limite la cantidad de resultados que aparecen.</string>
<string name="settings_debrid_sort_results">Ordenar resultados</string>
@ -1916,6 +1918,7 @@
<string name="p2p_consent_cancel">Cancelar</string>
<string name="p2p_error_unknown">Error de torrent desconocido</string>
<string name="settings_p2p_title">Transmisión P2P</string>
<string name="settings_playback_section_p2p">TRANSMISIÓN P2P</string>
<string name="settings_p2p_subtitle">Permitir transmisiones de igual a igual (torrent)</string>
<string name="settings_p2p_hide_stats_title">Ocultar estadísticas de torrents</string>
<string name="settings_p2p_hide_stats_subtitle">Ocultar buffer, semillas, pares y velocidad de descarga durante la carga y reproducción</string>

View file

@ -418,12 +418,14 @@
<string name="compose_settings_page_licenses_attributions">Licences et attributions</string>
<string name="compose_settings_page_mdblist_ratings">Notes MDBList</string>
<string name="compose_settings_page_meta_screen">Écran méta</string>
<string name="settings_appearance_section_detail_page">ÉCRAN MÉTA</string>
<string name="compose_settings_page_notifications">Notifications</string>
<string name="compose_settings_page_playback">Lecture</string>
<string name="compose_settings_page_plugins">Plugins</string>
<string name="compose_settings_page_poster_customization">Personnalisation des affiches</string>
<string name="compose_settings_page_root">Paramètres</string>
<string name="compose_settings_page_streams">Streams</string>
<string name="settings_appearance_section_streams">STREAMS</string>
<string name="compose_settings_page_supporters_contributors">Supporters et contributeurs</string>
<string name="compose_settings_page_tmdb_enrichment">Enrichissement TMDB</string>
<string name="compose_settings_page_trakt">Trakt</string>
@ -661,7 +663,7 @@
<string name="settings_integrations_tmdb_description">Enrichissez les pages de détails avec de lart, des crédits, des métadonnées dépisodes et plus depuis TMDB.</string>
<string name="settings_integrations_mdblist_description">Ajoutez des notes externes dIMDb, Rotten Tomatoes, Metacritic et dautres aux pages de détails.</string>
<string name="settings_integrations_debrid_description">Connectez des comptes pour les liens et laccès à la bibliothèque</string>
<string name="settings_debrid_section_title">Services connectés</string>
<string name="settings_debrid_section_title">SERVICES CONNECTÉS</string>
<string name="settings_debrid_experimental_notice">Ces intégrations sont expérimentales et peuvent être conservées, modifiées ou supprimées plus tard.</string>
<string name="settings_debrid_cloud_library">Cloud library</string>
<string name="settings_debrid_cloud_library_description">Parcourez et lisez les fichiers déjà présents dans vos comptes connectés.</string>
@ -670,7 +672,7 @@
<string name="settings_debrid_resolve_with">Résoudre avec</string>
<string name="settings_debrid_resolve_with_description">Choisissez quel compte connecté gère les liens lisibles.</string>
<string name="settings_debrid_add_key_first">Connectez dabord un compte.</string>
<string name="settings_debrid_section_providers">Comptes</string>
<string name="settings_debrid_section_providers">COMPTES</string>
<string name="settings_debrid_provider_description">Connectez votre compte %1$s.</string>
<string name="settings_debrid_provider_device_description">Liez votre compte %1$s dans le navigateur.</string>
<string name="settings_debrid_dialog_title">Clé API %1$s</string>
@ -690,26 +692,26 @@
<string name="settings_debrid_device_auth_failed">Impossible de démarrer la connexion.</string>
<string name="settings_debrid_device_auth_missing_configuration">Cette méthode de connexion nest pas configurée dans ce build.</string>
<string name="settings_debrid_device_auth_expired">Ce code a expiré. Réessayez.</string>
<string name="settings_debrid_section_instant_playback">Préparation des liens</string>
<string name="settings_debrid_section_instant_playback">PRÉPARATION DES LIENS</string>
<string name="settings_debrid_prepare_instant_playback">Préparer les liens</string>
<string name="settings_debrid_prepare_instant_playback_description">Résoudre les liens lisibles avant le démarrage de la lecture.</string>
<string name="settings_debrid_prepare_stream_count">Liens à préparer</string>
<string name="settings_debrid_prepare_stream_count_warning">Utilisez un nombre plus faible si possible. Les services connectés peuvent limiter le nombre de liens résolus sur une période donnée. Ouvrir un film ou un épisode peut compter dans ces limites même si vous nappuyez pas sur Regarder, car les liens sont préparés à lavance.</string>
<string name="settings_debrid_prepare_count_one">1 lien</string>
<string name="settings_debrid_prepare_count_many">%1$d liens</string>
<string name="settings_debrid_section_formatting">Mise en forme</string>
<string name="settings_debrid_section_formatting">MISE EN FORME</string>
<string name="settings_debrid_name_template">Modèle de nom</string>
<string name="settings_debrid_name_template_description">Contrôle la manière dont les noms de résultats apparaissent.</string>
<string name="settings_debrid_description_template">Modèle de description</string>
<string name="settings_debrid_description_template_description">Contrôle les métadonnées affichées sous chaque résultat.</string>
<string name="settings_debrid_formatter_reset_title">Réinitialiser la mise en forme</string>
<string name="settings_debrid_formatter_reset_subtitle">Restaurer la mise en forme par défaut des résultats.</string>
<string name="settings_stream_badges_section">Style Fusion</string>
<string name="settings_stream_badges_section">STYLE FUSION</string>
<string name="settings_stream_size_badges_title">Badges de taille</string>
<string name="settings_stream_size_badges_description">Affiche les badges de taille de fichier dans les résultats de flux et les panneaux de source du lecteur.</string>
<string name="settings_stream_addon_logo_title">Logo de laddon</string>
<string name="settings_stream_addon_logo_description">Affiche le logo et le nom de laddon à côté des sources de streams.</string>
<string name="settings_stream_display_section">Affichage</string>
<string name="settings_stream_display_section">AFFICHAGE</string>
<string name="settings_stream_badge_position_title">Position des badges</string>
<string name="settings_stream_badge_position_description">Choisissez si les badges Fusion et de taille apparaissent au-dessus ou en dessous des cartes de flux.</string>
<string name="settings_stream_badge_position_dialog_title">Position des badges</string>
@ -1647,7 +1649,7 @@
<string name="player_ios_preset_custom_desc">Utilisez vos valeurs avancées ci-dessous.</string>
<string name="player_ios_hardware_decoder_off">Désactivé</string>
<!-- Playback settings iOS video output -->
<string name="settings_playback_ios_video_output">Sortie vidéo iOS</string>
<string name="settings_playback_ios_video_output">SORTIE VIDÉO iOS</string>
<string name="settings_playback_ios_hardware_decoder">Décodeur matériel</string>
<string name="settings_playback_ios_extended_dynamic_range">Plage dynamique étendue</string>
<string name="settings_playback_ios_extended_dynamic_range_desc">Mode de sortie Metal par défaut pour les nouvelles sessions de lecture.</string>
@ -1655,13 +1657,13 @@
<string name="settings_playback_ios_display_color_hint_desc">Permettre à mpv de cibler par défaut lespace colorimétrique de lécran actif.</string>
<string name="settings_playback_ios_target_primaries">Primaires cibles</string>
<string name="settings_playback_ios_target_transfer">Transfert cible</string>
<string name="settings_playback_ios_audio_output_section">Sortie audio iOS</string>
<string name="settings_playback_ios_audio_output_section">SORTIE AUDIO iOS</string>
<string name="settings_playback_ios_audio_output">Sortie audio</string>
<string name="settings_playback_ios_audio_output_auto_desc">Utilise AudioUnit tant que la sortie AVFoundation est temporairement désactivée.</string>
<string name="settings_playback_ios_audio_output_avfoundation_desc">Prise en charge expérimentale de laudio spatial et de la sortie multicanal.</string>
<string name="settings_playback_ios_audio_output_audiounit_desc">Utilise lancienne sortie AudioUnit.</string>
<!-- Debrid Result Management section -->
<string name="settings_debrid_section_result_management">Gestion des résultats</string>
<string name="settings_debrid_section_result_management">GESTION DES RÉSULTATS</string>
<string name="settings_debrid_max_results">Nombre max de résultats</string>
<string name="settings_debrid_max_results_desc">Limite le nombre de résultats affichés.</string>
<string name="settings_debrid_sort_results">Tri des résultats</string>
@ -1945,6 +1947,7 @@
<string name="p2p_consent_cancel">Annuler</string>
<string name="p2p_error_unknown">Erreur torrent inconnue</string>
<string name="settings_p2p_title">Streaming P2P</string>
<string name="settings_playback_section_p2p">STREAMING P2P</string>
<string name="settings_p2p_subtitle">Autoriser les flux pair-à-pair (torrent)</string>
<string name="settings_p2p_hide_stats_title">Masquer les statistiques torrent</string>
<string name="settings_p2p_hide_stats_subtitle">Masquer la mémoire tampon, les seeders, les pairs et la vitesse de téléchargement pendant le chargement et la lecture</string>

View file

@ -251,6 +251,7 @@
<string name="compose_settings_page_integrations">אינטגרציות</string>
<string name="compose_settings_page_licenses_attributions">רישיונות וקרדיטים</string>
<string name="compose_settings_page_meta_screen">עמוד פרטים</string>
<string name="settings_appearance_section_detail_page">עמוד פרטים</string>
<string name="compose_settings_page_notifications">התראות</string>
<string name="compose_settings_page_playback">הפעלה</string>
<string name="compose_settings_page_plugins">פלאגינים</string>
@ -350,7 +351,7 @@
<string name="settings_poster_width_large">גדול</string>
<string name="settings_poster_width_standard">רגיל</string>
<string name="settings_show_secret">הצג ערך</string>
<string name="settings_stream_badges_section">סגנון Fusion</string>
<string name="settings_stream_badges_section">סגנון FUSION</string>
<string name="settings_stream_size_badges_title">תגי גודל</string>
<string name="settings_stream_size_badges_description">הצג תגי גודל קובץ בתוצאות Stream ובפאנלי מקור בנגן.</string>
<string name="settings_stream_addon_logo_title">הצגת לוגו התוסף</string>
@ -373,8 +374,8 @@
<string name="settings_playback_section_next_episode">הפרק הבא</string>
<string name="settings_playback_section_player">נגן</string>
<string name="settings_playback_section_skip_segments">דילוג על קטעים</string>
<string name="settings_playback_section_stream_auto_play">הפעלה אוטומטית של Stream</string>
<string name="settings_playback_section_stream_selection">בחירת Stream</string>
<string name="settings_playback_section_stream_auto_play">הפעלה אוטומטית של STREAM</string>
<string name="settings_playback_section_stream_selection">בחירת STREAM</string>
<string name="settings_playback_section_subtitle_audio">כתוביות ואודיו</string>
<string name="settings_playback_section_subtitle_rendering">עיבוד כתוביות</string>
<string name="settings_playback_show_loading_overlay">שכבת טעינה</string>
@ -708,6 +709,7 @@
<string name="p2p_consent_enable">P2P הפעל</string>
<string name="p2p_consent_cancel">ביטול</string>
<string name="settings_p2p_title">סטרימינג P2P</string>
<string name="settings_playback_section_p2p">סטרימינג P2P</string>
<string name="settings_p2p_subtitle">אפשר מקורות Stream עמית-לעמית (חינמי אך טעינה איטית).</string>
<string name="player_torrent_peer_info">%1$d מקורות · %2$d עמיתים</string>
<string name="player_torrent_stats">%1$d עמיתים · %2$d מקורות · %3$d%%</string>

View file

@ -386,6 +386,7 @@
<string name="compose_settings_page_licenses_attributions">Licencek és köszönetnyilvánítások</string>
<string name="compose_settings_page_mdblist_ratings">MDBList értékelések</string>
<string name="compose_settings_page_meta_screen">Részletek oldal</string>
<string name="settings_appearance_section_detail_page">RÉSZLETEK OLDAL</string>
<string name="compose_settings_page_notifications">Értesítések</string>
<string name="compose_settings_page_playback">Lejátszás</string>
<string name="compose_settings_page_plugins">Beépülő modulok</string>
@ -517,8 +518,8 @@
<string name="settings_homescreen_section_catalogs">KATALÓGUSOK</string>
<string name="settings_homescreen_section_catalogs_collections">KATALÓGUSOK &amp; GYŰJTEMÉNYEK</string>
<string name="settings_homescreen_section_collections">GYŰJTEMÉNYEK</string>
<string name="settings_homescreen_section_hero">Kezdőlap elrendezése</string>
<string name="settings_homescreen_section_hero_sources">Kiemelt katalógusok</string>
<string name="settings_homescreen_section_hero">KEZDŐLAP ELRENDEZÉSE</string>
<string name="settings_homescreen_section_hero_sources">KIEMELT KATALÓGUSOK</string>
<string name="settings_homescreen_selected_count">%1$d / %2$d kiválasztva</string>
<string name="settings_homescreen_show_hero">Kiemelt rész megjelenítése</string>
<string name="settings_homescreen_show_hero_description">Kiemelt galéria (carousel) megjelenítése a kezdőlap tetején.</string>
@ -532,7 +533,7 @@
<string name="settings_hide_secret">Érték elrejtése</string>
<string name="settings_playback_subtitle">Lejátszó, feliratok és automatikus lejátszás</string>
<string name="settings_poster_card_radius">Sarokkerekítés</string>
<string name="settings_poster_card_style">Poszterkártya stílusa</string>
<string name="settings_poster_card_style">POSZTERKÁRTYA STÍLUSA</string>
<string name="settings_poster_card_width">Szélesség</string>
<string name="settings_poster_custom">Egyéni</string>
<string name="settings_poster_description">Kártyaszélesség és sarokkerekítés finomhangolása.</string>
@ -566,7 +567,7 @@
<string name="settings_continue_watching_sort_mode_default_desc">Minden elem rendezése a legutóbbi megtekintés alapján</string>
<string name="settings_continue_watching_sort_mode_streaming">Streaming stílus</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">A már megjelent elemek előre, a közelgők a sor végére kerülnek</string>
<string name="settings_continue_watching_section_card_style">Poszterkártya stílusa</string>
<string name="settings_continue_watching_section_card_style">POSZTERKÁRTYA STÍLUSA</string>
<string name="settings_continue_watching_section_on_launch">INDÍTÁSKOR</string>
<string name="settings_continue_watching_section_up_next_behavior">KÖVETKEZŐ ELEM VISELKEDÉSE</string>
<string name="settings_continue_watching_section_visibility">LÁTHATÓSÁG</string>
@ -587,28 +588,28 @@
<string name="settings_content_discovery_homescreen_description">Kezdőlap elrendezésének, a tartalom láthatóságának és a poszterek viselkedésének beállítása</string>
<string name="settings_content_discovery_meta_screen_description">A részletek és az epizódok képernyőinek beállításai.</string>
<string name="settings_content_discovery_collections_description">Egyéni katalóguscsoportok létrehozása a Kezdőlapon megjelenő mappákkal.</string>
<string name="settings_integrations_section_title">Integrációk</string>
<string name="settings_integrations_section_title">INTEGRÁCIÓK</string>
<string name="settings_integrations_tmdb_description">Metaadat-bővítés vezérlői</string>
<string name="settings_integrations_mdblist_description">Külső értékelési szolgáltatók</string>
<string name="settings_integrations_debrid_description">Kísérleti felhőalapú fiókforrások</string>
<string name="settings_debrid_section_title">Debrid</string>
<string name="settings_debrid_section_title">DEBRID</string>
<string name="settings_debrid_experimental_notice">A Debrid támogatás kísérleti jellegű; a későbbiekben megtartható, módosítható vagy eltávolítható.</string>
<string name="settings_debrid_enable">Források engedélyezése</string>
<string name="settings_debrid_enable_description">Lejátszható találatok megjelenítése a csatlakoztatott fiókokból.</string>
<string name="settings_debrid_add_key_first">Először adj meg egy API-kulcsot.</string>
<string name="settings_debrid_section_providers">Fiók</string>
<string name="settings_debrid_section_providers">FIÓK</string>
<string name="settings_debrid_dialog_title">Torbox API-kulcs</string>
<string name="settings_debrid_dialog_subtitle">Add meg a Torbox API-kulcsodat.</string>
<string name="settings_debrid_dialog_placeholder">Írd be a Torbox API-kulcsot</string>
<string name="settings_debrid_not_set">Nincs beállítva</string>
<string name="settings_debrid_section_instant_playback">Azonnali lejátszás</string>
<string name="settings_debrid_section_instant_playback">AZONNALI LEJÁTSZÁS</string>
<string name="settings_debrid_prepare_instant_playback">Linkek előkészítése</string>
<string name="settings_debrid_prepare_instant_playback_description">Az elsődleges források feloldása a lejátszás megkezdése előtt.</string>
<string name="settings_debrid_prepare_stream_count">Előkészítendő források száma</string>
<string name="settings_debrid_prepare_stream_count_warning">Lehetőleg alacsony értéket használj. A Debrid szolgáltatások korlátozhatják (rate-limit), hogy egy adott időszakon belül hány link oldható fel. Egy film vagy epizód adatlapjának megnyitása is beleszámíthat ezekbe a korlátokba még akkor is, ha nem nyomsz a Lejátszás gombra , mivel a linkek előre előkészítésre kerülnek.</string>
<string name="settings_debrid_prepare_count_one">1 forrás</string>
<string name="settings_debrid_prepare_count_many">%1$d forrás</string>
<string name="settings_debrid_section_formatting">Formázás</string>
<string name="settings_debrid_section_formatting">FORMÁZÁS</string>
<string name="settings_debrid_name_template">Név sablon</string>
<string name="settings_debrid_name_template_description">A források nevének megjelenítését szabályozza.</string>
<string name="settings_debrid_description_template">Leírás sablon</string>
@ -623,9 +624,9 @@
<string name="settings_mdb_api_key_title">API-kulcs</string>
<string name="settings_mdb_enable_ratings">MDBList értékelések engedélyezése</string>
<string name="settings_mdb_enable_ratings_description">Külső szolgáltatóktól származó értékelések megjelenítése a részletek képernyőn</string>
<string name="settings_mdb_section_api_key">API-kulcs</string>
<string name="settings_mdb_section_rating_providers">Külső értékelési szolgáltatók</string>
<string name="settings_mdb_section_title">MDBList értékelések</string>
<string name="settings_mdb_section_api_key">API-KULCS</string>
<string name="settings_mdb_section_rating_providers">KÜLSŐ ÉRTÉKELÉSI SZOLGÁLTATÓK</string>
<string name="settings_mdb_section_title">MDBList ÉRTÉKELÉSEK</string>
<string name="settings_meta_actions">Műveletek</string>
<string name="settings_meta_actions_description">Lejátszás és mentés vezérlők.</string>
<string name="settings_meta_cast">Stáb</string>
@ -862,7 +863,7 @@
<string name="settings_tmdb_section_credentials">HITELOSÍTÓ ADATOK</string>
<string name="settings_tmdb_section_localization">LOKALIZÁCIÓ</string>
<string name="settings_tmdb_section_modules">MODULOK</string>
<string name="settings_tmdb_section_title">TMDB adatbővítés</string>
<string name="settings_tmdb_section_title">TMDB ADATBŐVÍTÉS</string>
<string name="settings_trakt_approval_redirect">A jóváhagyás után automatikusan visszairányítunk az alkalmazásba.</string>
<string name="settings_trakt_authentication">HITELOSÍTÁS</string>
<string name="settings_trakt_comments">Hozzászólások</string>
@ -1380,6 +1381,7 @@
<string name="compose_player_text_opacity">Szöveg átlátszósága</string>
<string name="compose_settings_page_advanced">Haladó</string>
<string name="compose_settings_page_streams">Streamek</string>
<string name="settings_appearance_section_streams">STREAMEK</string>
<string name="compose_settings_root_advanced_description">Indítási és profil működés</string>
<string name="compose_settings_root_advanced_section">HALADÓ</string>
<string name="compose_settings_root_streams_description">Stream találatok megjelenítése és jelvény URL szabályok.</string>
@ -1421,12 +1423,12 @@
<string name="settings_debrid_device_auth_failed">Nem sikerült elindítani a bejelentkezést.</string>
<string name="settings_debrid_device_auth_missing_configuration">Ez a bejelentkezési mód nincs konfigurálva ebben a verzióban.</string>
<string name="settings_debrid_device_auth_expired">Ez a kód lejárt. Próbáld újra.</string>
<string name="settings_stream_badges_section">Fusion stílus</string>
<string name="settings_stream_badges_section">FUSION STÍLUS</string>
<string name="settings_stream_size_badges_title">Méret jelvények</string>
<string name="settings_stream_size_badges_description">Fájlméret-jelvények megjelenítése a stream találatoknál és a lejátszó forrás-panelein.</string>
<string name="settings_stream_addon_logo_title">Kiegészítő logója</string>
<string name="settings_stream_addon_logo_description">A kiegészítő logójának és nevének megjelenítése a stream források mellett.</string>
<string name="settings_stream_display_section">Megjelenítés</string>
<string name="settings_stream_display_section">MEGJELENÍTÉS</string>
<string name="settings_stream_badge_position_title">Jelvény pozíciója</string>
<string name="settings_stream_badge_position_description">Válaszd ki, hogy a Fusion és a méret jelvények a stream kártyák felett vagy alatt jelenjenek meg.</string>
<string name="settings_stream_badge_position_dialog_title">Jelvény pozíciója</string>
@ -1638,7 +1640,7 @@
<string name="player_ios_preset_custom_label">Egyéni</string>
<string name="player_ios_preset_custom_desc">A lentebb megadott haladó értékeid használata.</string>
<string name="player_ios_hardware_decoder_off">Ki</string>
<string name="settings_playback_ios_video_output">iOS videó kimenet</string>
<string name="settings_playback_ios_video_output">iOS VIDEÓ KIMENET</string>
<string name="settings_playback_ios_hardware_decoder">Hardveres dekóder</string>
<string name="settings_playback_ios_extended_dynamic_range">Kiterjesztett dinamikatartomány (EDR)</string>
<string name="settings_playback_ios_extended_dynamic_range_desc">Alapértelmezett Metal kimeneti mód az új lejátszásokhoz.</string>
@ -1646,12 +1648,12 @@
<string name="settings_playback_ios_display_color_hint_desc">Hagyja, hogy az mpv alapértelmezés szerint az aktív kijelző színtartományát célozza meg.</string>
<string name="settings_playback_ios_target_primaries">Cél alapszínek (Primaries)</string>
<string name="settings_playback_ios_target_transfer">Cél átviteli karakterisztika</string>
<string name="settings_playback_ios_audio_output_section">iOS hangkimenet</string>
<string name="settings_playback_ios_audio_output_section">iOS HANGKIMENET</string>
<string name="settings_playback_ios_audio_output">Hangkimenet</string>
<string name="settings_playback_ios_audio_output_auto_desc">Az AudioUnit használata, amíg az AVFoundation kimenet átmenetileg le van tiltva.</string>
<string name="settings_playback_ios_audio_output_avfoundation_desc">Kísérleti támogatás a térbeli hangzáshoz (Spatial Audio) és a többcsatornás kimenethez.</string>
<string name="settings_playback_ios_audio_output_audiounit_desc">A korábbi, örökölt AudioUnit kimenet használata.</string>
<string name="settings_debrid_section_result_management">Találatok kezelése</string>
<string name="settings_debrid_section_result_management">TALÁLATOK KEZELÉSE</string>
<string name="settings_debrid_max_results">Maximális találatszám</string>
<string name="settings_debrid_max_results_desc">Korlátozza a megjelenő találatok számát.</string>
<string name="settings_debrid_sort_results">Találatok rendezése</string>
@ -1896,6 +1898,7 @@
<string name="p2p_consent_cancel">Mégse</string>
<string name="p2p_error_unknown">Ismeretlen torrent hiba</string>
<string name="settings_p2p_title">P2P Streaming</string>
<string name="settings_playback_section_p2p">P2P STREAMING</string>
<string name="settings_p2p_subtitle">Peer-to-peer (torrent) streamek engedélyezése</string>
<string name="settings_p2p_hide_stats_title">Torrent statisztikák elrejtése</string>
<string name="settings_p2p_hide_stats_subtitle">Pufferelés, seederek, peerek és letöltési sebesség elrejtése a betöltés és lejátszás alatt</string>
@ -1939,7 +1942,7 @@
<string name="sentry_turn_on">Bekapcsolás</string>
<string name="downloads_open_directory">Letöltési mappa megnyitása</string>
<string name="downloads_open_directory_failed">Nem sikerült megnyitni a letöltési mappát</string>
<string name="settings_card_depth_title">Kártyák mélység-effektusa</string>
<string name="settings_card_depth_title">KÁRTYÁK MÉLYSÉG-EFFEKTUSA</string>
<string name="settings_card_depth_description">Megvilágított felső élet és lágy fényt ad a képkártyáknak a finom mélységérzet érdekében.</string>
<string name="settings_card_depth_enabled">Mélység-effektus engedélyezése</string>
<string name="settings_card_depth_edge">Élvilágítás</string>

View file

@ -404,6 +404,7 @@
<string name="compose_settings_page_licenses_attributions">Lisensi &amp; Atribusi</string>
<string name="compose_settings_page_mdblist_ratings">Rating MDBList</string>
<string name="compose_settings_page_meta_screen">Halaman Detail</string>
<string name="settings_appearance_section_detail_page">HALAMAN DETAIL</string>
<string name="compose_settings_page_notifications">Notifikasi</string>
<string name="compose_settings_page_playback">Pemutaran</string>
<string name="compose_settings_page_plugins">Plugin</string>
@ -539,8 +540,8 @@
<string name="settings_homescreen_section_catalogs">KATALOG</string>
<string name="settings_homescreen_section_catalogs_collections">KATALOG &amp; KOLEKSI</string>
<string name="settings_homescreen_section_collections">KOLEKSI</string>
<string name="settings_homescreen_section_hero">Tata Letak Beranda</string>
<string name="settings_homescreen_section_hero_sources">Katalog Hero</string>
<string name="settings_homescreen_section_hero">TATA LETAK BERANDA</string>
<string name="settings_homescreen_section_hero_sources">KATALOG HERO</string>
<string name="settings_homescreen_selected_count">%1$d dari %2$d dipilih</string>
<string name="settings_homescreen_show_hero">Tampilkan Bagian Hero</string>
<string name="settings_homescreen_show_hero_description">Tampilkan karousel hero di bagian atas beranda.</string>
@ -554,7 +555,7 @@
<string name="settings_hide_secret">Sembunyikan nilai</string>
<string name="settings_playback_subtitle">Pemutar, subtitle, dan putar otomatis</string>
<string name="settings_poster_card_radius">Radius Sudut</string>
<string name="settings_poster_card_style">Gaya Kartu Poster</string>
<string name="settings_poster_card_style">GAYA KARTU POSTER</string>
<string name="settings_poster_card_width">Lebar</string>
<string name="settings_poster_custom">Kustom</string>
<string name="settings_poster_description">Sesuaikan lebar kartu dan radius sudut.</string>
@ -589,7 +590,7 @@
<string name="settings_continue_watching_sort_mode_default_desc">Urutkan semua item berdasarkan yang terbaru</string>
<string name="settings_continue_watching_sort_mode_streaming">Gaya Streaming</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">Item yang sudah rilis lebih dulu, yang mendatang di akhir</string>
<string name="settings_continue_watching_section_card_style">Gaya Kartu Poster</string>
<string name="settings_continue_watching_section_card_style">GAYA KARTU POSTER</string>
<string name="settings_continue_watching_section_on_launch">SAAT DIBUKA</string>
<string name="settings_continue_watching_section_up_next_behavior">PERILAKU BERIKUTNYA</string>
<string name="settings_continue_watching_section_visibility">VISIBILITAS</string>
@ -611,11 +612,11 @@
<string name="settings_content_discovery_homescreen_description">Sesuaikan tata letak beranda, visibilitas konten, dan poster</string>
<string name="settings_content_discovery_meta_screen_description">Pengaturan untuk layar detail dan episode.</string>
<string name="settings_content_discovery_collections_description">Buat pengelompokan katalog kustom dengan folder yang ditampilkan di Beranda.</string>
<string name="settings_integrations_section_title">Integrasi</string>
<string name="settings_integrations_section_title">INTEGRASI</string>
<string name="settings_integrations_tmdb_description">Kontrol pengayaan metadata</string>
<string name="settings_integrations_mdblist_description">Penyedia rating eksternal</string>
<string name="settings_integrations_debrid_description">Hubungkan akun untuk akses link dan pustaka cloud</string>
<string name="settings_debrid_section_title">Layanan Debrid</string>
<string name="settings_debrid_section_title">LAYANAN DEBRID</string>
<string name="settings_debrid_experimental_notice">Integrasi ini masih bersifat eksperimental dan dapat dipertahankan, diubah, atau dihapus sewaktu-waktu.</string>
<string name="settings_debrid_cloud_library">Pustaka Cloud</string>
<string name="settings_debrid_cloud_library_description">Telusuri dan mainkan file yang tersedia di akun Anda.</string>
@ -624,7 +625,7 @@
<string name="settings_debrid_resolve_with">Uraikan dengan</string>
<string name="settings_debrid_resolve_with_description">Pilih akun layanan debrid mana untuk menangani link yang dapat dimainkan.</string>
<string name="settings_debrid_add_key_first">Hubungkan akun terlebih dahulu.</string>
<string name="settings_debrid_section_providers">Akun</string>
<string name="settings_debrid_section_providers">AKUN</string>
<string name="settings_debrid_provider_description">Hubungkan akun %1$s Anda.</string>
<string name="settings_debrid_provider_device_description">Hubungkan akun %1$s Anda melalui browser.</string>
<string name="settings_debrid_dialog_title">%1$s API Key</string>
@ -644,14 +645,14 @@
<string name="settings_debrid_device_auth_failed">Tidak dapat memulai sign-in.</string>
<string name="settings_debrid_device_auth_missing_configuration">Metode sign-in ini tidak dikonfigurasi dalam rilis ini.</string>
<string name="settings_debrid_device_auth_expired">Kode ini telah kedaluwarsa. Coba lagi.</string>
<string name="settings_debrid_section_instant_playback">Persiapan Tautan</string>
<string name="settings_debrid_section_instant_playback">PERSIAPAN TAUTAN</string>
<string name="settings_debrid_prepare_instant_playback">Menyiapkan tautan</string>
<string name="settings_debrid_prepare_instant_playback_description">Siapkan sumber video sebelum mulai pemutaran dimulai.</string>
<string name="settings_debrid_prepare_stream_count">Jumlah sumber yang disiapkan</string>
<string name="settings_debrid_prepare_stream_count_warning">Gunakan jumlah sekecil mungkin. Penyedia layanan Debrid bisa saja membatasi seberapa banyak tautan yang dapat diurai dalam jangka waktu tertentu. Membuka film atau episode serial dihitung dalam batas tersebut meskipun Anda tidak menonton, karena tautan tetap disiapkan terlebih dahulu.</string>
<string name="settings_debrid_prepare_count_one">1 sumber</string>
<string name="settings_debrid_prepare_count_many">%1$d sumber</string>
<string name="settings_debrid_section_formatting">Format</string>
<string name="settings_debrid_section_formatting">FORMAT</string>
<string name="settings_debrid_name_template">Pola Nama</string>
<string name="settings_debrid_name_template_description">Mengatur bagaimana hasil sumber ditampilkan.</string>
<string name="settings_debrid_description_template">Pola Deskripsi</string>
@ -666,9 +667,9 @@
<string name="settings_mdb_api_key_title">Kunci API</string>
<string name="settings_mdb_enable_ratings">Aktifkan Rating MDBList</string>
<string name="settings_mdb_enable_ratings_description">Ambil rating dari penyedia eksternal di layar detail metadata</string>
<string name="settings_mdb_section_api_key">Kunci API</string>
<string name="settings_mdb_section_rating_providers">Penyedia rating eksternal</string>
<string name="settings_mdb_section_title">Rating MDBList</string>
<string name="settings_mdb_section_api_key">KUNCI API</string>
<string name="settings_mdb_section_rating_providers">PENYEDIA RATING EKSTERNAL</string>
<string name="settings_mdb_section_title">RATING MDBList</string>
<string name="settings_meta_actions">Tindakan</string>
<string name="settings_meta_actions_description">Kontrol putar dan simpan.</string>
<string name="settings_meta_cast">Pemeran</string>
@ -928,7 +929,7 @@
<string name="settings_tmdb_section_credentials">KREDENSIAL</string>
<string name="settings_tmdb_section_localization">LOKALISASI</string>
<string name="settings_tmdb_section_modules">MODUL</string>
<string name="settings_tmdb_section_title">Pengayaan TMDB</string>
<string name="settings_tmdb_section_title">PENGAYAAN TMDB</string>
<string name="settings_trakt_approval_redirect">Setelah disetujui, Anda akan diarahkan kembali secara otomatis.</string>
<string name="settings_trakt_authentication">AUTENTIKASI</string>
<string name="settings_trakt_comments">Komentar</string>

View file

@ -404,6 +404,7 @@
<string name="compose_settings_page_licenses_attributions">Lisensi &amp; Atribusi</string>
<string name="compose_settings_page_mdblist_ratings">Rating MDBList</string>
<string name="compose_settings_page_meta_screen">Halaman Detail</string>
<string name="settings_appearance_section_detail_page">HALAMAN DETAIL</string>
<string name="compose_settings_page_notifications">Notifikasi</string>
<string name="compose_settings_page_playback">Pemutaran</string>
<string name="compose_settings_page_plugins">Plugin</string>
@ -539,8 +540,8 @@
<string name="settings_homescreen_section_catalogs">KATALOG</string>
<string name="settings_homescreen_section_catalogs_collections">KATALOG &amp; KOLEKSI</string>
<string name="settings_homescreen_section_collections">KOLEKSI</string>
<string name="settings_homescreen_section_hero">Tata Letak Beranda</string>
<string name="settings_homescreen_section_hero_sources">Katalog Hero</string>
<string name="settings_homescreen_section_hero">TATA LETAK BERANDA</string>
<string name="settings_homescreen_section_hero_sources">KATALOG HERO</string>
<string name="settings_homescreen_selected_count">%1$d dari %2$d dipilih</string>
<string name="settings_homescreen_show_hero">Tampilkan Bagian Hero</string>
<string name="settings_homescreen_show_hero_description">Tampilkan karousel hero di bagian atas beranda.</string>
@ -554,7 +555,7 @@
<string name="settings_hide_secret">Sembunyikan nilai</string>
<string name="settings_playback_subtitle">Pemutar, subtitle, dan putar otomatis</string>
<string name="settings_poster_card_radius">Radius Sudut</string>
<string name="settings_poster_card_style">Gaya Kartu Poster</string>
<string name="settings_poster_card_style">GAYA KARTU POSTER</string>
<string name="settings_poster_card_width">Lebar</string>
<string name="settings_poster_custom">Kustom</string>
<string name="settings_poster_description">Sesuaikan lebar kartu dan radius sudut.</string>
@ -589,7 +590,7 @@
<string name="settings_continue_watching_sort_mode_default_desc">Urutkan semua item berdasarkan yang terbaru</string>
<string name="settings_continue_watching_sort_mode_streaming">Gaya Streaming</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">Item yang sudah rilis lebih dulu, yang mendatang di akhir</string>
<string name="settings_continue_watching_section_card_style">Gaya Kartu Poster</string>
<string name="settings_continue_watching_section_card_style">GAYA KARTU POSTER</string>
<string name="settings_continue_watching_section_on_launch">SAAT DIBUKA</string>
<string name="settings_continue_watching_section_up_next_behavior">PERILAKU BERIKUTNYA</string>
<string name="settings_continue_watching_section_visibility">VISIBILITAS</string>
@ -611,11 +612,11 @@
<string name="settings_content_discovery_homescreen_description">Sesuaikan tata letak beranda, visibilitas konten, dan poster</string>
<string name="settings_content_discovery_meta_screen_description">Pengaturan untuk layar detail dan episode.</string>
<string name="settings_content_discovery_collections_description">Buat pengelompokan katalog kustom dengan folder yang ditampilkan di Beranda.</string>
<string name="settings_integrations_section_title">Integrasi</string>
<string name="settings_integrations_section_title">INTEGRASI</string>
<string name="settings_integrations_tmdb_description">Kontrol pengayaan metadata</string>
<string name="settings_integrations_mdblist_description">Penyedia rating eksternal</string>
<string name="settings_integrations_debrid_description">Hubungkan akun untuk akses link dan pustaka cloud</string>
<string name="settings_debrid_section_title">Layanan Debrid</string>
<string name="settings_debrid_section_title">LAYANAN DEBRID</string>
<string name="settings_debrid_experimental_notice">Integrasi ini masih bersifat eksperimental dan dapat dipertahankan, diubah, atau dihapus sewaktu-waktu.</string>
<string name="settings_debrid_cloud_library">Pustaka Cloud</string>
<string name="settings_debrid_cloud_library_description">Telusuri dan mainkan file yang tersedia di akun Anda.</string>
@ -624,7 +625,7 @@
<string name="settings_debrid_resolve_with">Uraikan dengan</string>
<string name="settings_debrid_resolve_with_description">Pilih akun layanan debrid mana untuk menangani link yang dapat dimainkan.</string>
<string name="settings_debrid_add_key_first">Hubungkan akun terlebih dahulu.</string>
<string name="settings_debrid_section_providers">Akun</string>
<string name="settings_debrid_section_providers">AKUN</string>
<string name="settings_debrid_provider_description">Hubungkan akun %1$s Anda.</string>
<string name="settings_debrid_provider_device_description">Hubungkan akun %1$s Anda melalui browser.</string>
<string name="settings_debrid_dialog_title">%1$s API Key</string>
@ -644,14 +645,14 @@
<string name="settings_debrid_device_auth_failed">Tidak dapat memulai sign-in.</string>
<string name="settings_debrid_device_auth_missing_configuration">Metode sign-in ini tidak dikonfigurasi dalam rilis ini.</string>
<string name="settings_debrid_device_auth_expired">Kode ini telah kedaluwarsa. Coba lagi.</string>
<string name="settings_debrid_section_instant_playback">Persiapan Tautan</string>
<string name="settings_debrid_section_instant_playback">PERSIAPAN TAUTAN</string>
<string name="settings_debrid_prepare_instant_playback">Menyiapkan tautan</string>
<string name="settings_debrid_prepare_instant_playback_description">Siapkan sumber video sebelum mulai pemutaran dimulai.</string>
<string name="settings_debrid_prepare_stream_count">Jumlah sumber yang disiapkan</string>
<string name="settings_debrid_prepare_stream_count_warning">Gunakan jumlah sekecil mungkin. Penyedia layanan Debrid bisa saja membatasi seberapa banyak tautan yang dapat diurai dalam jangka waktu tertentu. Membuka film atau episode serial dihitung dalam batas tersebut meskipun Anda tidak menonton, karena tautan tetap disiapkan terlebih dahulu.</string>
<string name="settings_debrid_prepare_count_one">1 sumber</string>
<string name="settings_debrid_prepare_count_many">%1$d sumber</string>
<string name="settings_debrid_section_formatting">Format</string>
<string name="settings_debrid_section_formatting">FORMAT</string>
<string name="settings_debrid_name_template">Pola Nama</string>
<string name="settings_debrid_name_template_description">Mengatur bagaimana hasil sumber ditampilkan.</string>
<string name="settings_debrid_description_template">Pola Deskripsi</string>
@ -666,9 +667,9 @@
<string name="settings_mdb_api_key_title">Kunci API</string>
<string name="settings_mdb_enable_ratings">Aktifkan Rating MDBList</string>
<string name="settings_mdb_enable_ratings_description">Ambil rating dari penyedia eksternal di layar detail metadata</string>
<string name="settings_mdb_section_api_key">Kunci API</string>
<string name="settings_mdb_section_rating_providers">Penyedia rating eksternal</string>
<string name="settings_mdb_section_title">Rating MDBList</string>
<string name="settings_mdb_section_api_key">KUNCI API</string>
<string name="settings_mdb_section_rating_providers">PENYEDIA RATING EKSTERNAL</string>
<string name="settings_mdb_section_title">RATING MDBList</string>
<string name="settings_meta_actions">Tindakan</string>
<string name="settings_meta_actions_description">Kontrol putar dan simpan.</string>
<string name="settings_meta_cast">Pemeran</string>
@ -928,7 +929,7 @@
<string name="settings_tmdb_section_credentials">KREDENSIAL</string>
<string name="settings_tmdb_section_localization">LOKALISASI</string>
<string name="settings_tmdb_section_modules">MODUL</string>
<string name="settings_tmdb_section_title">Pengayaan TMDB</string>
<string name="settings_tmdb_section_title">PENGAYAAN TMDB</string>
<string name="settings_trakt_approval_redirect">Setelah disetujui, Anda akan diarahkan kembali secara otomatis.</string>
<string name="settings_trakt_authentication">AUTENTIKASI</string>
<string name="settings_trakt_comments">Komentar</string>

View file

@ -236,6 +236,7 @@
<string name="compose_settings_page_integrations">Integrazioni</string>
<string name="compose_settings_page_mdblist_ratings">Valutazioni MDBList</string>
<string name="compose_settings_page_meta_screen">Schermata Meta</string>
<string name="settings_appearance_section_detail_page">SCHERMATA META</string>
<string name="compose_settings_page_notifications">Notifiche</string>
<string name="compose_settings_page_playback">Riproduzione</string>
<string name="compose_settings_page_plugins">Plugin</string>
@ -624,7 +625,7 @@
<string name="settings_playback_timeout_unlimited">Illimitato</string>
<string name="settings_playback_tunneled_playback">Riproduzione Tunneled</string>
<string name="settings_playback_tunneled_playback_description">Abilita la riproduzione tunneled per una minore latenza nella sincronizzazione audio/video.</string>
<string name="settings_playback_ios_audio_output_section">Uscita Audio iOS</string>
<string name="settings_playback_ios_audio_output_section">USCITA AUDIO iOS</string>
<string name="settings_playback_ios_audio_output_auto_desc">Prova prima AVFoundation, altrimenti passa ad AudioUnit.</string>
<string name="settings_playback_ios_audio_output_audiounit_desc">Usa l'uscita legacy AudioUnit.</string>
<string name="settings_playback_ios_hardware_decoder">Decodifica hardware</string>
@ -1331,7 +1332,7 @@
<string name="settings_continue_watching_use_episode_thumbnails_description">Prediligi le miniature dell'episodio quando disponibili.</string>
<string name="settings_continue_watching_use_episode_thumbnails_title">Prediligi miniature episodio in Inizia a guardare</string>
<string name="settings_integrations_debrid_description">Connetti gli account per i link e l'accesso alla libreria</string>
<string name="settings_debrid_section_title">Servizi connessi</string>
<string name="settings_debrid_section_title">SERVIZI CONNESSI</string>
<string name="settings_debrid_experimental_notice">Queste integrazioni sono sperimentali e potrebbero essere mantenute, modificate o rimosse in seguito.</string>
<string name="settings_debrid_cloud_library">Libreria cloud</string>
<string name="settings_debrid_cloud_library_description">Sfoglia e riproduci i file già presenti nei tuoi account connessi.</string>
@ -1340,7 +1341,7 @@
<string name="settings_debrid_resolve_with">Risolvi con</string>
<string name="settings_debrid_resolve_with_description">Scegli quale account connesso gestisce i link riproducibili.</string>
<string name="settings_debrid_add_key_first">Connetti prima un account.</string>
<string name="settings_debrid_section_providers">Account</string>
<string name="settings_debrid_section_providers">ACCOUNT</string>
<string name="settings_debrid_provider_description">Connetti il tuo account %1$s.</string>
<string name="settings_debrid_provider_device_description">Collega il tuo account %1$s nel browser.</string>
<string name="settings_debrid_dialog_title">Chiave API %1$s</string>
@ -1360,26 +1361,26 @@
<string name="settings_debrid_device_auth_failed">Impossibile avviare l'accesso.</string>
<string name="settings_debrid_device_auth_missing_configuration">Questo metodo di accesso non è configurato in questa build.</string>
<string name="settings_debrid_device_auth_expired">Questo codice è scaduto. Riprova.</string>
<string name="settings_debrid_section_instant_playback">Preparazione dei link</string>
<string name="settings_debrid_section_instant_playback">PREPARAZIONE DEI LINK</string>
<string name="settings_debrid_prepare_instant_playback">Prepara i link</string>
<string name="settings_debrid_prepare_instant_playback_description">Risolvi i link riproducibili prima dell'inizio della riproduzione.</string>
<string name="settings_debrid_prepare_stream_count">Link da preparare</string>
<string name="settings_debrid_prepare_stream_count_warning">Usa un numero inferiore quando possibile. I servizi connessi potrebbero limitare la frequenza dei link che possono essere risolti in un determinato periodo di tempo. L'apertura di un film o di un episodio può essere conteggiata ai fini di tali limiti anche se non premi Guarda, poiché i link vengono preparati in anticipo.</string>
<string name="settings_debrid_prepare_count_one">1 link</string>
<string name="settings_debrid_prepare_count_many">%1$d link</string>
<string name="settings_debrid_section_formatting">Formattazione</string>
<string name="settings_debrid_section_formatting">FORMATTAZIONE</string>
<string name="settings_debrid_name_template">Modello del nome</string>
<string name="settings_debrid_name_template_description">Controlla come appaiono i nomi dei risultati.</string>
<string name="settings_debrid_description_template">Modello della descrizione</string>
<string name="settings_debrid_description_template_description">Controlla i metadati mostrati sotto ogni risultato.</string>
<string name="settings_debrid_formatter_reset_title">Ripristina formattazione</string>
<string name="settings_debrid_formatter_reset_subtitle">Ripristina la formattazione predefinita dei risultati.</string>
<string name="settings_stream_badges_section">Stile Fusion</string>
<string name="settings_stream_badges_section">STILE FUSION</string>
<string name="settings_stream_size_badges_title">Badge della dimensione</string>
<string name="settings_stream_size_badges_description">Mostra il badge della dimensione nei risultati stream e nei pannelli origine del player.</string>
<string name="settings_stream_addon_logo_title">Logo Addon</string>
<string name="settings_stream_addon_logo_description">Mostra il logo e il nome dell'addon accanto alle sorgenti degli stream.</string>
<string name="settings_stream_display_section">Display</string>
<string name="settings_stream_display_section">DISPLAY</string>
<string name="settings_stream_badge_position_title">Posizione badge</string>
<string name="settings_stream_badge_position_description">Scegli se i badge Fusion e quelli della dimensione appaiono sopra o sotto le schede stream.</string>
<string name="settings_stream_badge_position_dialog_title">Posizione badge</string>
@ -1521,6 +1522,7 @@
<string name="plugins_group_by_repo_title">Raggruppa i provider dei plugin per repository</string>
<string name="plugins_group_by_repo_desc">Mostra negli stream un solo provider per repository invece che uno per sorgente.</string>
<string name="settings_p2p_title">Streaming P2P</string>
<string name="settings_playback_section_p2p">STREAMING P2P</string>
<string name="settings_p2p_subtitle">Consenti lo streaming peer-to-peer (torrent)</string>
<string name="settings_p2p_hide_stats_title">Nascondi le statistiche torrent</string>
<string name="settings_p2p_hide_stats_subtitle">Nascondi buffer, seeds, peers e velocità download durante il caricamento e la riproduzione</string>
@ -1550,6 +1552,7 @@
<string name="compose_auth_terms_link">Termini</string>
<string name="compose_player_subtitle_lines_load_error">Impossibile caricare le righe dei sottotitoli</string>
<string name="compose_settings_page_streams">Flussi</string>
<string name="settings_appearance_section_streams">FLUSSI</string>
<string name="settings_advanced_section_diagnostics">DIAGNOSTICA</string>
<string name="settings_advanced_section_cache">CACHE</string>
<string name="settings_advanced_sentry_reports">Report arresti anomali Sentry</string>
@ -1702,10 +1705,10 @@
<string name="player_ios_preset_custom_label">Personalizzato</string>
<string name="player_ios_preset_custom_desc">Usa i valori avanzati riportati sotto.</string>
<string name="player_ios_hardware_decoder_off">Disattivato</string>
<string name="settings_playback_ios_video_output">Uscita video iOS</string>
<string name="settings_playback_ios_video_output">USCITA VIDEO iOS</string>
<string name="settings_playback_ios_audio_output">Uscita audio</string>
<string name="settings_playback_ios_audio_output_avfoundation_desc">Supporto sperimentale per Audio Spaziale e uscita multicanale.</string>
<string name="settings_debrid_section_result_management">Gestione risultati</string>
<string name="settings_debrid_section_result_management">GESTIONE RISULTATI</string>
<string name="settings_debrid_max_results">Risultati massimi</string>
<string name="settings_debrid_max_results_desc">Limita il numero di risultati visualizzati.</string>
<string name="settings_debrid_sort_results">Ordina risultati</string>

View file

@ -426,12 +426,14 @@
<string name="compose_settings_page_licenses_attributions">ライセンスと帰属</string>
<string name="compose_settings_page_mdblist_ratings">MDBList評価</string>
<string name="compose_settings_page_meta_screen">詳細ページ</string>
<string name="settings_appearance_section_detail_page">詳細ページ</string>
<string name="compose_settings_page_notifications">通知</string>
<string name="compose_settings_page_playback">再生</string>
<string name="compose_settings_page_plugins">プラグイン</string>
<string name="compose_settings_page_poster_customization">ポスターカードスタイル</string>
<string name="compose_settings_page_root">設定</string>
<string name="compose_settings_page_streams">ストリーム</string>
<string name="settings_appearance_section_streams">ストリーム</string>
<string name="compose_settings_page_supporters_contributors">サポーターとコントリビューター</string>
<string name="compose_settings_page_tmdb_enrichment">TMDB補完</string>
<string name="compose_settings_page_trakt">Trakt</string>
@ -694,7 +696,7 @@
<string name="settings_debrid_description_template_description">各結果に表示されるメタデータを制御します。空白の場合は元の結果詳細を使用します。</string>
<string name="settings_debrid_formatter_reset_title">フォーマットをリセット</string>
<string name="settings_debrid_formatter_reset_subtitle">結果のフォーマットをデフォルトに戻します。</string>
<string name="settings_stream_badges_section">Fusionスタイル</string>
<string name="settings_stream_badges_section">FUSIONスタイル</string>
<string name="settings_stream_size_badges_title">サイズバッジ</string>
<string name="settings_stream_size_badges_description">ストリーム結果とプレーヤーのソースパネルにファイルサイズバッジを表示します。</string>
<string name="settings_stream_addon_logo_title">アドオンロゴ</string>
@ -1911,6 +1913,7 @@
<string name="p2p_consent_cancel">キャンセル</string>
<string name="p2p_error_unknown">不明なトレントエラー</string>
<string name="settings_p2p_title">P2Pストリーミング</string>
<string name="settings_playback_section_p2p">P2Pストリーミング</string>
<string name="settings_p2p_subtitle">ピアツーピア(トレント)ストリームを許可する</string>
<string name="settings_p2p_hide_stats_title">トレント統計を非表示</string>
<string name="settings_p2p_hide_stats_subtitle">読み込み中および再生中のバッファ・シード数・ピア数・ダウンロード速度を非表示にする</string>

View file

@ -392,6 +392,7 @@
<string name="compose_settings_page_licenses_attributions">Lisenser &amp; attribusjon</string>
<string name="compose_settings_page_mdblist_ratings">MDBList-vurderinger</string>
<string name="compose_settings_page_meta_screen">Detaljside</string>
<string name="settings_appearance_section_detail_page">DETALJSIDE</string>
<string name="compose_settings_page_notifications">Varsler</string>
<string name="compose_settings_page_playback">Avspilling</string>
<string name="compose_settings_page_plugins">Plugins</string>
@ -523,8 +524,8 @@
<string name="settings_homescreen_section_catalogs">KATALOGER</string>
<string name="settings_homescreen_section_catalogs_collections">KATALOGER &amp; SAMLINGER</string>
<string name="settings_homescreen_section_collections">SAMLINGER</string>
<string name="settings_homescreen_section_hero">Hjemmeoppsett</string>
<string name="settings_homescreen_section_hero_sources">Hero-kataloger</string>
<string name="settings_homescreen_section_hero">HJEMMEOPPSETT</string>
<string name="settings_homescreen_section_hero_sources">HERO-KATALOGER</string>
<string name="settings_homescreen_selected_count">%1$d av %2$d valgt</string>
<string name="settings_homescreen_show_hero">Vis hero-seksjon</string>
<string name="settings_homescreen_show_hero_description">Vis hero-karusell øverst på hjemskjermen.</string>
@ -538,7 +539,7 @@
<string name="settings_hide_secret">Skjul verdi</string>
<string name="settings_playback_subtitle">Avspiller, undertekster og auto-play</string>
<string name="settings_poster_card_radius">Hjørneradius</string>
<string name="settings_poster_card_style">Plakatstil</string>
<string name="settings_poster_card_style">PLAKATSTIL</string>
<string name="settings_poster_card_width">Bredde</string>
<string name="settings_poster_custom">Tilpasset</string>
<string name="settings_poster_description">Juster kortbredde og hjørneradius.</string>
@ -567,13 +568,13 @@
<string name="settings_continue_watching_blur_next_up_title">Slør usette i Fortsett å se</string>
<string name="settings_continue_watching_show_unaired_next_up_description">Inkluder kommende episoder i Fortsett å se før de sendes.</string>
<string name="settings_continue_watching_show_unaired_next_up_title">Vis ikke-sendte neste episoder</string>
<string name="settings_continue_watching_section_sort_order">SORTERINGSREKKefølge</string>
<string name="settings_continue_watching_section_sort_order">SORTERINGSREKKEFØLGE</string>
<string name="settings_continue_watching_sort_mode_title">Sorteringsrekkefølge</string>
<string name="settings_continue_watching_sort_mode_default">Standard</string>
<string name="settings_continue_watching_sort_mode_default_desc">Sorter alle etter nyhet</string>
<string name="settings_continue_watching_sort_mode_streaming">Streaming-stil</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">Utgitt først, kommende til slutt</string>
<string name="settings_continue_watching_section_card_style">Plakatstil</string>
<string name="settings_continue_watching_section_card_style">PLAKATSTIL</string>
<string name="settings_continue_watching_section_on_launch">VED OPPSTART</string>
<string name="settings_continue_watching_section_up_next_behavior">NESTE OPPFØRSEL</string>
<string name="settings_continue_watching_section_visibility">SYNLIGHET</string>
@ -595,11 +596,11 @@
<string name="settings_content_discovery_homescreen_description">Juster hjemmeoppsett, synlighet og plakat-oppførsel.</string>
<string name="settings_content_discovery_meta_screen_description">Innstillinger for detalj- og episodeskjermer.</string>
<string name="settings_content_discovery_collections_description">Opprett egne kataloggrupperinger med mapper vist på hjem.</string>
<string name="settings_integrations_section_title">Integrasjoner</string>
<string name="settings_integrations_section_title">INTEGRASJONER</string>
<string name="settings_integrations_tmdb_description">Metadata-berikelse-kontroller</string>
<string name="settings_integrations_mdblist_description">Eksterne vurderingsleverandører</string>
<string name="settings_integrations_debrid_description">Koble til kontoer for lenker og bibliotektilgang</string>
<string name="settings_debrid_section_title">Tilkoblede tjenester</string>
<string name="settings_debrid_section_title">TILKOBLEDE TJENESTER</string>
<string name="settings_debrid_experimental_notice">Disse integrasjonene er eksperimentelle og kan endres eller fjernes senere.</string>
<string name="settings_debrid_cloud_library">Skybibliotek</string>
<string name="settings_debrid_cloud_library_description">Bla gjennom og spill filer som allerede finnes i tilkoblede kontoer.</string>
@ -608,7 +609,7 @@
<string name="settings_debrid_resolve_with">Løs med</string>
<string name="settings_debrid_resolve_with_description">Velg hvilken tilkoblet konto som håndterer spillbare lenker.</string>
<string name="settings_debrid_add_key_first">Koble til en konto først.</string>
<string name="settings_debrid_section_providers">Kontoer</string>
<string name="settings_debrid_section_providers">KONTOER</string>
<string name="settings_debrid_provider_description">Koble til %1$s-kontoen din.</string>
<string name="settings_debrid_provider_device_description">Koble til %1$s-kontoen din i nettleseren.</string>
<string name="settings_debrid_dialog_title">%1$s API-nøkkel</string>
@ -627,13 +628,13 @@
<string name="settings_debrid_device_auth_failed">Kunne ikke starte innlogging.</string>
<string name="settings_debrid_device_auth_missing_configuration">Denne innloggingsmetoden er ikke konfigurert i denne versjonen.</string>
<string name="settings_debrid_device_auth_expired">Denne koden er utløpt. Prøv igjen.</string>
<string name="settings_debrid_section_instant_playback">Lenkeforberedelse</string>
<string name="settings_debrid_section_instant_playback">LENKEFORBEREDELSE</string>
<string name="settings_debrid_prepare_instant_playback">Forbered lenker</string>
<string name="settings_debrid_prepare_instant_playback_description">Løs spillbare lenker før avspilling starter.</string>
<string name="settings_debrid_prepare_stream_count">Lenker å forberede</string>
<string name="settings_debrid_prepare_count_one">1 lenke</string>
<string name="settings_debrid_prepare_count_many">%1$d lenker</string>
<string name="settings_debrid_section_formatting">Formatering</string>
<string name="settings_debrid_section_formatting">FORMATERING</string>
<string name="settings_debrid_name_template">Navnemal</string>
<string name="settings_debrid_name_template_description">Styrer hvordan resultatnavn vises.</string>
<string name="settings_debrid_description_template">Beskrivelsesmal</string>
@ -646,9 +647,9 @@
<string name="settings_mdb_api_key_title">API-nøkkel</string>
<string name="settings_mdb_enable_ratings">Aktiver MDBList-vurderinger</string>
<string name="settings_mdb_enable_ratings_description">Hent vurderinger fra eksterne leverandører på detaljsiden</string>
<string name="settings_mdb_section_api_key">API-nøkkel</string>
<string name="settings_mdb_section_rating_providers">Eksterne vurderingsleverandører</string>
<string name="settings_mdb_section_title">MDBList-vurderinger</string>
<string name="settings_mdb_section_api_key">API-NØKKEL</string>
<string name="settings_mdb_section_rating_providers">EKSTERNE VURDERINGSLEVERANDØRER</string>
<string name="settings_mdb_section_title">MDBList-VURDERINGER</string>
<string name="settings_meta_actions">Handlinger</string>
<string name="settings_meta_actions_description">Avspillings- og lagringskontroller.</string>
<string name="settings_meta_cast">Rollebesetning</string>
@ -691,7 +692,7 @@
<string name="settings_notifications_episode_release_alerts_description">Send lokale varsler når en ny episode for en lagret serie blir tilgjengelig.</string>
<string name="settings_notifications_permission_disabled">Systemvarsler er deaktivert for Nuvio. Skru dem på for å motta varsler.</string>
<string name="settings_notifications_scheduled_count">%1$d utgivelsesvarsler er planlagt på denne enheten.</string>
<string name="settings_notifications_section_alerts">VARSler</string>
<string name="settings_notifications_section_alerts">VARSLER</string>
<string name="settings_notifications_section_test">TEST</string>
<string name="settings_notifications_send_test">Send testvarsel</string>
<string name="settings_notifications_sending_test">Sender testvarsel…</string>
@ -885,7 +886,7 @@
<string name="settings_tmdb_section_credentials">LEGITIMASJONER</string>
<string name="settings_tmdb_section_localization">LOKALISERING</string>
<string name="settings_tmdb_section_modules">MODULER</string>
<string name="settings_tmdb_section_title">TMDB-berikelse</string>
<string name="settings_tmdb_section_title">TMDB-BERIKELSE</string>
<string name="settings_trakt_approval_redirect">Etter godkjenning blir du automatisk sendt tilbake.</string>
<string name="settings_trakt_authentication">AUTENTISERING</string>
<string name="settings_trakt_comments">Kommentarer</string>

View file

@ -426,12 +426,14 @@
<string name="compose_settings_page_licenses_attributions">Licenties en attributie</string>
<string name="compose_settings_page_mdblist_ratings">MDBList-beoordelingen</string>
<string name="compose_settings_page_meta_screen">Detailpagina</string>
<string name="settings_appearance_section_detail_page">DETAILPAGINA</string>
<string name="compose_settings_page_notifications">Meldingen</string>
<string name="compose_settings_page_playback">Afspelen</string>
<string name="compose_settings_page_plugins">Plug-ins</string>
<string name="compose_settings_page_poster_customization">Posterkaartstijl</string>
<string name="compose_settings_page_root">Instellingen</string>
<string name="compose_settings_page_streams">Streams</string>
<string name="settings_appearance_section_streams">STREAMS</string>
<string name="compose_settings_page_supporters_contributors">Ondersteuners en bijdragers</string>
<string name="compose_settings_page_tmdb_enrichment">TMDB-verrijking</string>
<string name="compose_settings_page_trakt">Trakt</string>
@ -591,8 +593,8 @@
<string name="settings_homescreen_section_catalogs">CATALOGI</string>
<string name="settings_homescreen_section_catalogs_collections">CATALOGI EN COLLECTIES</string>
<string name="settings_homescreen_section_collections">COLLECTIES</string>
<string name="settings_homescreen_section_hero">Startschermindeling</string>
<string name="settings_homescreen_section_hero_sources">Hero-catalogi</string>
<string name="settings_homescreen_section_hero">STARTSCHERMINDELING</string>
<string name="settings_homescreen_section_hero_sources">HERO-CATALOGI</string>
<string name="settings_homescreen_selected_count">%1$d van %2$d geselecteerd</string>
<string name="settings_homescreen_show_hero">Herosectie tonen</string>
<string name="settings_homescreen_show_hero_description">Toon herocarrousel bovenaan de startpagina.</string>
@ -606,7 +608,7 @@
<string name="settings_hide_secret">Waarde verbergen</string>
<string name="settings_playback_subtitle">Speler, ondertitels en automatisch afspelen</string>
<string name="settings_poster_card_radius">Hoekradius</string>
<string name="settings_poster_card_style">Posterkaartstijl</string>
<string name="settings_poster_card_style">POSTERKAARTSTIJL</string>
<string name="settings_poster_card_width">Breedte</string>
<string name="settings_poster_custom">Aangepast</string>
<string name="settings_poster_description">Pas kaartbreedte en hoekradius aan.</string>
@ -641,7 +643,7 @@
<string name="settings_continue_watching_sort_mode_default_desc">Sorteer alle items op recentheid</string>
<string name="settings_continue_watching_sort_mode_streaming">Streamingstijl</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">Uitgebrachte items eerst, aankomende items achteraan</string>
<string name="settings_continue_watching_section_card_style">Posterkaartstijl</string>
<string name="settings_continue_watching_section_card_style">POSTERKAARTSTIJL</string>
<string name="settings_continue_watching_section_on_launch">BIJ OPSTARTEN</string>
<string name="settings_continue_watching_section_up_next_behavior">GEDRAG VOLGENDE AFLEVERING</string>
<string name="settings_continue_watching_section_visibility">ZICHTBAARHEID</string>
@ -665,11 +667,11 @@
<string name="settings_content_discovery_homescreen_description">Pas startschermindeling, zichtbaarheid van inhoud en postergedrag aan</string>
<string name="settings_content_discovery_meta_screen_description">Instellingen voor de detail- en afleveringsschermen.</string>
<string name="settings_content_discovery_collections_description">Maak aangepaste catalogusgroepen met mappen die op Home worden getoond.</string>
<string name="settings_integrations_section_title">Integraties</string>
<string name="settings_integrations_section_title">INTEGRATIES</string>
<string name="settings_integrations_tmdb_description">Bediening voor metadataverrijking</string>
<string name="settings_integrations_mdblist_description">Externe beoordelingsproviders</string>
<string name="settings_integrations_debrid_description">Verbind accounts voor links en bibliotheektoegang</string>
<string name="settings_debrid_section_title">Verbonden diensten</string>
<string name="settings_debrid_section_title">VERBONDEN DIENSTEN</string>
<string name="settings_debrid_experimental_notice">Deze integraties zijn experimenteel en kunnen later behouden, gewijzigd of verwijderd worden.</string>
<string name="settings_debrid_cloud_library">Cloudbibliotheek</string>
<string name="settings_debrid_cloud_library_description">Blader door en speel bestanden die al in je verbonden accounts staan.</string>
@ -678,7 +680,7 @@
<string name="settings_debrid_resolve_with">Oplossen met</string>
<string name="settings_debrid_resolve_with_description">Kies welk verbonden account afspeelbare links afhandelt.</string>
<string name="settings_debrid_add_key_first">Verbind eerst een account.</string>
<string name="settings_debrid_section_providers">Accounts</string>
<string name="settings_debrid_section_providers">ACCOUNTS</string>
<string name="settings_debrid_provider_description">Verbind je %1$s-account.</string>
<string name="settings_debrid_provider_device_description">Koppel je %1$s-account in de browser.</string>
<string name="settings_debrid_dialog_title">%1$s API-sleutel</string>
@ -698,26 +700,26 @@
<string name="settings_debrid_device_auth_failed">Kan inloggen niet starten.</string>
<string name="settings_debrid_device_auth_missing_configuration">Deze inlogmethode is niet geconfigureerd in deze build.</string>
<string name="settings_debrid_device_auth_expired">Deze code is verlopen. Probeer het opnieuw.</string>
<string name="settings_debrid_section_instant_playback">Voorbereiding van links</string>
<string name="settings_debrid_section_instant_playback">VOORBEREIDING VAN LINKS</string>
<string name="settings_debrid_prepare_instant_playback">Links voorbereiden</string>
<string name="settings_debrid_prepare_instant_playback_description">Los afspeelbare links op voordat het afspelen begint.</string>
<string name="settings_debrid_prepare_stream_count">Voor te bereiden links</string>
<string name="settings_debrid_prepare_stream_count_warning">Gebruik indien mogelijk een lager aantal. Verbonden diensten kunnen het aantal links dat binnen een bepaalde periode kan worden opgelost, beperken. Het openen van een film of aflevering kan meetellen voor die limieten, zelfs als je niet op Afspelen drukt, omdat de links van tevoren worden voorbereid.</string>
<string name="settings_debrid_prepare_count_one">1 link</string>
<string name="settings_debrid_prepare_count_many">%1$d links</string>
<string name="settings_debrid_section_formatting">Opmaak</string>
<string name="settings_debrid_section_formatting">OPMAAK</string>
<string name="settings_debrid_name_template">Naamsjabloon</string>
<string name="settings_debrid_name_template_description">Bepaalt hoe resultaatnamen worden weergegeven. Laat leeg om de originele resultaatnaam te gebruiken.</string>
<string name="settings_debrid_description_template">Beschrijvingssjabloon</string>
<string name="settings_debrid_description_template_description">Bepaalt de metadata die onder elk resultaat wordt getoond. Laat leeg om de originele resultaatdetails te gebruiken.</string>
<string name="settings_debrid_formatter_reset_title">Opmaak resetten</string>
<string name="settings_debrid_formatter_reset_subtitle">Herstel de standaard resultaatopmaak.</string>
<string name="settings_stream_badges_section">Fusion-stijl</string>
<string name="settings_stream_badges_section">FUSION-STIJL</string>
<string name="settings_stream_size_badges_title">Bestandsgrootte-badges</string>
<string name="settings_stream_size_badges_description">Toon badges met bestandsgrootte in streamresultaten en spelerbronpanelen.</string>
<string name="settings_stream_addon_logo_title">Addon-logo</string>
<string name="settings_stream_addon_logo_description">Toon addon-logo en -naam naast streambronnen.</string>
<string name="settings_stream_display_section">Weergave</string>
<string name="settings_stream_display_section">WEERGAVE</string>
<string name="settings_stream_badge_position_title">Badgepositie</string>
<string name="settings_stream_badge_position_description">Kies of Fusion- en groottebadges boven of onder streamkaarten verschijnen.</string>
<string name="settings_stream_badge_position_dialog_title">Badgepositie</string>
@ -752,9 +754,9 @@
<string name="settings_mdb_api_key_title">API-sleutel</string>
<string name="settings_mdb_enable_ratings">MDBList-beoordelingen inschakelen</string>
<string name="settings_mdb_enable_ratings_description">Haal beoordelingen op van externe providers op het detailscherm van metadata</string>
<string name="settings_mdb_section_api_key">API-sleutel</string>
<string name="settings_mdb_section_rating_providers">Externe beoordelingsproviders</string>
<string name="settings_mdb_section_title">MDBList-beoordelingen</string>
<string name="settings_mdb_section_api_key">API-SLEUTEL</string>
<string name="settings_mdb_section_rating_providers">EXTERNE BEOORDELINGSPROVIDERS</string>
<string name="settings_mdb_section_title">MDBList-BEOORDELINGEN</string>
<string name="settings_meta_actions">Acties</string>
<string name="settings_meta_actions_description">Bedieningselementen voor afspelen en opslaan.</string>
<string name="settings_meta_cast">Cast</string>
@ -1050,7 +1052,7 @@
<string name="settings_tmdb_section_credentials">INLOGGEGEVENS</string>
<string name="settings_tmdb_section_localization">LOKALISATIE</string>
<string name="settings_tmdb_section_modules">MODULES</string>
<string name="settings_tmdb_section_title">TMDB-verrijking</string>
<string name="settings_tmdb_section_title">TMDB-VERRIJKING</string>
<string name="settings_trakt_approval_redirect">Na goedkeuring word je automatisch teruggeleid.</string>
<string name="settings_trakt_authentication">AUTHENTICATIE</string>
<string name="settings_trakt_comments">Reacties</string>
@ -1659,7 +1661,7 @@
<string name="player_ios_preset_custom_desc">Gebruik je geavanceerde waarden hieronder.</string>
<string name="player_ios_hardware_decoder_off">Uit</string>
<!-- Playback settings iOS video output -->
<string name="settings_playback_ios_video_output">iOS-video-uitvoer</string>
<string name="settings_playback_ios_video_output">iOS-VIDEO-UITVOER</string>
<string name="settings_playback_ios_hardware_decoder">Hardwaredecoder</string>
<string name="settings_playback_ios_extended_dynamic_range">Uitgebreid dynamisch bereik</string>
<string name="settings_playback_ios_extended_dynamic_range_desc">Standaard Metal-uitvoermodus voor nieuwe afspeelsessies.</string>
@ -1668,13 +1670,13 @@
<string name="settings_playback_ios_target_primaries">Doelprimairen</string>
<string name="settings_playback_ios_target_transfer">Doeloverdracht</string>
<!-- Playback settings iOS audio output -->
<string name="settings_playback_ios_audio_output_section">iOS-audio-uitvoer</string>
<string name="settings_playback_ios_audio_output_section">iOS-AUDIO-UITVOER</string>
<string name="settings_playback_ios_audio_output">Audio-uitvoer</string>
<string name="settings_playback_ios_audio_output_auto_desc">Gebruik AudioUnit terwijl AVFoundation-uitvoer tijdelijk is uitgeschakeld.</string>
<string name="settings_playback_ios_audio_output_avfoundation_desc">Experimentele ondersteuning voor Spatial Audio en meerkanaals uitvoer.</string>
<string name="settings_playback_ios_audio_output_audiounit_desc">Gebruik de verouderde AudioUnit-uitvoer.</string>
<!-- Debrid Result Management section -->
<string name="settings_debrid_section_result_management">Resultaatbeheer</string>
<string name="settings_debrid_section_result_management">RESULTAATBEHEER</string>
<string name="settings_debrid_max_results">Max. resultaten</string>
<string name="settings_debrid_max_results_desc">Beperk het aantal weergegeven resultaten.</string>
<string name="settings_debrid_sort_results">Resultaten sorteren</string>
@ -1942,6 +1944,7 @@
<string name="p2p_consent_cancel">Annuleren</string>
<string name="p2p_error_unknown">Onbekende torrentfout</string>
<string name="settings_p2p_title">P2P-streaming</string>
<string name="settings_playback_section_p2p">P2P-STREAMING</string>
<string name="settings_p2p_subtitle">Peer-to-peer (torrent) streams toestaan</string>
<string name="settings_p2p_hide_stats_title">Torrentstatistieken verbergen</string>
<string name="settings_p2p_hide_stats_subtitle">Verberg buffer, seeds, peers en downloadsnelheid tijdens laden en afspelen</string>

View file

@ -404,6 +404,7 @@
<string name="compose_settings_page_licenses_attributions">Licencje i atrybucje</string>
<string name="compose_settings_page_mdblist_ratings">Oceny MDBList</string>
<string name="compose_settings_page_meta_screen">Ekran metadanych</string>
<string name="settings_appearance_section_detail_page">EKRAN METADANYCH</string>
<string name="compose_settings_page_notifications">Powiadomienia</string>
<string name="compose_settings_page_playback">Odtwarzanie</string>
<string name="compose_settings_page_plugins">Wtyczki</string>
@ -589,6 +590,9 @@
<string name="settings_continue_watching_sort_mode_default_desc">Sortuj wszystkie elementy według czasu</string>
<string name="settings_continue_watching_sort_mode_streaming">Styl streamingowy</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">Wydane najpierw, nadchodzące na końcu</string>
<string name="settings_continue_watching_sort_mode_split_upcoming">Oddzielny rząd Nadchodzące</string>
<string name="settings_continue_watching_sort_mode_split_upcoming_desc">Przenieś niewydane odcinki do oddzielnego rzędu Nadchodzące</string>
<string name="upcoming_section_title">Nadchodzące</string>
<string name="settings_continue_watching_section_card_style">STYL KARTY</string>
<string name="settings_continue_watching_section_on_launch">PRZY URUCHOMIENIU</string>
<string name="settings_continue_watching_section_up_next_behavior">ZACHOWANIE NASTĘPNEGO</string>
@ -615,7 +619,7 @@
<string name="settings_integrations_tmdb_description">Wzbogać strony szczegółów grafikami TMDB, obsadą, metadanymi odcinków i nie tylko.</string>
<string name="settings_integrations_mdblist_description">Dodaj oceny IMDb, Rotten Tomatoes, Metacritic i inne zewnętrzne oceny do stron szczegółów.</string>
<string name="settings_integrations_debrid_description">Eksperymentalne źródła z kont chmurowych</string>
<string name="settings_debrid_section_title">Debrid</string>
<string name="settings_debrid_section_title">DEBRID</string>
<string name="settings_debrid_experimental_notice">Obsługa Debrid jest eksperymentalna i może zostać zachowana, zmieniona lub usunięta w przyszłości.</string>
<string name="settings_debrid_cloud_library">Biblioteka w chmurze</string>
<string name="settings_debrid_cloud_library_description">Przeglądaj i odtwarzaj pliki z połączonych kont.</string>
@ -624,7 +628,7 @@
<string name="settings_debrid_resolve_with">Rozwiązuj przez</string>
<string name="settings_debrid_resolve_with_description">Wybierz, które połączone konto obsługuje odtwarzalne linki.</string>
<string name="settings_debrid_add_key_first">Najpierw dodaj klucz API.</string>
<string name="settings_debrid_section_providers">Konto</string>
<string name="settings_debrid_section_providers">KONTO</string>
<string name="settings_debrid_provider_description">Połącz swoje konto %1$s.</string>
<string name="settings_debrid_provider_device_description">Połącz konto %1$s w przeglądarce.</string>
<string name="settings_debrid_provider_torbox_description">Połącz swoje konto Torbox.</string>
@ -645,14 +649,14 @@
<string name="settings_debrid_device_auth_failed">Nie udało się rozpocząć logowania.</string>
<string name="settings_debrid_device_auth_missing_configuration">Ta metoda logowania nie jest skonfigurowana w tej wersji.</string>
<string name="settings_debrid_device_auth_expired">Ten kod wygasł. Spróbuj ponownie.</string>
<string name="settings_debrid_section_instant_playback">Natychmiastowe odtwarzanie</string>
<string name="settings_debrid_section_instant_playback">NATYCHMIASTOWE ODTWARZANIE</string>
<string name="settings_debrid_prepare_instant_playback">Przygotuj linki</string>
<string name="settings_debrid_prepare_instant_playback_description">Rozwiąż pierwsze źródła przed rozpoczęciem odtwarzania.</string>
<string name="settings_debrid_prepare_stream_count">Źródła do przygotowania</string>
<string name="settings_debrid_prepare_stream_count_warning">Używaj niższej liczby, gdy to możliwe. Usługi Debrid mogą ograniczać liczbę linków rozwiązywanych w danym okresie. Otwarcie filmu lub odcinka może się wliczać do tych limitów, nawet jeśli nie naciśniesz Odtwórz, ponieważ linki są przygotowywane z wyprzedzeniem.</string>
<string name="settings_debrid_prepare_count_one">1 źródło</string>
<string name="settings_debrid_prepare_count_many">%1$d źródeł</string>
<string name="settings_debrid_section_formatting">Formatowanie</string>
<string name="settings_debrid_section_formatting">FORMATOWANIE</string>
<string name="settings_debrid_name_template">Szablon nazwy</string>
<string name="settings_debrid_name_template_description">Kontroluje sposób wyświetlania nazw źródeł.</string>
<string name="settings_debrid_description_template">Szablon opisu</string>
@ -1468,7 +1472,7 @@
<string name="cloud_library_type_web">Web</string>
<string name="settings_stream_addon_logo_title">Logo dodatku</string>
<string name="settings_stream_addon_logo_description">Pokazuj logo i nazwę dodatku obok źródeł.</string>
<string name="settings_stream_display_section">Wyświetlanie</string>
<string name="settings_stream_display_section">WYŚWIETLANIE</string>
<string name="settings_playback_option_original">Język oryginalny</string>
<string name="settings_playback_option_original_hint">Wymaga włączonego wzbogacania TMDB</string>
<string name="settings_appearance_app_language_device">Język urządzenia</string>
@ -1543,6 +1547,7 @@
<string name="compose_player_subtitle_lines_load_error">Nie udało się załadować linii napisów</string>
<string name="compose_settings_page_advanced">Zaawansowane</string>
<string name="compose_settings_page_streams">Źródła</string>
<string name="settings_appearance_section_streams">ŹRÓDŁA</string>
<string name="compose_settings_root_advanced_description">Uruchamianie i zachowanie profilu.</string>
<string name="compose_settings_root_advanced_section">ZAAWANSOWANE</string>
<string name="compose_settings_root_streams_description">Wyświetlanie wyników źródeł i reguły badge URL.</string>
@ -1730,7 +1735,7 @@
<string name="settings_debrid_rule_required_resolutions_desc">Pokazuj tylko wybrane rozdzielczości.</string>
<string name="settings_debrid_rule_required_visual_tags">Wymagane tagi wizualne</string>
<string name="settings_debrid_rule_required_visual_tags_desc">Wymagaj DV, HDR, 10bit, IMAX, SDR i podobnych tagów.</string>
<string name="settings_debrid_section_result_management">Zarządzanie wynikami</string>
<string name="settings_debrid_section_result_management">ZARZĄDZANIE WYNIKAMI</string>
<string name="settings_debrid_selection_any">Dowolne</string>
<string name="settings_debrid_selection_count">%1$d wybranych</string>
<string name="settings_debrid_size_min">%1$dGB+</string>
@ -1767,6 +1772,7 @@
<string name="settings_p2p_hide_stats_title">Ukryj statystyki torrenta</string>
<string name="settings_p2p_subtitle">Zezwalaj na strumienie peer-to-peer (torrent)</string>
<string name="settings_p2p_title">Strumieniowanie P2P</string>
<string name="settings_playback_section_p2p">STRUMIENIOWANIE P2P</string>
<string name="settings_playback_introdb_invalid_api_key">Nieprawidłowy klucz API lub błąd połączenia</string>
<string name="settings_playback_introdb_invalid_key">Nieprawidłowy klucz API lub błąd połączenia</string>
<string name="settings_playback_ios_audio_output">Wyjście audio</string>
@ -1774,7 +1780,7 @@
<string name="settings_playback_ios_audio_output_auto_desc">Użyj AudioUnit, gdy wyjście AVFoundation jest tymczasowo wyłączone.</string>
<string name="settings_playback_ios_audio_output_avfoundation_desc">Eksperymentalna obsługa Spatial Audio i wielokanałowego wyjścia.</string>
<string name="settings_playback_ios_audio_output_dialog">Wyjście audio</string>
<string name="settings_playback_ios_audio_output_section">Wyjście audio iOS</string>
<string name="settings_playback_ios_audio_output_section">WYJŚCIE AUDIO iOS</string>
<string name="settings_playback_ios_display_color_hint">Wskazówka koloru wyświetlacza</string>
<string name="settings_playback_ios_display_color_hint_desc">Pozwól mpv celować w aktywną przestrzeń kolorów wyświetlacza.</string>
<string name="settings_playback_ios_extended_dynamic_range">Rozszerzony zakres dynamiczny</string>
@ -1785,7 +1791,7 @@
<string name="settings_playback_ios_target_primaries_dialog">Docelowe kolory podstawowe</string>
<string name="settings_playback_ios_target_transfer">Docelowy transfer</string>
<string name="settings_playback_ios_target_transfer_dialog">Docelowy transfer</string>
<string name="settings_playback_ios_video_output">Wyjście wideo iOS</string>
<string name="settings_playback_ios_video_output">WYJŚCIE WIDEO iOS</string>
<string name="settings_playback_touch_gestures">Gesty dotykowe</string>
<string name="settings_playback_touch_gestures_description">Pozwól na przesunięcia i podwójne dotknięcia na powierzchni odtwarzacza do przewijania, regulacji jasności lub głośności.</string>
<string name="settings_stream_badge_enter_url">Wprowadź URL JSON badge.</string>
@ -1797,7 +1803,7 @@
<string name="settings_stream_badge_position_dialog_title">Pozycja badge</string>
<string name="settings_stream_badge_position_title">Pozycja badge</string>
<string name="settings_stream_badge_position_top">Na górze</string>
<string name="settings_stream_badges_section">Styl Fusion</string>
<string name="settings_stream_badges_section">STYL FUSION</string>
<string name="settings_stream_badge_url_scheme_invalid">URL badge musi zaczynać się od http:// lub https://.</string>
<string name="settings_stream_badge_urls_description">Importuj do %1$d URL JSON badge Fusion. Każdy URL można aktualizować lub usuwać osobno.</string>
<string name="settings_stream_badge_urls_search_description">Zarządzaj zaimportowanymi URL JSON badge Fusion.</string>
@ -1906,7 +1912,7 @@
<string name="settings_nav_bar_style_expanded">Zawsze rozwinięty</string>
<string name="settings_nav_bar_style_compact">Zawsze kompaktowy</string>
<string name="settings_nav_bar_style_classic">Klasyczny</string>
<string name="settings_card_depth_title">Efekt głębi karty</string>
<string name="settings_card_depth_title">EFEKT GŁĘBI KARTY</string>
<string name="settings_card_depth_description">Dodaje podświetloną górną krawędź i delikatny połysk do kart graficznych, tworząc subtelne wrażenie głębi.</string>
<string name="settings_card_depth_enabled">Włącz efekt głębi</string>
<string name="settings_card_depth_edge">Blask krawędzi</string>

View file

@ -393,6 +393,7 @@
<string name="compose_settings_page_licenses_attributions">Licenças &amp; Atribuições</string>
<string name="compose_settings_page_mdblist_ratings">Classificações do MDBList</string>
<string name="compose_settings_page_meta_screen">Página de Detalhes</string>
<string name="settings_appearance_section_detail_page">PÁGINA DE DETALHES</string>
<string name="compose_settings_page_notifications">Notificações</string>
<string name="compose_settings_page_playback">Reprodução</string>
<string name="compose_settings_page_plugins">Plugins</string>
@ -524,8 +525,8 @@
<string name="settings_homescreen_section_catalogs">CATÁLOGOS</string>
<string name="settings_homescreen_section_catalogs_collections">CATÁLOGOS &amp; COLEÇÕES</string>
<string name="settings_homescreen_section_collections">COLEÇÕES</string>
<string name="settings_homescreen_section_hero">Layout da Página Inicial</string>
<string name="settings_homescreen_section_hero_sources">Catálogos do Hero</string>
<string name="settings_homescreen_section_hero">LAYOUT DA PÁGINA INICIAL</string>
<string name="settings_homescreen_section_hero_sources">CATÁLOGOS DO HERO</string>
<string name="settings_homescreen_selected_count">%1$d de %2$d selecionado(s)</string>
<string name="settings_homescreen_show_hero">Mostrar Seção Hero</string>
<string name="settings_homescreen_show_hero_description">Exibir carrossel hero no topo da página inicial.</string>
@ -539,7 +540,7 @@
<string name="settings_hide_secret">Ocultar valor</string>
<string name="settings_playback_subtitle">Player, legendas e reprodução automática</string>
<string name="settings_poster_card_radius">Raio dos Cantos</string>
<string name="settings_poster_card_style">Estilo do Card de Pôster</string>
<string name="settings_poster_card_style">ESTILO DO CARD DE PÔSTER</string>
<string name="settings_poster_card_width">Largura</string>
<string name="settings_poster_custom">Personalizado</string>
<string name="settings_poster_description">Ajuste largura do card e raio dos cantos.</string>
@ -574,7 +575,7 @@
<string name="settings_continue_watching_sort_mode_default_desc">Classificar todos os itens por recenticidade</string>
<string name="settings_continue_watching_sort_mode_streaming">Estilo Streaming</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">Itens lançados primeiro, futuros no final</string>
<string name="settings_continue_watching_section_card_style">Estilo do Card de Pôster</string>
<string name="settings_continue_watching_section_card_style">ESTILO DO CARD DE PÔSTER</string>
<string name="settings_continue_watching_section_on_launch">AO INICIAR</string>
<string name="settings_continue_watching_section_up_next_behavior">COMPORTAMENTO DO PRÓXIMO</string>
<string name="settings_continue_watching_section_visibility">VISIBILIDADE</string>
@ -596,29 +597,29 @@
<string name="settings_content_discovery_homescreen_description">Ajuste layout da página inicial, visibilidade de conteúdo e comportamento de pôsteres</string>
<string name="settings_content_discovery_meta_screen_description">Configurações para as telas de detalhes e episódios.</string>
<string name="settings_content_discovery_collections_description">Crie agrupamentos personalizados de catálogos com pastas exibidas na Página Inicial.</string>
<string name="settings_integrations_section_title">Integrações</string>
<string name="settings_integrations_section_title">INTEGRAÇÕES</string>
<string name="settings_integrations_tmdb_description">Controles de enriquecimento de metadados</string>
<string name="settings_integrations_mdblist_description">Provedores externos de classificações</string>
<string name="settings_integrations_debrid_description">Fontes de contas em nuvem experimentais</string>
<string name="settings_debrid_section_title">Debrid</string>
<string name="settings_debrid_section_title">DEBRID</string>
<string name="settings_debrid_experimental_notice">O suporte a Debrid é experimental e pode ser mantido, alterado ou removido posteriormente.</string>
<string name="settings_debrid_enable">Ativar fontes</string>
<string name="settings_debrid_enable_description">Exibir resultados reproduzíveis de contas conectadas.</string>
<string name="settings_debrid_add_key_first">Adicione uma chave de API primeiro.</string>
<string name="settings_debrid_section_providers">Conta</string>
<string name="settings_debrid_section_providers">CONTA</string>
<string name="settings_debrid_provider_torbox_description">Conecte sua conta do Torbox.</string>
<string name="settings_debrid_dialog_title">Chave de API do Torbox</string>
<string name="settings_debrid_dialog_subtitle">Insira sua chave de API do Torbox.</string>
<string name="settings_debrid_dialog_placeholder">Insira a chave de API do Torbox</string>
<string name="settings_debrid_not_set">Não definido</string>
<string name="settings_debrid_section_instant_playback">Reprodução Instantânea</string>
<string name="settings_debrid_section_instant_playback">REPRODUÇÃO INSTANTÂNEA</string>
<string name="settings_debrid_prepare_instant_playback">Preparar links</string>
<string name="settings_debrid_prepare_instant_playback_description">Resolver as primeiras fontes antes do início da reprodução.</string>
<string name="settings_debrid_prepare_stream_count">Fontes a preparar</string>
<string name="settings_debrid_prepare_stream_count_warning">Use uma contagem menor quando possível. Os serviços de Debrid podem limitar a taxa de quantos links podem ser resolvidos em um período de tempo. Abrir um filme ou episódio pode contar para esses limites mesmo se você não pressionar Assistir, porque os links são preparados antecipadamente.</string>
<string name="settings_debrid_prepare_count_one">1 fonte</string>
<string name="settings_debrid_prepare_count_many">%1$d fontes</string>
<string name="settings_debrid_section_formatting">Formatação</string>
<string name="settings_debrid_section_formatting">FORMATAÇÃO</string>
<string name="settings_debrid_name_template">Modelo de nome</string>
<string name="settings_debrid_name_template_description">Controla como os nomes das fontes aparecem.</string>
<string name="settings_debrid_description_template">Modelo de descrição</string>
@ -633,9 +634,9 @@
<string name="settings_mdb_api_key_title">Chave de API</string>
<string name="settings_mdb_enable_ratings">Ativar Classificações do MDBList</string>
<string name="settings_mdb_enable_ratings_description">Buscar classificações de provedores externos na tela de detalhes de metadados</string>
<string name="settings_mdb_section_api_key">Chave de API</string>
<string name="settings_mdb_section_rating_providers">Provedores externos de classificações</string>
<string name="settings_mdb_section_title">Classificações do MDBList</string>
<string name="settings_mdb_section_api_key">CHAVE DE API</string>
<string name="settings_mdb_section_rating_providers">PROVEDORES EXTERNOS DE CLASSIFICAÇÕES</string>
<string name="settings_mdb_section_title">CLASSIFICAÇÕES DO MDBList</string>
<string name="settings_meta_actions">Ações</string>
<string name="settings_meta_actions_description">Controles de reprodução e salvamento.</string>
<string name="settings_meta_cast">Elenco</string>
@ -874,7 +875,7 @@
<string name="settings_tmdb_section_credentials">CREDENCIAIS</string>
<string name="settings_tmdb_section_localization">LOCALIZAÇÃO</string>
<string name="settings_tmdb_section_modules">MÓDULOS</string>
<string name="settings_tmdb_section_title">Enriquecimento do TMDB</string>
<string name="settings_tmdb_section_title">ENRIQUECIMENTO DO TMDB</string>
<string name="settings_trakt_approval_redirect">Após a aprovação, você será redirecionado de volta automaticamente.</string>
<string name="settings_trakt_authentication">AUTENTICAÇÃO</string>
<string name="settings_trakt_comments">Comentários</string>

View file

@ -421,12 +421,14 @@
<string name="compose_settings_page_licenses_attributions">Licenças e atribuição</string>
<string name="compose_settings_page_mdblist_ratings">Classificações MDBList</string>
<string name="compose_settings_page_meta_screen">Página de detalhes</string>
<string name="settings_appearance_section_detail_page">PÁGINA DE DETALHES</string>
<string name="compose_settings_page_notifications">Notificações</string>
<string name="compose_settings_page_playback">Reprodução</string>
<string name="compose_settings_page_plugins">Plugins</string>
<string name="compose_settings_page_poster_customization">Estilo dos cartões com cartaz</string>
<string name="compose_settings_page_root">Definições</string>
<string name="compose_settings_page_streams">Streams</string>
<string name="settings_appearance_section_streams">STREAMS</string>
<string name="compose_settings_page_supporters_contributors">Apoiantes e colaboradores</string>
<string name="compose_settings_page_tmdb_enrichment">Enriquecimento TMDB</string>
<string name="compose_settings_page_trakt">Trakt</string>
@ -567,8 +569,8 @@
<string name="settings_homescreen_section_catalogs">CATÁLOGOS</string>
<string name="settings_homescreen_section_catalogs_collections">CATÁLOGOS E COLEÇÕES</string>
<string name="settings_homescreen_section_collections">COLEÇÕES</string>
<string name="settings_homescreen_section_hero">Esquema da página inicial</string>
<string name="settings_homescreen_section_hero_sources">Catálogos em destaque</string>
<string name="settings_homescreen_section_hero">ESQUEMA DA PÁGINA INICIAL</string>
<string name="settings_homescreen_section_hero_sources">CATÁLOGOS EM DESTAQUE</string>
<string name="settings_homescreen_selected_count">%1$d de %2$d selecionados</string>
<string name="settings_homescreen_show_hero">Mostrar secção de destaques</string>
<string name="settings_homescreen_show_hero_description">Apresenta o carrossel de destaques no topo da página inicial.</string>
@ -582,7 +584,7 @@
<string name="settings_hide_secret">Ocultar valor</string>
<string name="settings_playback_subtitle">Leitor, legendas e reprodução automática</string>
<string name="settings_poster_card_radius">Raio dos cantos</string>
<string name="settings_poster_card_style">Estilo dos cartões com cartaz</string>
<string name="settings_poster_card_style">ESTILO DOS CARTÕES COM CARTAZ</string>
<string name="settings_poster_card_width">Largura</string>
<string name="settings_poster_custom">Personalizado</string>
<string name="settings_poster_description">Ajusta a largura dos cartões e o raio dos cantos.</string>
@ -617,7 +619,7 @@
<string name="settings_continue_watching_sort_mode_default_desc">Ordena todos os itens pelos mais recentes</string>
<string name="settings_continue_watching_sort_mode_streaming">Estilo de plataforma de streaming</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">Itens já lançados primeiro, os próximos no fim</string>
<string name="settings_continue_watching_section_card_style">Estilo dos cartões com cartaz</string>
<string name="settings_continue_watching_section_card_style">ESTILO DOS CARTÕES COM CARTAZ</string>
<string name="settings_continue_watching_section_on_launch">AO INICIAR</string>
<string name="settings_continue_watching_section_up_next_behavior">COMPORTAMENTO DO SEGUINTE</string>
<string name="settings_continue_watching_section_visibility">VISIBILIDADE</string>
@ -641,11 +643,11 @@
<string name="settings_content_discovery_homescreen_description">Ajusta o esquema da página inicial, a visibilidade do conteúdo e o comportamento dos cartazes.</string>
<string name="settings_content_discovery_meta_screen_description">Definições para as páginas de detalhes e de episódios.</string>
<string name="settings_content_discovery_collections_description">Cria agrupamentos personalizados de catálogos com pastas apresentadas no Início.</string>
<string name="settings_integrations_section_title">Integrações</string>
<string name="settings_integrations_section_title">INTEGRAÇÕES</string>
<string name="settings_integrations_tmdb_description">Controlos de enriquecimento de metadados</string>
<string name="settings_integrations_mdblist_description">Fornecedores de classificações externas</string>
<string name="settings_integrations_debrid_description">Liga contas para obter links e acesso à biblioteca</string>
<string name="settings_debrid_section_title">Serviços ligados</string>
<string name="settings_debrid_section_title">SERVIÇOS LIGADOS</string>
<string name="settings_debrid_experimental_notice">Estas integrações são experimentais e podem ser mantidas, alteradas ou removidas mais tarde.</string>
<string name="settings_debrid_cloud_library">Biblioteca na nuvem</string>
<string name="settings_debrid_cloud_library_description">Explora e reproduz ficheiros que já se encontram nas tuas contas ligadas.</string>
@ -654,7 +656,7 @@
<string name="settings_debrid_resolve_with">Resolver com</string>
<string name="settings_debrid_resolve_with_description">Escolhe qual das contas ligadas deve gerir os links reproduzíveis.</string>
<string name="settings_debrid_add_key_first">Liga uma conta primeiro.</string>
<string name="settings_debrid_section_providers">Contas</string>
<string name="settings_debrid_section_providers">CONTAS</string>
<string name="settings_debrid_provider_description">Liga a tua conta %1$s.</string>
<string name="settings_debrid_provider_device_description">Associa a tua conta %1$s no navegador.</string>
<string name="settings_debrid_dialog_title">Chave API do %1$s</string>
@ -674,21 +676,21 @@
<string name="settings_debrid_device_auth_failed">Não foi possível iniciar o início de sessão.</string>
<string name="settings_debrid_device_auth_missing_configuration">Este método de início de sessão não está configurado nesta versão da app.</string>
<string name="settings_debrid_device_auth_expired">Este código expirou. Tenta novamente.</string>
<string name="settings_debrid_section_instant_playback">Preparação de links</string>
<string name="settings_debrid_section_instant_playback">PREPARAÇÃO DE LINKS</string>
<string name="settings_debrid_prepare_instant_playback">Preparar links</string>
<string name="settings_debrid_prepare_instant_playback_description">Resolve links reproduzíveis antes de a reprodução começar.</string>
<string name="settings_debrid_prepare_stream_count">Links a preparar</string>
<string name="settings_debrid_prepare_stream_count_warning">Utiliza um número mais baixo quando possível. Os serviços ligados podem limitar a frequência com que os links são resolvidos num determinado período. Abrir um filme ou episódio pode contar para esses limites mesmo que não toques em Reproduzir, porque os links são preparados antecipadamente.</string>
<string name="settings_debrid_prepare_count_one">1 link</string>
<string name="settings_debrid_prepare_count_many">%1$d links</string>
<string name="settings_debrid_section_formatting">Formatação</string>
<string name="settings_debrid_section_formatting">FORMATAÇÃO</string>
<string name="settings_debrid_name_template">Modelo de nome</string>
<string name="settings_debrid_name_template_description">Controla a forma como os nomes dos resultados aparecem. Deixa em branco para utilizar o nome original.</string>
<string name="settings_debrid_description_template">Modelo de descrição</string>
<string name="settings_debrid_description_template_description">Controla os metadados apresentados sob cada resultado. Deixa em branco para utilizar os detalhes originais.</string>
<string name="settings_debrid_formatter_reset_title">Repor formatação</string>
<string name="settings_debrid_formatter_reset_subtitle">Restaura a formatação predefinida dos resultados.</string>
<string name="settings_stream_badges_section">Estilo Fusion</string>
<string name="settings_stream_badges_section">ESTILO FUSION</string>
<string name="settings_stream_size_badges_title">Emblemas de tamanho</string>
<string name="settings_stream_size_badges_description">Mostra emblemas com o tamanho do ficheiro nos resultados de streams e nos painéis de fontes do leitor.</string>
<string name="settings_stream_addon_logo_title">Logótipo do addon</string>
@ -724,9 +726,9 @@
<string name="settings_mdb_api_key_title">Chave API</string>
<string name="settings_mdb_enable_ratings">Ativar classificações do MDBList</string>
<string name="settings_mdb_enable_ratings_description">Obtém classificações de fornecedores externos na página de detalhes.</string>
<string name="settings_mdb_section_api_key">Chave API</string>
<string name="settings_mdb_section_rating_providers">Fornecedores de classificações externos</string>
<string name="settings_mdb_section_title">Classificações MDBList</string>
<string name="settings_mdb_section_api_key">CHAVE API</string>
<string name="settings_mdb_section_rating_providers">FORNECEDORES DE CLASSIFICAÇÕES EXTERNOS</string>
<string name="settings_mdb_section_title">CLASSIFICAÇÕES MDBList</string>
<string name="settings_meta_actions">Ações</string>
<string name="settings_meta_actions_description">Controlos de reprodução e gravação.</string>
<string name="settings_meta_cast">Elenco</string>
@ -996,7 +998,7 @@
<string name="settings_tmdb_section_credentials">CREDENCIAIS</string>
<string name="settings_tmdb_section_localization">LOCALIZAÇÃO</string>
<string name="settings_tmdb_section_modules">MÓDULOS</string>
<string name="settings_tmdb_section_title">Enriquecimento TMDB</string>
<string name="settings_tmdb_section_title">ENRIQUECIMENTO TMDB</string>
<string name="settings_trakt_approval_redirect">Após a aprovação, serás redirecionado de volta automaticamente.</string>
<string name="settings_trakt_authentication">AUTENTICAÇÃO</string>
<string name="settings_trakt_comments">Comentários</string>
@ -1586,7 +1588,7 @@
<string name="player_ios_preset_custom_label">Personalizado</string>
<string name="player_ios_preset_custom_desc">Utiliza os valores avançados abaixo.</string>
<string name="player_ios_hardware_decoder_off">Desativado</string>
<string name="settings_playback_ios_video_output">Saída de vídeo do iOS</string>
<string name="settings_playback_ios_video_output">SAÍDA DE VÍDEO DO iOS</string>
<string name="settings_playback_ios_hardware_decoder">Descodificador por hardware</string>
<string name="settings_playback_ios_extended_dynamic_range">Gama dinâmica alargada</string>
<string name="settings_playback_ios_extended_dynamic_range_desc">Modo de saída Metal predefinido para novas sessões de reprodução.</string>
@ -1599,7 +1601,7 @@
<string name="settings_playback_ios_audio_output_auto_desc">Use AudioUnit enquanto a saída AVFoundation estiver temporariamente desativada.</string>
<string name="settings_playback_ios_audio_output_avfoundation_desc">Suporte experimental para Áudio Espacial e saída multicanal.</string>
<string name="settings_playback_ios_audio_output_audiounit_desc">Utiliza a saída AudioUnit antiga.</string>
<string name="settings_debrid_section_result_management">Gestão dos resultados</string>
<string name="settings_debrid_section_result_management">GESTÃO DOS RESULTADOS</string>
<string name="settings_debrid_max_results">Máximo de resultados</string>
<string name="settings_debrid_max_results_desc">Limita quantos resultados aparecem.</string>
<string name="settings_debrid_sort_results">Ordenar resultados</string>
@ -1843,6 +1845,7 @@
<string name="p2p_consent_enable">Ativar P2P</string>
<string name="p2p_consent_cancel">Cancelar</string>
<string name="settings_p2p_title">Streaming P2P</string>
<string name="settings_playback_section_p2p">STREAMING P2P</string>
<string name="settings_p2p_subtitle">Permitir streams ponto a ponto (torrent)</string>
<string name="settings_p2p_hide_stats_title">Ocultar estatísticas de torrent</string>
<string name="settings_p2p_hide_stats_subtitle">Oculta buffer, seeds, peers e velocidade de transferência durante o carregamento e a reprodução</string>

View file

@ -426,12 +426,14 @@
<string name="compose_settings_page_licenses_attributions">Licențe și atribuiri</string>
<string name="compose_settings_page_mdblist_ratings">Evaluări MDBList</string>
<string name="compose_settings_page_meta_screen">Pagina de detalii</string>
<string name="settings_appearance_section_detail_page">PAGINA DE DETALII</string>
<string name="compose_settings_page_notifications">Notificări</string>
<string name="compose_settings_page_playback">Redare</string>
<string name="compose_settings_page_plugins">Module (Plugins)</string>
<string name="compose_settings_page_poster_customization">Stil card poster</string>
<string name="compose_settings_page_root">Setări</string>
<string name="compose_settings_page_streams">Fluxuri</string>
<string name="settings_appearance_section_streams">FLUXURI</string>
<string name="compose_settings_page_supporters_contributors">Susținători și colaboratori</string>
<string name="compose_settings_page_tmdb_enrichment">Îmbunătățire TMDB</string>
<string name="compose_settings_page_trakt">Trakt</string>
@ -591,8 +593,8 @@
<string name="settings_homescreen_section_catalogs">CATALOAGE</string>
<string name="settings_homescreen_section_catalogs_collections">CATALOAGE ȘI COLECȚII</string>
<string name="settings_homescreen_section_collections">COLECȚII</string>
<string name="settings_homescreen_section_hero">Aspect ecran principal</string>
<string name="settings_homescreen_section_hero_sources">Cataloage secțiune principală (Hero)</string>
<string name="settings_homescreen_section_hero">ASPECT ECRAN PRINCIPAL</string>
<string name="settings_homescreen_section_hero_sources">CATALOAGE SECȚIUNE PRINCIPALĂ (HERO)</string>
<string name="settings_homescreen_selected_count">%1$d din %2$d selectate</string>
<string name="settings_homescreen_show_hero">Afișează secțiunea principală (Hero)</string>
<string name="settings_homescreen_show_hero_description">Afișează un carusel în partea de sus a ecranului principal.</string>
@ -606,7 +608,7 @@
<string name="settings_hide_secret">Ascunde valoarea</string>
<string name="settings_playback_subtitle">Player, subtitrări și redare automată</string>
<string name="settings_poster_card_radius">Raza colțurilor</string>
<string name="settings_poster_card_style">Stil card poster</string>
<string name="settings_poster_card_style">STIL CARD POSTER</string>
<string name="settings_poster_card_width">Lățime</string>
<string name="settings_poster_custom">Personalizat</string>
<string name="settings_poster_description">Ajustează lățimea cardului și raza colțurilor.</string>
@ -641,7 +643,7 @@
<string name="settings_continue_watching_sort_mode_default_desc">Sortează toate elementele după recență</string>
<string name="settings_continue_watching_sort_mode_streaming">Stil platformă de streaming</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">Elementele lansate primele, cele viitoare la sfârșit</string>
<string name="settings_continue_watching_section_card_style">Stil card poster</string>
<string name="settings_continue_watching_section_card_style">STIL CARD POSTER</string>
<string name="settings_continue_watching_section_on_launch">LA PORNIRE</string>
<string name="settings_continue_watching_section_up_next_behavior">COMPORTAMENT EPISODUL URMĂTOR</string>
<string name="settings_continue_watching_section_visibility">VIZIBILITATE</string>
@ -665,11 +667,11 @@
<string name="settings_content_discovery_homescreen_description">Ajustează aspectul ecranului principal, vizibilitatea conținutului și comportamentul posterului</string>
<string name="settings_content_discovery_meta_screen_description">Setări pentru ecranele de detalii și de episoade.</string>
<string name="settings_content_discovery_collections_description">Creează grupări personalizate de cataloage cu dosare afișate pe ecranul principal.</string>
<string name="settings_integrations_section_title">Integrări</string>
<string name="settings_integrations_section_title">INTEGRĂRI</string>
<string name="settings_integrations_tmdb_description">Comenzi pentru îmbunătățirea metadatelor</string>
<string name="settings_integrations_mdblist_description">Furnizori externi de evaluări</string>
<string name="settings_integrations_debrid_description">Conectează conturi pentru linkuri și acces la bibliotecă</string>
<string name="settings_debrid_section_title">Servicii conectate</string>
<string name="settings_debrid_section_title">SERVICII CONECTATE</string>
<string name="settings_debrid_experimental_notice">Aceste integrări sunt experimentale și pot fi păstrate, modificate sau eliminate ulterior.</string>
<string name="settings_debrid_cloud_library">Bibliotecă în cloud</string>
<string name="settings_debrid_cloud_library_description">Răsfoiește și redă fișierele existente deja în conturile tale conectate.</string>
@ -678,7 +680,7 @@
<string name="settings_debrid_resolve_with">Rezolvă cu</string>
<string name="settings_debrid_resolve_with_description">Alege ce cont conectat se ocupă de linkurile redabile.</string>
<string name="settings_debrid_add_key_first">Conectează mai întâi un cont.</string>
<string name="settings_debrid_section_providers">Conturi</string>
<string name="settings_debrid_section_providers">CONTURI</string>
<string name="settings_debrid_provider_description">Conectează contul tău %1$s.</string>
<string name="settings_debrid_provider_device_description">Asociază contul tău %1$s în browser.</string>
<string name="settings_debrid_dialog_title">Cheie API %1$s</string>
@ -698,26 +700,26 @@
<string name="settings_debrid_device_auth_failed">Nu s-a putut iniția autentificarea.</string>
<string name="settings_debrid_device_auth_missing_configuration">Această metodă de autentificare nu este configurată în acest build.</string>
<string name="settings_debrid_device_auth_expired">Acest cod a expirat. Încearcă din nou.</string>
<string name="settings_debrid_section_instant_playback">Pregătire linkuri</string>
<string name="settings_debrid_section_instant_playback">PREGĂTIRE LINKURI</string>
<string name="settings_debrid_prepare_instant_playback">Pregătește linkurile</string>
<string name="settings_debrid_prepare_instant_playback_description">Rezolvă linkurile redabile înainte ca redarea să înceapă.</string>
<string name="settings_debrid_prepare_stream_count">Număr de linkuri de pregătit</string>
<string name="settings_debrid_prepare_stream_count_warning">Folosește un număr cât mai mic posibil. Serviciile conectate pot limita rata de rezolvare a linkurilor într-o anumită perioadă de timp. Deschiderea unui film sau episod poate conta la aceste limite chiar dacă nu apeși pe Vizionare, deoarece linkurile sunt pregătite în avans.</string>
<string name="settings_debrid_prepare_count_one">1 link</string>
<string name="settings_debrid_prepare_count_many">%1$d linkuri</string>
<string name="settings_debrid_section_formatting">Formatare</string>
<string name="settings_debrid_section_formatting">FORMATARE</string>
<string name="settings_debrid_name_template">Șablon nume</string>
<string name="settings_debrid_name_template_description">Controlează modul în care apar numele rezultatelor. Lasă gol pentru a folosi numele original al rezultatului.</string>
<string name="settings_debrid_description_template">Șablon descriere</string>
<string name="settings_debrid_description_template_description">Controlează metadatele afișate sub fiecare rezultat. Lasă gol pentru a folosi detaliile originale ale rezultatului.</string>
<string name="settings_debrid_formatter_reset_title">Resetează formatarea</string>
<string name="settings_debrid_formatter_reset_subtitle">Restaurează formatarea implicită a rezultatelor.</string>
<string name="settings_stream_badges_section">Stil Fusion</string>
<string name="settings_stream_badges_section">STIL FUSION</string>
<string name="settings_stream_size_badges_title">Insigne de dimensiune</string>
<string name="settings_stream_size_badges_description">Afișează insigne cu dimensiunea fișierului în rezultatele fluxului și în panourile cu surse ale playerului.</string>
<string name="settings_stream_addon_logo_title">Siglă extensie</string>
<string name="settings_stream_addon_logo_description">Afișează sigla și numele extensiei lângă sursele fluxului.</string>
<string name="settings_stream_display_section">Afișare</string>
<string name="settings_stream_display_section">AFIȘARE</string>
<string name="settings_stream_badge_position_title">Poziție insignă</string>
<string name="settings_stream_badge_position_description">Alege dacă insignele Fusion și de dimensiune apar deasupra sau dedesubtul cardurilor de flux.</string>
<string name="settings_stream_badge_position_dialog_title">Poziție insignă</string>
@ -752,9 +754,9 @@
<string name="settings_mdb_api_key_title">Cheie API</string>
<string name="settings_mdb_enable_ratings">Activează evaluările MDBList</string>
<string name="settings_mdb_enable_ratings_description">Preia evaluările de la furnizorii externi în ecranul cu detalii despre metadate</string>
<string name="settings_mdb_section_api_key">Cheie API</string>
<string name="settings_mdb_section_rating_providers">Furnizori externi de evaluări</string>
<string name="settings_mdb_section_title">Evaluări MDBList</string>
<string name="settings_mdb_section_api_key">CHEIE API</string>
<string name="settings_mdb_section_rating_providers">FURNIZORI EXTERNI DE EVALUĂRI</string>
<string name="settings_mdb_section_title">EVALUĂRI MDBList</string>
<string name="settings_meta_actions">Acțiuni</string>
<string name="settings_meta_actions_description">Comenzi de redare și salvare.</string>
<string name="settings_meta_cast">Distribuție</string>
@ -1050,7 +1052,7 @@
<string name="settings_tmdb_section_credentials">CREDENTIALE</string>
<string name="settings_tmdb_section_localization">LOCALIZARE</string>
<string name="settings_tmdb_section_modules">MODULE</string>
<string name="settings_tmdb_section_title">Îmbunătățire TMDB</string>
<string name="settings_tmdb_section_title">ÎMBUNĂTĂȚIRE TMDB</string>
<string name="settings_trakt_approval_redirect">După aprobare, vei fi redirecționat înapoi în mod automat.</string>
<string name="settings_trakt_authentication">AUTENTIFICARE</string>
<string name="settings_trakt_comments">Comentarii</string>
@ -1653,7 +1655,7 @@
<string name="player_ios_preset_custom_label">Personalizat</string>
<string name="player_ios_preset_custom_desc">Folosește valorile tale avansate de mai jos.</string>
<string name="player_ios_hardware_decoder_off">Dezactivat</string>
<string name="settings_playback_ios_video_output">Ieșire video iOS</string>
<string name="settings_playback_ios_video_output">IEȘIRE VIDEO iOS</string>
<string name="settings_playback_ios_hardware_decoder">Decodor hardware</string>
<string name="settings_playback_ios_extended_dynamic_range">Interval dinamic extins (EDR)</string>
<string name="settings_playback_ios_extended_dynamic_range_desc">Modul de ieșire Metal implicit pentru noile sesiuni de redare.</string>
@ -1661,12 +1663,12 @@
<string name="settings_playback_ios_display_color_hint_desc">Permite mpv să vizeze spațiul de culoare activ al ecranului în mod implicit.</string>
<string name="settings_playback_ios_target_primaries">Culori primare țintă</string>
<string name="settings_playback_ios_target_transfer">Funcție de transfer țintă</string>
<string name="settings_playback_ios_audio_output_section">Ieșire audio iOS</string>
<string name="settings_playback_ios_audio_output_section">IEȘIRE AUDIO iOS</string>
<string name="settings_playback_ios_audio_output">Ieșire audio</string>
<string name="settings_playback_ios_audio_output_auto_desc">Se folosește AudioUnit în timp ce ieșirea AVFoundation este dezactivată temporar.</string>
<string name="settings_playback_ios_audio_output_avfoundation_desc">Suport experimental pentru Spatial Audio și ieșire multicanal.</string>
<string name="settings_playback_ios_audio_output_audiounit_desc">Folosește ieșirea moștenită (legacy) AudioUnit.</string>
<string name="settings_debrid_section_result_management">Gestionare rezultate</string>
<string name="settings_debrid_section_result_management">GESTIONARE REZULTATE</string>
<string name="settings_debrid_max_results">Număr maxim de rezultate</string>
<string name="settings_debrid_max_results_desc">Limitează numărul de rezultate afișate.</string>
<string name="settings_debrid_sort_results">Ordonează rezultatele</string>
@ -1911,6 +1913,7 @@
<string name="p2p_consent_cancel">Anulează</string>
<string name="p2p_error_unknown">Eroare torrent necunoscută</string>
<string name="settings_p2p_title">Streaming P2P</string>
<string name="settings_playback_section_p2p">STREAMING P2P</string>
<string name="settings_p2p_subtitle">Permite fluxurile de tip peer-to-peer (torrent)</string>
<string name="settings_p2p_hide_stats_title">Ascunde statisticile torrentului</string>
<string name="settings_p2p_hide_stats_subtitle">Ascunde datele despre buffer, seeders, peers și viteza de descărcare în timpul încărcării și redării</string>

View file

@ -416,12 +416,14 @@
<string name="compose_settings_page_licenses_attributions">Licencie a atribúcie</string>
<string name="compose_settings_page_mdblist_ratings">Hodnotenia MDBList</string>
<string name="compose_settings_page_meta_screen">Stránka s podrobnosťami</string>
<string name="settings_appearance_section_detail_page">STRÁNKA S PODROBNOSŤAMI</string>
<string name="compose_settings_page_notifications">Oznámenia</string>
<string name="compose_settings_page_playback">Prehrávanie</string>
<string name="compose_settings_page_plugins">Pluginy</string>
<string name="compose_settings_page_poster_customization">Štýl karty plagátu</string>
<string name="compose_settings_page_root">Nastavenia</string>
<string name="compose_settings_page_streams">Streamy</string>
<string name="settings_appearance_section_streams">STREAMY</string>
<string name="compose_settings_page_supporters_contributors">Podporovatelia a prispievatelia</string>
<string name="compose_settings_page_tmdb_enrichment">Obohatenie TMDB</string>
<string name="compose_settings_page_trakt">Trakt</string>
@ -562,8 +564,8 @@
<string name="settings_homescreen_section_catalogs">KATALÓGY</string>
<string name="settings_homescreen_section_catalogs_collections">KATALÓGY A KOLEKCIE</string>
<string name="settings_homescreen_section_collections">KOLEKCIE</string>
<string name="settings_homescreen_section_hero">Rozloženie domovskej obrazovky</string>
<string name="settings_homescreen_section_hero_sources">Katalógy hero sekcie</string>
<string name="settings_homescreen_section_hero">ROZLOŽENIE DOMOVSKEJ OBRAZOVKY</string>
<string name="settings_homescreen_section_hero_sources">KATALÓGY HERO SEKCIE</string>
<string name="settings_homescreen_selected_count">%1$d z %2$d vybraných</string>
<string name="settings_homescreen_show_hero">Zobraziť hero sekciu</string>
<string name="settings_homescreen_show_hero_description">Zobraziť hero karusel v hornej časti domovskej obrazovky.</string>
@ -577,7 +579,7 @@
<string name="settings_hide_secret">Skryť hodnotu</string>
<string name="settings_playback_subtitle">Prehrávač, titulky a automatické prehrávanie</string>
<string name="settings_poster_card_radius">Polomer rohov</string>
<string name="settings_poster_card_style">Štýl karty plagátu</string>
<string name="settings_poster_card_style">ŠTÝL KARTY PLAGÁTU</string>
<string name="settings_poster_card_width">Šírka</string>
<string name="settings_poster_custom">Vlastné</string>
<string name="settings_poster_description">Nastaviť šírku karty a polomer rohov.</string>
@ -612,7 +614,7 @@
<string name="settings_continue_watching_sort_mode_default_desc">Zoradiť všetky položky podľa aktuálnosti</string>
<string name="settings_continue_watching_sort_mode_streaming">Štýl streamovania</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">Vydané položky ako prvé, nadchádzajúce na konci</string>
<string name="settings_continue_watching_section_card_style">Štýl karty plagátu</string>
<string name="settings_continue_watching_section_card_style">ŠTÝL KARTY PLAGÁTU</string>
<string name="settings_continue_watching_section_on_launch">PRI SPUSTENÍ</string>
<string name="settings_continue_watching_section_up_next_behavior">SPRÁVANIE ĎALŠEJ EPIZÓDY</string>
<string name="settings_continue_watching_section_visibility">VIDITEĽNOSŤ</string>
@ -635,11 +637,11 @@
<string name="settings_content_discovery_homescreen_description">Upraviť rozloženie domovskej obrazovky, viditeľnosť obsahu a správanie plagátov</string>
<string name="settings_content_discovery_meta_screen_description">Nastavenia pre obrazovky s podrobnosťami a epizódami.</string>
<string name="settings_content_discovery_collections_description">Vytvoriť vlastné skupiny katalógov s priečinkami zobrazenými na domovskej obrazovke.</string>
<string name="settings_integrations_section_title">Integrácie</string>
<string name="settings_integrations_section_title">INTEGRÁCIE</string>
<string name="settings_integrations_tmdb_description">Ovládacie prvky obohatenia metadát</string>
<string name="settings_integrations_mdblist_description">Poskytovatelia externých hodnotení</string>
<string name="settings_integrations_debrid_description">Pripojiť účty pre odkazy a prístup ku knižnici</string>
<string name="settings_debrid_section_title">Prepojené služby</string>
<string name="settings_debrid_section_title">PREPOJENÉ SLUŽBY</string>
<string name="settings_debrid_experimental_notice">Tieto integrácie sú experimentálne a môžu byť zachované, zmenené alebo neskôr odstránené.</string>
<string name="settings_debrid_cloud_library">Cloudová knižnica</string>
<string name="settings_debrid_cloud_library_description">Prehliadať a prehrávať súbory, ktoré sú už vo vašich prepojených účtoch.</string>
@ -648,7 +650,7 @@
<string name="settings_debrid_resolve_with">Riešiť pomocou</string>
<string name="settings_debrid_resolve_with_description">Vyberte, ktorý prepojený účet spracováva prehrávateľné odkazy.</string>
<string name="settings_debrid_add_key_first">Najprv pripojte účet.</string>
<string name="settings_debrid_section_providers">Účty</string>
<string name="settings_debrid_section_providers">ÚČTY</string>
<string name="settings_debrid_provider_description">Pripojiť váš účet %1$s.</string>
<string name="settings_debrid_provider_device_description">Prepojiť váš účet %1$s v prehliadači.</string>
<string name="settings_debrid_dialog_title">API kľúč %1$s</string>
@ -668,26 +670,26 @@
<string name="settings_debrid_device_auth_failed">Nepodarilo sa spustiť prihlásenie.</string>
<string name="settings_debrid_device_auth_missing_configuration">Táto metóda prihlásenia nie je nakonfigurovaná v tejto zostave.</string>
<string name="settings_debrid_device_auth_expired">Platnosť tohto kódu vypršala. Skúste znova.</string>
<string name="settings_debrid_section_instant_playback">Príprava odkazov</string>
<string name="settings_debrid_section_instant_playback">PRÍPRAVA ODKAZOV</string>
<string name="settings_debrid_prepare_instant_playback">Pripraviť odkazy</string>
<string name="settings_debrid_prepare_instant_playback_description">Riešiť prehrávateľné odkazy pred začatím prehrávania.</string>
<string name="settings_debrid_prepare_stream_count">Počet odkazov na prípravu</string>
<string name="settings_debrid_prepare_stream_count_warning">Ak je to možné, použite nižší počet. Prepojené služby môžu obmedzovať počet odkazov, ktoré je možné vyriešiť za určité časové obdobie. Otvorenie filmu alebo epizódy sa môže zarátať do týchto limitov, aj keď nestlačíte Sledovať, pretože sa odkazy pripravujú vopred.</string>
<string name="settings_debrid_prepare_count_one">1 odkaz</string>
<string name="settings_debrid_prepare_count_many">%1$d odkazov</string>
<string name="settings_debrid_section_formatting">Formátovanie</string>
<string name="settings_debrid_section_formatting">FORMÁTOVANIE</string>
<string name="settings_debrid_name_template">Šablóna názvu</string>
<string name="settings_debrid_name_template_description">Ovláda, ako sa zobrazujú názvy výsledkov. Nechajte prázdne, aby sa použil pôvodný názov výsledku.</string>
<string name="settings_debrid_description_template">Šablóna popisu</string>
<string name="settings_debrid_description_template_description">Ovláda metadáta zobrazené pod každým výsledkom. Nechajte prázdne, aby sa použili pôvodné podrobnosti výsledku.</string>
<string name="settings_debrid_formatter_reset_title">Obnoviť formátovanie</string>
<string name="settings_debrid_formatter_reset_subtitle">Obnoviť predvolené formátovanie výsledkov.</string>
<string name="settings_stream_badges_section">Štýl Fusion</string>
<string name="settings_stream_badges_section">ŠTÝL FUSION</string>
<string name="settings_stream_size_badges_title">Odznaky veľkosti</string>
<string name="settings_stream_size_badges_description">Zobraziť odznaky veľkosti súboru vo výsledkoch streamov a paneloch zdrojov prehrávača.</string>
<string name="settings_stream_addon_logo_title">Logo doplnku</string>
<string name="settings_stream_addon_logo_description">Zobraziť logo a názov doplnku vedľa zdrojov streamov.</string>
<string name="settings_stream_display_section">Zobrazenie</string>
<string name="settings_stream_display_section">ZOBRAZENIE</string>
<string name="settings_stream_badge_position_title">Poloha odznaku</string>
<string name="settings_stream_badge_position_description">Vyberte, či sa odznaky Fusion a veľkosti zobrazujú nad alebo pod kartami streamov.</string>
<string name="settings_stream_badge_position_dialog_title">Poloha odznaku</string>
@ -722,9 +724,9 @@
<string name="settings_mdb_api_key_title">API kľúč</string>
<string name="settings_mdb_enable_ratings">Povoliť hodnotenia MDBList</string>
<string name="settings_mdb_enable_ratings_description">Načítať hodnotenia od externých poskytovateľov na obrazovke s podrobnosťami metadát</string>
<string name="settings_mdb_section_api_key">API kľúč</string>
<string name="settings_mdb_section_rating_providers">Poskytovatelia externých hodnotení</string>
<string name="settings_mdb_section_title">Hodnotenia MDBList</string>
<string name="settings_mdb_section_api_key">API KĽÚČ</string>
<string name="settings_mdb_section_rating_providers">POSKYTOVATELIA EXTERNÝCH HODNOTENÍ</string>
<string name="settings_mdb_section_title">HODNOTENIA MDBList</string>
<string name="settings_meta_actions">Akcie</string>
<string name="settings_meta_actions_description">Ovládacie prvky prehrávania a ukladania.</string>
<string name="settings_meta_cast">Obsadenie</string>
@ -998,7 +1000,7 @@
<string name="settings_tmdb_section_credentials">PRIHLASOVACIE ÚDAJE</string>
<string name="settings_tmdb_section_localization">LOKALIZÁCIA</string>
<string name="settings_tmdb_section_modules">MODULY</string>
<string name="settings_tmdb_section_title">Obohatenie TMDB</string>
<string name="settings_tmdb_section_title">OBOHATENIE TMDB</string>
<string name="settings_trakt_approval_redirect">Po schválení budete automaticky presmerovaný späť.</string>
<string name="settings_trakt_authentication">AUTENTIFIKÁCIA</string>
<string name="settings_trakt_comments">Komentáre</string>
@ -1604,7 +1606,7 @@
<string name="player_ios_preset_custom_desc">Použiť vaše pokročilé hodnoty nižšie.</string>
<string name="player_ios_hardware_decoder_off">Vypnuté</string>
<!-- Playback settings iOS video output -->
<string name="settings_playback_ios_video_output">Výstup videa iOS</string>
<string name="settings_playback_ios_video_output">VÝSTUP VIDEA iOS</string>
<string name="settings_playback_ios_hardware_decoder">Hardvérový dekodér</string>
<string name="settings_playback_ios_extended_dynamic_range">Rozšírený dynamický rozsah</string>
<string name="settings_playback_ios_extended_dynamic_range_desc">Predvolený režim výstupu Metal pre nové relácie prehrávania.</string>
@ -1613,13 +1615,13 @@
<string name="settings_playback_ios_target_primaries">Cieľové primárne farby</string>
<string name="settings_playback_ios_target_transfer">Cieľový prenos</string>
<!-- Playback settings iOS audio output -->
<string name="settings_playback_ios_audio_output_section">Výstup zvuku iOS</string>
<string name="settings_playback_ios_audio_output_section">VÝSTUP ZVUKU iOS</string>
<string name="settings_playback_ios_audio_output">Výstup zvuku</string>
<string name="settings_playback_ios_audio_output_auto_desc">Najprv vyskúšať AVFoundation, potom zálohovať na AudioUnit.</string>
<string name="settings_playback_ios_audio_output_avfoundation_desc">Experimentálna podpora priestorového zvuku a viackanálového výstupu.</string>
<string name="settings_playback_ios_audio_output_audiounit_desc">Použiť starší výstup AudioUnit.</string>
<!-- Debrid Result Management section -->
<string name="settings_debrid_section_result_management">Správa výsledkov</string>
<string name="settings_debrid_section_result_management">SPRÁVA VÝSLEDKOV</string>
<string name="settings_debrid_max_results">Maximálny počet výsledkov</string>
<string name="settings_debrid_max_results_desc">Obmedziť počet zobrazených výsledkov.</string>
<string name="settings_debrid_sort_results">Zoradiť výsledky</string>
@ -1887,6 +1889,7 @@
<string name="p2p_consent_cancel">Zrušiť</string>
<string name="p2p_error_unknown">Neznáma chyba torrentu</string>
<string name="settings_p2p_title">P2P Streamovanie</string>
<string name="settings_playback_section_p2p">P2P STREAMOVANIE</string>
<string name="settings_p2p_subtitle">Povoliť peer-to-peer (torrent) streamy</string>
<string name="settings_p2p_hide_stats_title">Skryť štatistiky torrentu</string>
<string name="settings_p2p_hide_stats_subtitle">Skryť buffer, seedy, peery a rýchlosť sťahovania počas načítavania a prehrávania</string>

View file

@ -394,6 +394,7 @@
<string name="compose_settings_page_licenses_attributions">Lisanslar &amp; Teşekkürler</string>
<string name="compose_settings_page_mdblist_ratings">MDBList puanları</string>
<string name="compose_settings_page_meta_screen">Detay ekranı</string>
<string name="settings_appearance_section_detail_page">DETAY EKRANI</string>
<string name="compose_settings_page_notifications">Bildirimler</string>
<string name="compose_settings_page_playback">Oynatma</string>
<string name="compose_settings_page_plugins">Pluginler</string>
@ -605,7 +606,7 @@
<string name="settings_integrations_tmdb_description">Detay sayfalarını TMDB görselleri, ekip bilgileri, bölüm meta verileri ve daha fazlasıyla zenginleştir.</string>
<string name="settings_integrations_mdblist_description">Detay sayfalarına IMDb, Rotten Tomatoes, Metacritic ve diğer dış puanları ekle.</string>
<string name="settings_integrations_debrid_description">Bağlantı ve kitaplık erişimi için hesapları bağla</string>
<string name="settings_debrid_section_title">Bağlı Servisler</string>
<string name="settings_debrid_section_title">BAĞLI SERVİSLER</string>
<string name="settings_debrid_experimental_notice">Bu entegrasyonlar deneyseldir; gelecekte tutulabilir, değiştirilebilir veya tamamen kaldırılabilir.</string>
<string name="settings_debrid_cloud_library">Bulut kitaplığı</string>
<string name="settings_debrid_cloud_library_description">Bağlı hesaplarında zaten bulunan dosyaları gözden geçir ve oynat.</string>
@ -614,7 +615,7 @@
<string name="settings_debrid_resolve_with">Şununla çöz</string>
<string name="settings_debrid_resolve_with_description">Oynatılabilir bağlantıları hangi bağlı hesabın işleyeceğini seç.</string>
<string name="settings_debrid_add_key_first">Önce bir hesap bağla.</string>
<string name="settings_debrid_section_providers">Hesaplar</string>
<string name="settings_debrid_section_providers">HESAPLAR</string>
<string name="settings_debrid_provider_description">%1$s hesabını bağla.</string>
<string name="settings_debrid_provider_device_description">%1$s hesabını tarayıcıda eşleştir.</string>
<string name="settings_debrid_dialog_title">%1$s API Anahtarı</string>
@ -634,14 +635,14 @@
<string name="settings_debrid_device_auth_failed">Giriş başlatılamadı.</string>
<string name="settings_debrid_device_auth_missing_configuration">Bu giriş yöntemi bu sürümde yapılandırılmamış.</string>
<string name="settings_debrid_device_auth_expired">Kodun süresi doldu. Tekrar dene.</string>
<string name="settings_debrid_section_instant_playback">Bağlantı Hazırlığı</string>
<string name="settings_debrid_section_instant_playback">BAĞLANTI HAZIRLIĞI</string>
<string name="settings_debrid_prepare_instant_playback">Bağlantıları hazırla</string>
<string name="settings_debrid_prepare_instant_playback_description">Oynatma başlamadan önce oynatılabilir bağlantıları çöz.</string>
<string name="settings_debrid_prepare_stream_count">Hazırlanacak bağlantı sayısı</string>
<string name="settings_debrid_prepare_stream_count_warning">Mümkünse daha düşük bir sayı seç. Bağlı servisler, belirli bir sürede çözülebilecek bağlantı sayısını sınırlayabilir. Bir filmi veya bölümü açmak, İzle butonuna basmasan bile limitlerini etkileyebilir, çünkü bağlantılar önceden hazırlanır.</string>
<string name="settings_debrid_prepare_count_one">1 bağlantı</string>
<string name="settings_debrid_prepare_count_many">%1$d bağlantı</string>
<string name="settings_debrid_section_formatting">Biçimlendirme</string>
<string name="settings_debrid_section_formatting">BİÇİMLENDİRME</string>
<string name="settings_debrid_name_template">Ad şablonu</string>
<string name="settings_debrid_name_template_description">Sonuç adlarının nasıl görüneceğini belirler.</string>
<string name="settings_debrid_description_template">ıklama şablonu</string>

View file

@ -425,12 +425,14 @@
<string name="compose_settings_page_licenses_attributions">Giấy phép &amp; Ghi nhận</string>
<string name="compose_settings_page_mdblist_ratings">Đánh giá MDBList</string>
<string name="compose_settings_page_meta_screen">Trang chi tiết</string>
<string name="settings_appearance_section_detail_page">TRANG CHI TIẾT</string>
<string name="compose_settings_page_notifications">Thông báo</string>
<string name="compose_settings_page_playback">Phát</string>
<string name="compose_settings_page_plugins">Plugin</string>
<string name="compose_settings_page_poster_customization">Kiểu poster</string>
<string name="compose_settings_page_root">Cài đặt</string>
<string name="compose_settings_page_streams">Nguồn phát</string>
<string name="settings_appearance_section_streams">NGUỒN PHÁT</string>
<string name="compose_settings_page_supporters_contributors">Nhà tài trợ &amp; Cộng tác viên</string>
<string name="compose_settings_page_tmdb_enrichment">Bổ sung dữ liệu TMDB</string>
<string name="compose_settings_page_trakt">Trakt</string>
@ -644,11 +646,11 @@
<string name="settings_content_discovery_homescreen_description">Tùy chỉnh bố cục Trang chủ và cách hiển thị nội dung</string>
<string name="settings_content_discovery_meta_screen_description">Thiết lập trang chi tiết và danh sách tập</string>
<string name="settings_content_discovery_collections_description">Tạo bộ sưu tập tùy chỉnh bằng thư mục trên Trang chủ</string>
<string name="settings_integrations_section_title">Tích hợp</string>
<string name="settings_integrations_section_title">TÍCH HỢP</string>
<string name="settings_integrations_tmdb_description">Thiết lập dữ liệu TMDB</string>
<string name="settings_integrations_mdblist_description">Dịch vụ điểm đánh giá bên ngoài</string>
<string name="settings_integrations_debrid_description">Kết nối tài khoản để truy cập nguồn và thư viện đám mây</string>
<string name="settings_debrid_section_title">Dịch vụ kết nối</string>
<string name="settings_debrid_section_title">DỊCH VỤ KẾT NỐI</string>
<string name="settings_debrid_experimental_notice">Tính năng này đang được thử nghiệm, có thể thay đổi hoặc gỡ bỏ trong tương lai</string>
<string name="settings_debrid_cloud_library">Thư viện đám mây</string>
<string name="settings_debrid_cloud_library_description">Duyệt và phát tệp trong tài khoản đã kết nối</string>
@ -657,7 +659,7 @@
<string name="settings_debrid_resolve_with">Sử dụng dịch vụ</string>
<string name="settings_debrid_resolve_with_description">Chọn dịch vụ dùng để tạo liên kết phát</string>
<string name="settings_debrid_add_key_first">Hãy kết nối tài khoản trước</string>
<string name="settings_debrid_section_providers">Tài khoản</string>
<string name="settings_debrid_section_providers">TÀI KHOẢN</string>
<string name="settings_debrid_provider_description">Kết nối tài khoản %1$s</string>
<string name="settings_debrid_provider_device_description">Liên kết tài khoản %1$s bằng trình duyệt</string>
<string name="settings_debrid_dialog_title">Khóa API %1$s</string>
@ -677,14 +679,14 @@
<string name="settings_debrid_device_auth_failed">Đăng nhập thất bại</string>
<string name="settings_debrid_device_auth_missing_configuration">Phương thức đăng nhập này chưa được hỗ trợ trong phiên bản hiện tại</string>
<string name="settings_debrid_device_auth_expired">Mã đã hết hạn, vui lòng thử lại</string>
<string name="settings_debrid_section_instant_playback">Chuẩn bị liên kết phát</string>
<string name="settings_debrid_section_instant_playback">CHUẨN BỊ LIÊN KẾT PHÁT</string>
<string name="settings_debrid_prepare_instant_playback">Chuẩn bị liên kết phát</string>
<string name="settings_debrid_prepare_instant_playback_description">Xác thực liên kết trước khi bắt đầu</string>
<string name="settings_debrid_prepare_stream_count">Liên kết cần xử lý</string>
<string name="settings_debrid_prepare_stream_count_warning">Hãy đặt số lượng thấp nhất có thể. Các dịch vụ kết nối có thể giới hạn số lượng liên kết được xác thực trong một khoảng thời gian. Việc mở một bộ phim hoặc tập phim có thể được tính vào hạn mức này ngay cả khi bạn không nhấn "Xem", vì các liên kết đã được chuẩn bị sẵn từ trước</string>
<string name="settings_debrid_prepare_count_one">1 liên kết</string>
<string name="settings_debrid_prepare_count_many">%1$d liên kết</string>
<string name="settings_debrid_section_formatting">Định dạng hiển thị</string>
<string name="settings_debrid_section_formatting">ĐỊNH DẠNG HIỂN THỊ</string>
<string name="settings_debrid_name_template">Mẫu tên</string>
<string name="settings_debrid_name_template_description">Tùy chỉnh tên hiển thị. Bỏ trống nếu dùng tên gốc</string>
<string name="settings_debrid_description_template">Mẫu mô tả</string>
@ -731,9 +733,9 @@
<string name="settings_mdb_api_key_title">Khóa API</string>
<string name="settings_mdb_enable_ratings">Hiển thị đánh giá MDBList</string>
<string name="settings_mdb_enable_ratings_description">Hiển thị điểm đánh giá từ các nguồn bên ngoài trên trang chi tiết</string>
<string name="settings_mdb_section_api_key">Khóa API</string>
<string name="settings_mdb_section_rating_providers">Nguồn điểm đánh giá</string>
<string name="settings_mdb_section_title">Đánh giá MDBList</string>
<string name="settings_mdb_section_api_key">KHÓA API</string>
<string name="settings_mdb_section_rating_providers">NGUỒN ĐIỂM ĐÁNH GIÁ</string>
<string name="settings_mdb_section_title">ĐÁNH GIÁ MDBList</string>
<string name="settings_meta_actions">Thao tác</string>
<string name="settings_meta_actions_description">Phát và lưu nội dung</string>
<string name="settings_meta_cast">Diễn viên</string>
@ -1031,7 +1033,7 @@
<string name="settings_tmdb_section_credentials">THÔNG TIN XÁC THỰC</string>
<string name="settings_tmdb_section_localization">BẢN ĐỊA HÓA</string>
<string name="settings_tmdb_section_modules">MÔ-ĐUN</string>
<string name="settings_tmdb_section_title">Thông tin từ TMDB</string>
<string name="settings_tmdb_section_title">THÔNG TIN TỪ TMDB</string>
<string name="settings_trakt_approval_redirect">Bạn sẽ được tự động chuyển hướng sau khi yêu cầu được duyệt</string>
<string name="settings_trakt_authentication">XÁC THỰC</string>
<string name="settings_trakt_comments">Bình luận</string>
@ -1632,7 +1634,7 @@
<string name="player_ios_preset_custom_label">Tùy chỉnh</string>
<string name="player_ios_preset_custom_desc">Sử dụng các thiết lập nâng cao bên dưới</string>
<string name="player_ios_hardware_decoder_off">Tắt</string>
<string name="settings_playback_ios_video_output">Đầu ra video trên iOS</string>
<string name="settings_playback_ios_video_output">ĐẦU RA VIDEO TRÊN iOS</string>
<string name="settings_playback_ios_hardware_decoder">Decoder phần cứng</string>
<string name="settings_playback_ios_extended_dynamic_range">Dải tương phản động mở rộng</string>
<string name="settings_playback_ios_extended_dynamic_range_desc">Cài đặt Metal mặc định khi bắt đầu phát video</string>
@ -1640,12 +1642,12 @@
<string name="settings_playback_ios_display_color_hint_desc">Đồng bộ không gian màu với màn hình</string>
<string name="settings_playback_ios_target_primaries">Không gian màu mục tiêu</string>
<string name="settings_playback_ios_target_transfer">Cấu hình truyền tải</string>
<string name="settings_playback_ios_audio_output_section">Đầu ra âm thanh trên iOS</string>
<string name="settings_playback_ios_audio_output_section">ĐẦU RA ÂM THANH TRÊN iOS</string>
<string name="settings_playback_ios_audio_output">Đầu ra âm thanh</string>
<string name="settings_playback_ios_audio_output_auto_desc">Dùng AudioUnit khi đầu ra AVFoundation tạm thời bị vô hiệu hóa</string>
<string name="settings_playback_ios_audio_output_avfoundation_desc">Thử nghiệm: Spatial Audio và Âm thanh đa kênh</string>
<string name="settings_playback_ios_audio_output_audiounit_desc">Dùng AudioUnit truyền thống</string>
<string name="settings_debrid_section_result_management">Quản lý kết quả</string>
<string name="settings_debrid_section_result_management">QUẢN LÝ KẾT QUẢ</string>
<string name="settings_debrid_max_results">Số kết quả tối đa</string>
<string name="settings_debrid_max_results_desc">Giới hạn số kết quả hiển thị</string>
<string name="settings_debrid_sort_results">Sắp xếp kết quả</string>
@ -1890,6 +1892,7 @@
<string name="p2p_consent_cancel">Hủy</string>
<string name="p2p_error_unknown">Lỗi torrent không xác định</string>
<string name="settings_p2p_title">Phát qua P2P</string>
<string name="settings_playback_section_p2p">PHÁT QUA P2P</string>
<string name="settings_p2p_subtitle">Cho phép phát từ nguồn torrent (P2P)</string>
<string name="settings_p2p_hide_stats_title">Ẩn thống kê torrent</string>
<string name="settings_p2p_hide_stats_subtitle">Ẩn bộ đệm, số lượng seed, peer và tốc độ tải trong khi tải và phát</string>

View file

@ -427,12 +427,14 @@
<string name="compose_settings_page_licenses_attributions">Licenses &amp; Attribution</string>
<string name="compose_settings_page_mdblist_ratings">MDBList Ratings</string>
<string name="compose_settings_page_meta_screen">Detail Page</string>
<string name="settings_appearance_section_detail_page">DETAIL PAGE</string>
<string name="compose_settings_page_notifications">Notifications</string>
<string name="compose_settings_page_playback">Playback</string>
<string name="compose_settings_page_plugins">Plugins</string>
<string name="compose_settings_page_poster_customization">Poster Card Style</string>
<string name="compose_settings_page_root">Settings</string>
<string name="compose_settings_page_streams">Streams</string>
<string name="settings_appearance_section_streams">STREAMS</string>
<string name="compose_settings_page_supporters_contributors">Supporters &amp; Contributors</string>
<string name="compose_settings_page_tmdb_enrichment">TMDB Enrichment</string>
<string name="compose_settings_page_trakt">Trakt</string>
@ -598,8 +600,8 @@
<string name="settings_homescreen_section_catalogs">CATALOGS</string>
<string name="settings_homescreen_section_catalogs_collections">CATALOGS &amp; COLLECTIONS</string>
<string name="settings_homescreen_section_collections">COLLECTIONS</string>
<string name="settings_homescreen_section_hero">Home Layout</string>
<string name="settings_homescreen_section_hero_sources">Hero Catalogs</string>
<string name="settings_homescreen_section_hero">HOME LAYOUT</string>
<string name="settings_homescreen_section_hero_sources">HERO CATALOGS</string>
<string name="settings_homescreen_selected_count">%1$d of %2$d selected</string>
<string name="settings_homescreen_show_hero">Show Hero Section</string>
<string name="settings_homescreen_show_hero_description">Display hero carousel at top of home.</string>
@ -614,7 +616,7 @@
<string name="settings_homescreen_visible">Visible</string>
<string name="settings_hide_secret">Hide value</string>
<string name="settings_playback_subtitle">Player, subtitles, and auto-play</string>
<string name="settings_card_depth_title">Card Depth Effect</string>
<string name="settings_card_depth_title">CARD DEPTH EFFECT</string>
<string name="settings_card_depth_description">Adds a lit top edge and a soft sheen to image cards for a subtle sense of depth.</string>
<string name="settings_card_depth_enabled">Enable depth effect</string>
<string name="settings_card_depth_edge">Edge glow</string>
@ -646,7 +648,7 @@
<string name="settings_card_depth_surface_cast">Cast</string>
<string name="settings_card_depth_surface_trailers">Trailers</string>
<string name="settings_poster_card_radius">Corner Radius</string>
<string name="settings_poster_card_style">Poster Card Style</string>
<string name="settings_poster_card_style">POSTER CARD STYLE</string>
<string name="settings_poster_card_width">Width</string>
<string name="settings_poster_custom">Custom</string>
<string name="settings_poster_description">Tune card width and corner radius.</string>
@ -681,7 +683,10 @@
<string name="settings_continue_watching_sort_mode_default_desc">Sort all items by recency</string>
<string name="settings_continue_watching_sort_mode_streaming">Streaming Style</string>
<string name="settings_continue_watching_sort_mode_streaming_desc">Released items first, upcoming at the end</string>
<string name="settings_continue_watching_section_card_style">Poster Card Style</string>
<string name="settings_continue_watching_sort_mode_split_upcoming">Separate Upcoming Row</string>
<string name="settings_continue_watching_sort_mode_split_upcoming_desc">Move unaired episodes to a separate Upcoming row</string>
<string name="upcoming_section_title">Upcoming</string>
<string name="settings_continue_watching_section_card_style">POSTER CARD STYLE</string>
<string name="settings_continue_watching_section_on_launch">ON LAUNCH</string>
<string name="settings_continue_watching_section_up_next_behavior">NEXT UP BEHAVIOR</string>
<string name="settings_continue_watching_section_visibility">VISIBILITY</string>
@ -705,11 +710,11 @@
<string name="settings_content_discovery_homescreen_description">Adjust home layout, content visibility, and poster behavior</string>
<string name="settings_content_discovery_meta_screen_description">Settings for the detail and episode screens.</string>
<string name="settings_content_discovery_collections_description">Create custom catalog groupings with folders shown on Home.</string>
<string name="settings_integrations_section_title">Integrations</string>
<string name="settings_integrations_section_title">INTEGRATIONS</string>
<string name="settings_integrations_tmdb_description">Metadata enrichment controls</string>
<string name="settings_integrations_mdblist_description">External ratings providers</string>
<string name="settings_integrations_debrid_description">Connect accounts for links and library access</string>
<string name="settings_debrid_section_title">Connected Services</string>
<string name="settings_debrid_section_title">CONNECTED SERVICES</string>
<string name="settings_debrid_experimental_notice">These integrations are experimental and may be kept, changed, or removed later.</string>
<string name="settings_debrid_cloud_library">Cloud library</string>
<string name="settings_debrid_cloud_library_description">Browse and play files already in your connected accounts.</string>
@ -718,7 +723,7 @@
<string name="settings_debrid_resolve_with">Resolve with</string>
<string name="settings_debrid_resolve_with_description">Choose which connected account handles playable links.</string>
<string name="settings_debrid_add_key_first">Connect an account first.</string>
<string name="settings_debrid_section_providers">Accounts</string>
<string name="settings_debrid_section_providers">ACCOUNTS</string>
<string name="settings_debrid_provider_description">Connect your %1$s account.</string>
<string name="settings_debrid_provider_device_description">Link your %1$s account in the browser.</string>
<string name="settings_debrid_dialog_title">%1$s API Key</string>
@ -738,26 +743,26 @@
<string name="settings_debrid_device_auth_failed">Could not start sign-in.</string>
<string name="settings_debrid_device_auth_missing_configuration">This sign-in method is not configured in this build.</string>
<string name="settings_debrid_device_auth_expired">This code expired. Try again.</string>
<string name="settings_debrid_section_instant_playback">Link Preparation</string>
<string name="settings_debrid_section_instant_playback">LINK PREPARATION</string>
<string name="settings_debrid_prepare_instant_playback">Prepare links</string>
<string name="settings_debrid_prepare_instant_playback_description">Resolve playable links before playback starts.</string>
<string name="settings_debrid_prepare_stream_count">Links to prepare</string>
<string name="settings_debrid_prepare_stream_count_warning">Use a lower count when possible. Connected services may rate-limit how many links can be resolved in a time period. Opening a movie or episode can count toward those limits even if you do not press Watch, because the links are prepared ahead of time.</string>
<string name="settings_debrid_prepare_count_one">1 link</string>
<string name="settings_debrid_prepare_count_many">%1$d links</string>
<string name="settings_debrid_section_formatting">Formatting</string>
<string name="settings_debrid_section_formatting">FORMATTING</string>
<string name="settings_debrid_name_template">Name template</string>
<string name="settings_debrid_name_template_description">Controls how result names appear. Leave blank to use the original result name.</string>
<string name="settings_debrid_description_template">Description template</string>
<string name="settings_debrid_description_template_description">Controls the metadata shown under each result. Leave blank to use the original result details.</string>
<string name="settings_debrid_formatter_reset_title">Reset formatting</string>
<string name="settings_debrid_formatter_reset_subtitle">Restore default result formatting.</string>
<string name="settings_stream_badges_section">Fusion Style</string>
<string name="settings_stream_badges_section">FUSION STYLE</string>
<string name="settings_stream_size_badges_title">Size badges</string>
<string name="settings_stream_size_badges_description">Show file size badges in stream results and player source panels.</string>
<string name="settings_stream_addon_logo_title">Addon logo</string>
<string name="settings_stream_addon_logo_description">Show addon logo and name next to stream sources.</string>
<string name="settings_stream_display_section">Display</string>
<string name="settings_stream_display_section">DISPLAY</string>
<string name="settings_stream_badge_position_title">Badge position</string>
<string name="settings_stream_badge_position_description">Choose whether Fusion and size badges appear above or below stream cards.</string>
<string name="settings_stream_badge_position_dialog_title">Badge position</string>
@ -792,9 +797,9 @@
<string name="settings_mdb_api_key_title">API Key</string>
<string name="settings_mdb_enable_ratings">Enable MDBList Ratings</string>
<string name="settings_mdb_enable_ratings_description">Fetch ratings from external providers in metadata detail screen</string>
<string name="settings_mdb_section_api_key">API Key</string>
<string name="settings_mdb_section_rating_providers">External ratings providers</string>
<string name="settings_mdb_section_title">MDBList Ratings</string>
<string name="settings_mdb_section_api_key">API KEY</string>
<string name="settings_mdb_section_rating_providers">EXTERNAL RATINGS PROVIDERS</string>
<string name="settings_mdb_section_title">MDBList RATINGS</string>
<string name="settings_meta_actions">Actions</string>
<string name="settings_meta_actions_description">Play and save controls.</string>
<string name="settings_meta_cast">Cast</string>
@ -1094,7 +1099,7 @@
<string name="settings_tmdb_section_credentials">CREDENTIALS</string>
<string name="settings_tmdb_section_localization">LOCALIZATION</string>
<string name="settings_tmdb_section_modules">MODULES</string>
<string name="settings_tmdb_section_title">TMDB Enrichment</string>
<string name="settings_tmdb_section_title">TMDB ENRICHMENT</string>
<string name="settings_trakt_approval_redirect">After approval, you will be redirected back automatically.</string>
<string name="settings_trakt_authentication">AUTHENTICATION</string>
<string name="settings_trakt_comments">Comments</string>
@ -1146,6 +1151,7 @@
<string name="source_rotten_tomatoes">Rotten Tomatoes</string>
<string name="source_tmdb">TMDB</string>
<string name="source_trakt">Trakt</string>
<string name="source_mal">MyAnimeList</string>
<string name="subtitle_language_unknown">Unknown</string>
<string name="theme_amber">Amber</string>
<string name="theme_crimson">Crimson</string>
@ -1611,8 +1617,19 @@
<string name="home_catalog_default_title">%1$s - %2$s</string>
<string name="library_empty_message">Saved titles will appear here after you tap Save on a details screen.</string>
<string name="library_empty_title">Your library is empty</string>
<string name="library_filter_all_types">All types</string>
<string name="library_filter_list">Select list</string>
<string name="library_filter_sort">Sort library</string>
<string name="library_filter_type">Select type</string>
<string name="library_layout_show_horizontal">Show horizontal shelves</string>
<string name="library_layout_show_vertical">Show vertical grid</string>
<string name="library_load_failed">Couldn't load library</string>
<string name="library_other">Other</string>
<string name="library_sort_added_asc">Oldest added</string>
<string name="library_sort_added_desc">Recently added</string>
<string name="library_sort_title_asc">Title AZ</string>
<string name="library_sort_title_desc">Title ZA</string>
<string name="library_sort_trakt_order">Trakt order</string>
<string name="library_source_cloud">Cloud</string>
<string name="library_source_saved">Saved</string>
<string name="library_title">Library</string>
@ -1641,6 +1658,7 @@
<string name="cloud_library_playable_file_count">%1$d playable files</string>
<string name="cloud_library_provider_all">All</string>
<string name="cloud_library_refresh">Refresh cloud library</string>
<string name="cloud_library_search_label">Search cloud library</string>
<string name="cloud_library_select_provider">Select provider</string>
<string name="cloud_library_select_type">Select type</string>
<string name="cloud_library_status_ready">Ready to play</string>
@ -1708,7 +1726,7 @@
<string name="player_ios_preset_custom_desc">Use your advanced values below.</string>
<string name="player_ios_hardware_decoder_off">Off</string>
<!-- Playback settings iOS video output -->
<string name="settings_playback_ios_video_output">iOS video output</string>
<string name="settings_playback_ios_video_output">iOS VIDEO OUTPUT</string>
<string name="settings_playback_ios_hardware_decoder">Hardware decoder</string>
<string name="settings_playback_ios_extended_dynamic_range">Extended dynamic range</string>
<string name="settings_playback_ios_extended_dynamic_range_desc">Default Metal output mode for new playback sessions.</string>
@ -1717,13 +1735,13 @@
<string name="settings_playback_ios_target_primaries">Target primaries</string>
<string name="settings_playback_ios_target_transfer">Target transfer</string>
<!-- Playback settings iOS audio output -->
<string name="settings_playback_ios_audio_output_section">iOS audio output</string>
<string name="settings_playback_ios_audio_output_section">iOS AUDIO OUTPUT</string>
<string name="settings_playback_ios_audio_output">Audio output</string>
<string name="settings_playback_ios_audio_output_auto_desc">Use AudioUnit while AVFoundation output is temporarily disabled.</string>
<string name="settings_playback_ios_audio_output_avfoundation_desc">Experimental support for Spatial Audio and multichannel output.</string>
<string name="settings_playback_ios_audio_output_audiounit_desc">Use the legacy AudioUnit output.</string>
<!-- Debrid Result Management section -->
<string name="settings_debrid_section_result_management">Result Management</string>
<string name="settings_debrid_section_result_management">RESULT MANAGEMENT</string>
<string name="settings_debrid_max_results">Max results</string>
<string name="settings_debrid_max_results_desc">Limit how many results appear.</string>
<string name="settings_debrid_sort_results">Sort results</string>
@ -1991,15 +2009,39 @@
<string name="p2p_consent_cancel">Cancel</string>
<string name="p2p_error_unknown">Unknown torrent error</string>
<string name="settings_p2p_title">P2P Streaming</string>
<string name="settings_playback_section_p2p">P2P STREAMING</string>
<string name="settings_p2p_subtitle">Allow peer-to-peer (torrent) streams</string>
<string name="settings_p2p_hide_stats_title">Hide torrent stats</string>
<string name="settings_p2p_hide_stats_subtitle">Hide buffer, seeds, peers and download speed during loading and playback</string>
<string name="settings_p2p_profile_title">Torrent profile</string>
<string name="settings_p2p_profile_soft">Soft</string>
<string name="settings_p2p_profile_balanced">Balanced</string>
<string name="settings_p2p_profile_fast">Fast</string>
<string name="settings_p2p_profile_soft_description">Uses fewer peer connections to reduce battery and network load</string>
<string name="settings_p2p_profile_balanced_description">Recommended balance of startup speed and connection stability</string>
<string name="settings_p2p_profile_fast_description">Uses more peer connections for difficult or high-bitrate torrents</string>
<string name="settings_p2p_cache_size_title">Torrent cache size</string>
<string name="settings_p2p_cache_none">No persistent cache</string>
<string name="settings_p2p_cache_2_gb">2 GB</string>
<string name="settings_p2p_cache_5_gb">5 GB</string>
<string name="settings_p2p_cache_10_gb">10 GB</string>
<string name="settings_p2p_clear_cache_title">Clear torrent cache</string>
<string name="settings_p2p_clear_cache_usage">%1$s currently used</string>
<string name="settings_p2p_clear_cache_usage_pending">Usage is measured after P2P starts; tap to clear cached torrent data</string>
<string name="settings_p2p_clear_cache_clearing">Clearing inactive torrent data…</string>
<string name="settings_p2p_clear_cache_playback_active">Stop P2P playback before clearing the cache</string>
<string name="settings_p2p_clear_cache_done">Cleared %1$s of inactive torrent data</string>
<string name="settings_p2p_clear_cache_failed">Could not clear the torrent cache</string>
<string name="player_torrent_peer_info">%1$d seeds · %2$d peers</string>
<string name="player_torrent_buffered_status">%1$s buffered · %2$s · %3$s</string>
<string name="player_torrent_status">%1$s · %2$s</string>
<string name="player_torrent_stats">%1$d peers · %2$d seeds · %3$d%%</string>
<string name="player_torrent_connecting_peers">Connecting to peers…</string>
<string name="player_torrent_starting_engine">Starting P2P engine…</string>
<string name="player_torrent_fetching_metadata">Fetching torrent metadata…</string>
<string name="player_torrent_preparing_stream">Preparing torrent stream…</string>
<string name="player_torrent_connecting_status">%1$s · %2$s · %3$s</string>
<string name="player_torrent_loading_status">%1$s downloaded · %2$s · %3$s</string>
<string name="player_error_failed_start_torrent">Failed to start torrent: %1$s</string>
<string name="player_error_torrent">Torrent error: %1$s</string>
<!-- Person & TMDB entity detail (pass7) -->

View file

@ -74,6 +74,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.LocalNavAnimatedContentScope
import androidx.navigation3.ui.NavDisplay
import androidx.savedstate.serialization.SavedStateConfiguration
@ -101,6 +102,7 @@ import com.nuvio.app.core.ui.NuvioNavBarScrollState
import com.nuvio.app.core.ui.rememberNuvioNavBarScrollState
import com.nuvio.app.core.format.formatReleaseDateForDisplay
import com.nuvio.app.core.ui.NuvioContinueWatchingActionSheet
import com.nuvio.app.core.ui.NuvioCardDepthSurface
import com.nuvio.app.core.ui.NuvioPosterZoomActionOverlay
import com.nuvio.app.core.ui.PosterZoomAnchor
import com.nuvio.app.core.ui.PosterZoomAnchorHolder
@ -158,6 +160,7 @@ import com.nuvio.app.features.home.MetaPreview
import com.nuvio.app.features.library.LibraryItem
import com.nuvio.app.features.library.LibraryRepository
import com.nuvio.app.features.library.LibrarySection
import com.nuvio.app.features.library.LibrarySortOption
import com.nuvio.app.features.library.LibrarySourceMode
import com.nuvio.app.features.library.LibraryScreen
import com.nuvio.app.features.library.toLibraryItem
@ -759,13 +762,17 @@ private fun MainAppContent(
onSwitchProfile: () -> Unit = {},
) {
val navBackStack = rememberNavBackStack(navigationSavedStateConfiguration, initialRoute)
val routeDisposalDecorator = remember {
RouteDisposalNavEntryDecorator<NavKey> { key ->
if (key is AppRoute) disposeRoute(key)
}
}
val navController = remember(navBackStack, onNavigate, onGoBack, onReplace) {
NuvioNavigator(
backStack = navBackStack,
onExternalNavigate = onNavigate,
onExternalBack = onGoBack,
onExternalReplace = onReplace,
onRouteRemoved = ::disposeRoute,
)
}
val appUpdaterController = rememberAppUpdaterController()
@ -1676,7 +1683,7 @@ private fun MainAppContent(
stringResource(Res.string.compose_catalog_subtitle_library)
}
val onLibrarySectionViewAllClick: (LibrarySection) -> Unit = { section ->
val onLibrarySectionViewAllClick: (LibrarySection, LibrarySortOption) -> Unit = { section, sortOption ->
val launchId = CatalogLaunchStore.put(
CatalogLaunch(
title = section.displayTitle,
@ -1684,6 +1691,7 @@ private fun MainAppContent(
target = CatalogTarget.Library(
contentType = section.items.firstOrNull()?.type ?: "movie",
sectionType = section.type,
sortOption = sortOption,
),
),
)
@ -1824,8 +1832,12 @@ private fun MainAppContent(
backStack = navBackStack,
modifier = Modifier.fillMaxSize(),
onBack = { navController.popBackStack() },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
routeDisposalDecorator,
),
sharedTransitionScope = this@SharedTransitionLayout,
entryProvider = entryProvider {
entryProvider = entryProvider<NavKey> {
entry<TabsRoute> {
PlatformBackHandler(
enabled = true,
@ -3260,6 +3272,13 @@ private fun MainAppContent(
},
)
}
}.let { provider ->
{ key ->
routeDisposalDecorator.register(
key = key,
entry = provider(key),
)
}
},
)
}
@ -3384,6 +3403,7 @@ private fun MainAppContent(
imageUrl = cloudLibraryDisplayArtworkUrl(anchor.imageUrl ?: item.poster ?: item.imageUrl),
title = item.title,
subtitle = localizedContinueWatchingSubtitle(item),
depthSurface = NuvioCardDepthSurface.ContinueWatching,
anchor = anchor,
actions = buildList {
if (showDetailsOption) {
@ -3605,7 +3625,7 @@ private fun AppTabHost(
onPosterLongClick: ((MetaPreview) -> Unit)? = null,
onLibraryPosterClick: ((LibraryItem) -> Unit)? = null,
onLibraryPosterLongClick: ((LibraryItem, LibrarySection) -> Unit)? = null,
onLibrarySectionViewAllClick: ((LibrarySection) -> Unit)? = null,
onLibrarySectionViewAllClick: ((LibrarySection, LibrarySortOption) -> Unit)? = null,
onCloudFilePlay: ((CloudLibraryItem, CloudLibraryFile) -> Unit)? = null,
onConnectCloudClick: (() -> Unit)? = null,
onContinueWatchingClick: ((ContinueWatchingItem) -> Unit)? = null,

View file

@ -12,6 +12,7 @@ import com.nuvio.app.features.details.MetaScreenSettingsRepository
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
import com.nuvio.app.features.home.HomeRepository
import com.nuvio.app.features.library.LibraryRepository
import com.nuvio.app.features.library.LibraryDisplaySettingsRepository
import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsRepository
import com.nuvio.app.features.player.PlayerLaunchStore
import com.nuvio.app.features.player.PlayerSettingsRepository
@ -56,6 +57,7 @@ internal object LocalAccountDataCleaner {
HomeCatalogSettingsRepository.clearLocalState()
MetaScreenSettingsRepository.clearLocalState()
LibraryRepository.clearLocalState()
LibraryDisplaySettingsRepository.clearLocalState()
ContinueWatchingPreferencesRepository.clearLocalState()
EpisodeReleaseNotificationsRepository.clearLocalState()
CollectionMobileSettingsRepository.clearLocalState()

View file

@ -427,7 +427,7 @@ object SyncManager {
continue
}
TraktAuthRepository.ensureLoaded()
TraktAuthRepository.ensureLoaded(profileId)
TraktSettingsRepository.ensureLoaded()
val traktAuthenticated = TraktAuthRepository.isAuthenticated.value

View file

@ -129,6 +129,7 @@ fun NuvioPosterZoomActionOverlay(
title: String,
subtitle: String?,
isWatched: Boolean = false,
depthSurface: NuvioCardDepthSurface = NuvioCardDepthSurface.Posters,
anchor: PosterZoomAnchor?,
actions: List<PosterZoomOverlayAction>,
hazeState: HazeState,
@ -136,6 +137,7 @@ fun NuvioPosterZoomActionOverlay(
modifier: Modifier = Modifier,
) {
val tokens = MaterialTheme.nuvio
val previewShape = RoundedCornerShape(PosterZoomFinalCornerRadius)
val hapticFeedback = LocalHapticFeedback.current
val scope = rememberCoroutineScope()
@ -385,7 +387,11 @@ fun NuvioPosterZoomActionOverlay(
shape = RoundedCornerShape(posterCornerRadiusPx(anchor, clamped, scale))
clip = true
}
.background(tokens.colors.surfaceCard),
.background(tokens.colors.surfaceCard)
.nuvioCardDepth(
shape = previewShape,
surface = depthSurface,
),
contentAlignment = Alignment.Center,
) {
if (imageUrl != null) {

View file

@ -4,6 +4,7 @@ import com.nuvio.app.features.collection.CollectionRepository
import com.nuvio.app.features.collection.TmdbCollectionSourceResolver
import com.nuvio.app.features.collection.catalogRouteKey
import com.nuvio.app.features.library.LibraryRepository
import com.nuvio.app.features.library.sortLibraryItems
import com.nuvio.app.features.library.toMetaPreview
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
import com.nuvio.app.features.home.filterReleasedItems
@ -88,10 +89,16 @@ object CatalogRepository {
runCatching {
val target = request.target as CatalogTarget.Library
LibraryRepository.ensureLoaded()
LibraryRepository.uiState.value.sections
val libraryState = LibraryRepository.uiState.value
val items = libraryState.sections
.firstOrNull { it.type == target.sectionType }
?.items
.orEmpty()
sortLibraryItems(
items = items,
selected = target.sortOption,
sourceMode = libraryState.sourceMode,
)
.map { it.toMetaPreview() }
.let(::dedupeCatalogItems)
}.fold(

View file

@ -50,7 +50,9 @@ import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
import coil3.compose.AsyncImage
import com.nuvio.app.core.format.formatReleaseDateForDisplay
import com.nuvio.app.core.ui.NuvioBackButton
import com.nuvio.app.core.ui.NuvioCardDepthSurface
import com.nuvio.app.core.ui.NuvioPosterWatchedOverlay
import com.nuvio.app.core.ui.nuvioCardDepth
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
import com.nuvio.app.core.ui.posterCardClickable
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
@ -304,6 +306,10 @@ private fun CatalogPosterTile(
.aspectRatio(item.posterShape.catalogAspectRatio())
.clip(RoundedCornerShape(cornerRadiusDp.dp))
.background(MaterialTheme.colorScheme.surface)
.nuvioCardDepth(
shape = RoundedCornerShape(cornerRadiusDp.dp),
surface = NuvioCardDepthSurface.Posters,
)
.posterCardClickable(
onClick = onClick,
onLongClick = onLongClick,

View file

@ -1,5 +1,6 @@
package com.nuvio.app.features.catalog
import com.nuvio.app.features.library.LibrarySortOption
import kotlinx.serialization.Serializable
sealed interface CatalogTarget {
@ -17,6 +18,7 @@ sealed interface CatalogTarget {
data class Library(
override val contentType: String,
val sectionType: String,
val sortOption: LibrarySortOption = LibrarySortOption.DEFAULT,
) : CatalogTarget {
override val supportsPagination: Boolean = false
}

View file

@ -76,6 +76,7 @@ import com.nuvio.app.core.network.NetworkCondition
import com.nuvio.app.core.network.NetworkStatusRepository
import com.nuvio.app.core.i18n.localizedSeasonEpisodeCode
import com.nuvio.app.core.ui.NuvioBackButton
import com.nuvio.app.core.ui.NuvioCardDepthSurface
import com.nuvio.app.core.ui.NuvioPosterZoomActionOverlay
import com.nuvio.app.core.ui.PosterZoomAnchor
import com.nuvio.app.core.ui.PosterZoomAnchorHolder
@ -1339,6 +1340,7 @@ fun MetaDetailsScreen(
title = selectedEpisode.title,
subtitle = localizedSeasonEpisodeCode(selectedEpisode.season, selectedEpisode.episode) ?: seasonLabel,
isWatched = isSelectedEpisodeWatched,
depthSurface = NuvioCardDepthSurface.EpisodeCards,
anchor = zoomAnchor,
actions = buildList {
add(

View file

@ -50,6 +50,7 @@ import com.nuvio.app.features.mdblist.MdbListMetadataService.PROVIDER_AUDIENCE
import com.nuvio.app.features.mdblist.MdbListMetadataService.PROVIDER_IMDB
import com.nuvio.app.features.mdblist.MdbListMetadataService.PROVIDER_LETTERBOXD
import com.nuvio.app.features.mdblist.MdbListMetadataService.PROVIDER_METACRITIC
import com.nuvio.app.features.mdblist.MdbListMetadataService.PROVIDER_MAL
import com.nuvio.app.features.mdblist.MdbListMetadataService.PROVIDER_TMDB
import com.nuvio.app.features.mdblist.MdbListMetadataService.PROVIDER_TOMATOES
import com.nuvio.app.features.mdblist.MdbListMetadataService.PROVIDER_TRAKT
@ -358,22 +359,6 @@ private val ratingVisuals = listOf(
valueColor = Color(0xFF01B4E4),
format = ::formatWhole,
),
RatingVisuals(
source = PROVIDER_TOMATOES,
displayName = "Rotten Tomatoes",
logo = Res.drawable.rating_rotten_tomatoes,
logoWidth = 16.dp,
valueColor = Color(0xFFFA320A),
format = ::formatPercent,
),
RatingVisuals(
source = PROVIDER_METACRITIC,
displayName = "Metacritic",
logo = Res.drawable.rating_metacritic,
logoWidth = 16.dp,
valueColor = Color(0xFFFFCC33),
format = ::formatWhole,
),
RatingVisuals(
source = PROVIDER_TRAKT,
displayName = "Trakt",
@ -390,6 +375,22 @@ private val ratingVisuals = listOf(
valueColor = Color(0xFF00E054),
format = ::formatOneDecimal,
),
RatingVisuals(
source = PROVIDER_MAL,
displayName = "MyAnimeList",
logo = Res.drawable.rating_mal,
logoWidth = 16.dp,
valueColor = Color(0xFF2E51A2),
format = ::formatOneDecimal,
),
RatingVisuals(
source = PROVIDER_TOMATOES,
displayName = "Rotten Tomatoes",
logo = Res.drawable.rating_rotten_tomatoes,
logoWidth = 16.dp,
valueColor = Color(0xFFFA320A),
format = ::formatPercent,
),
RatingVisuals(
source = PROVIDER_AUDIENCE,
displayName = runBlocking { getString(Res.string.rating_audience_score) },
@ -398,6 +399,14 @@ private val ratingVisuals = listOf(
valueColor = Color(0xFFFA320A),
format = ::formatPercent,
),
RatingVisuals(
source = PROVIDER_METACRITIC,
displayName = "Metacritic",
logo = Res.drawable.rating_metacritic,
logoWidth = 16.dp,
valueColor = Color(0xFFFFCC33),
format = ::formatWhole,
),
)
private fun formatOneDecimal(value: Double): String {

View file

@ -3,6 +3,8 @@ package com.nuvio.app.features.home
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -13,6 +15,7 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nuvio.app.core.auth.AuthRepository
@ -45,6 +48,7 @@ import com.nuvio.app.features.home.components.HomeHeroSection
import com.nuvio.app.features.home.components.HomeSkeletonHero
import com.nuvio.app.features.home.components.HomeSkeletonRow
import com.nuvio.app.features.home.components.HomeContinueWatchingSectionBottomPadding
import com.nuvio.app.features.home.components.ContinueWatchingLayout
import com.nuvio.app.features.trakt.TRAKT_CONTINUE_WATCHING_DAYS_CAP_ALL
import com.nuvio.app.features.trakt.TraktSettingsRepository
import com.nuvio.app.features.trakt.WatchProgressSource
@ -58,11 +62,13 @@ import com.nuvio.app.features.watchprogress.CachedNextUpItem
import com.nuvio.app.features.watchprogress.ContinueWatchingEnrichmentCache
import com.nuvio.app.features.watchprogress.CurrentDateProvider
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesUiState
import com.nuvio.app.features.watchprogress.ContinueWatchingItem
import com.nuvio.app.features.watchprogress.ContinueWatchingSortMode
import com.nuvio.app.features.watchprogress.isMalformedNextUpSeedContentId
import com.nuvio.app.features.watchprogress.isSeriesTypeForContinueWatching
import com.nuvio.app.features.watchprogress.nextUpDismissKey
import com.nuvio.app.features.watchprogress.parseReleaseDateToEpochMs
import com.nuvio.app.features.watchprogress.resolvedProgressKey
import com.nuvio.app.features.watchprogress.shouldTreatAsInProgressForContinueWatching
import com.nuvio.app.features.watchprogress.shouldUseAsCompletedSeedForContinueWatching
@ -132,6 +138,7 @@ fun HomeScreen(
}.collectAsStateWithLifecycle()
val homeListState = rememberLazyListState()
val continueWatchingListState = rememberLazyListState()
val upcomingListState = rememberLazyListState()
val collections by CollectionRepository.collections.collectAsStateWithLifecycle()
val continueWatchingPreferences by ContinueWatchingPreferencesRepository.uiState.collectAsStateWithLifecycle()
val watchedUiState by WatchedRepository.uiState.collectAsStateWithLifecycle()
@ -286,6 +293,7 @@ fun HomeScreen(
val activeProfileId = profileState.activeProfile?.profileIndex ?: 1
val cwCacheGeneration by ContinueWatchingEnrichmentCache.generation.collectAsStateWithLifecycle()
var hasUserScrolledContinueWatching by remember(activeProfileId) { mutableStateOf(false) }
var hasUserScrolledUpcoming by remember(activeProfileId) { mutableStateOf(false) }
LaunchedEffect(activeProfileId, continueWatchingListState) {
snapshotFlow { continueWatchingListState.isScrollInProgress }.collect { isScrolling ->
@ -293,6 +301,12 @@ fun HomeScreen(
}
}
LaunchedEffect(activeProfileId, upcomingListState) {
snapshotFlow { upcomingListState.isScrollInProgress }.collect { isScrolling ->
if (isScrolling) hasUserScrolledUpcoming = true
}
}
var nextUpItemsBySeries by remember(activeProfileId, effectiveWatchProgressSource) {
mutableStateOf<Map<String, Pair<Long, ContinueWatchingItem>>>(emptyMap())
}
@ -421,7 +435,7 @@ fun HomeScreen(
)
}
val continueWatchingItems = remember(
val allContinueWatchingItems = remember(
visibleContinueWatchingEntries,
cachedInProgressItems,
effectivNextUpItems,
@ -439,6 +453,17 @@ fun HomeScreen(
cloudLibraryUiState = cloudLibraryUiState,
)
}
val (continueWatchingItems, upcomingItems) = remember(
allContinueWatchingItems,
continueWatchingPreferences.sortMode,
) {
splitUpcomingItems(
items = allContinueWatchingItems,
mode = continueWatchingPreferences.sortMode,
)
}
val hasContinueWatchingRows = continueWatchingItems.isNotEmpty() || upcomingItems.isNotEmpty()
LaunchedEffect(activeProfileId, continueWatchingItems.isNotEmpty(), hasUserScrolledContinueWatching) {
if (!hasUserScrolledContinueWatching && continueWatchingItems.isNotEmpty()) {
snapshotFlow {
@ -455,6 +480,22 @@ fun HomeScreen(
}
}
}
LaunchedEffect(activeProfileId, upcomingItems.isNotEmpty(), hasUserScrolledUpcoming) {
if (!hasUserScrolledUpcoming && upcomingItems.isNotEmpty()) {
snapshotFlow {
upcomingListState.firstVisibleItemIndex to
upcomingListState.firstVisibleItemScrollOffset
}.collect { (index, offset) ->
if (
!hasUserScrolledUpcoming &&
!upcomingListState.isScrollInProgress &&
(index != 0 || offset != 0)
) {
upcomingListState.scrollToItem(0)
}
}
}
}
val enabledAddons = remember(addonsUiState.addons) {
addonsUiState.addons.enabledAddons()
}
@ -817,7 +858,7 @@ fun HomeScreen(
maxWidth.value,
continueWatchingPreferences.isVisible,
continueWatchingPreferences.style,
continueWatchingItems.isNotEmpty(),
hasContinueWatchingRows,
continueWatchingLayout,
posterCardStyle.widthDp,
homeSettingsUiState.hideCatalogUnderline,
@ -826,7 +867,7 @@ fun HomeScreen(
if (
maxWidth.value < 600f &&
continueWatchingPreferences.isVisible &&
continueWatchingItems.isNotEmpty()
hasContinueWatchingRows
) {
continueWatchingHeroViewportReserveHeight(
style = continueWatchingPreferences.style,
@ -882,22 +923,17 @@ fun HomeScreen(
when {
!hasActiveAddons && !hasRenderableCollectionRows -> {
if (continueWatchingPreferences.isVisible && continueWatchingItems.isNotEmpty()) {
item(key = HOME_CONTINUE_WATCHING_SECTION_KEY) {
HomeContinueWatchingSection(
items = continueWatchingItems,
style = continueWatchingPreferences.style,
useEpisodeThumbnails = continueWatchingPreferences.useEpisodeThumbnails,
blurNextUp = continueWatchingPreferences.blurNextUp,
modifier = Modifier.padding(bottom = HomeContinueWatchingSectionBottomPadding),
sectionPadding = homeSectionPadding,
layout = continueWatchingLayout,
listState = continueWatchingListState,
onItemClick = onContinueWatchingClick,
onItemLongPress = onContinueWatchingLongPress,
)
}
}
homeContinueWatchingSections(
preferences = continueWatchingPreferences,
continueWatchingItems = continueWatchingItems,
upcomingItems = upcomingItems,
sectionPadding = homeSectionPadding,
layout = continueWatchingLayout,
continueWatchingListState = continueWatchingListState,
upcomingListState = upcomingListState,
onItemClick = onContinueWatchingClick,
onItemLongPress = onContinueWatchingLongPress,
)
item {
HomeEmptyStateCard(
modifier = Modifier.padding(horizontal = 16.dp),
@ -908,22 +944,17 @@ fun HomeScreen(
}
homeUiState.isLoading && homeUiState.sections.isEmpty() && !hasRenderableCollectionRows -> {
if (continueWatchingPreferences.isVisible && continueWatchingItems.isNotEmpty()) {
item(key = HOME_CONTINUE_WATCHING_SECTION_KEY) {
HomeContinueWatchingSection(
items = continueWatchingItems,
style = continueWatchingPreferences.style,
useEpisodeThumbnails = continueWatchingPreferences.useEpisodeThumbnails,
blurNextUp = continueWatchingPreferences.blurNextUp,
modifier = Modifier.padding(bottom = HomeContinueWatchingSectionBottomPadding),
sectionPadding = homeSectionPadding,
layout = continueWatchingLayout,
listState = continueWatchingListState,
onItemClick = onContinueWatchingClick,
onItemLongPress = onContinueWatchingLongPress,
)
}
}
homeContinueWatchingSections(
preferences = continueWatchingPreferences,
continueWatchingItems = continueWatchingItems,
upcomingItems = upcomingItems,
sectionPadding = homeSectionPadding,
layout = continueWatchingLayout,
continueWatchingListState = continueWatchingListState,
upcomingListState = upcomingListState,
onItemClick = onContinueWatchingClick,
onItemLongPress = onContinueWatchingLongPress,
)
items(3) {
HomeSkeletonRow(
modifier = Modifier.padding(horizontal = 16.dp),
@ -933,7 +964,7 @@ fun HomeScreen(
}
homeUiState.sections.isEmpty() && homeUiState.heroItems.isEmpty() &&
(!continueWatchingPreferences.isVisible || continueWatchingItems.isEmpty()) &&
(!continueWatchingPreferences.isVisible || !hasContinueWatchingRows) &&
!hasRenderableCollectionRows -> {
item {
if (networkStatusUiState.isOfflineLike) {
@ -957,22 +988,17 @@ fun HomeScreen(
}
else -> {
if (continueWatchingPreferences.isVisible && continueWatchingItems.isNotEmpty()) {
item(key = HOME_CONTINUE_WATCHING_SECTION_KEY) {
HomeContinueWatchingSection(
items = continueWatchingItems,
style = continueWatchingPreferences.style,
useEpisodeThumbnails = continueWatchingPreferences.useEpisodeThumbnails,
blurNextUp = continueWatchingPreferences.blurNextUp,
modifier = Modifier.padding(bottom = HomeContinueWatchingSectionBottomPadding),
sectionPadding = homeSectionPadding,
layout = continueWatchingLayout,
listState = continueWatchingListState,
onItemClick = onContinueWatchingClick,
onItemLongPress = onContinueWatchingLongPress,
)
}
}
homeContinueWatchingSections(
preferences = continueWatchingPreferences,
continueWatchingItems = continueWatchingItems,
upcomingItems = upcomingItems,
sectionPadding = homeSectionPadding,
layout = continueWatchingLayout,
continueWatchingListState = continueWatchingListState,
upcomingListState = upcomingListState,
onItemClick = onContinueWatchingClick,
onItemLongPress = onContinueWatchingLongPress,
)
keyedEnabledHomeItems.forEach { keyedSettingsItem ->
val settingsItem = keyedSettingsItem.value
@ -1018,8 +1044,58 @@ fun HomeScreen(
}
}
private fun LazyListScope.homeContinueWatchingSections(
preferences: ContinueWatchingPreferencesUiState,
continueWatchingItems: List<ContinueWatchingItem>,
upcomingItems: List<ContinueWatchingItem>,
sectionPadding: Dp,
layout: ContinueWatchingLayout,
continueWatchingListState: LazyListState,
upcomingListState: LazyListState,
onItemClick: ((ContinueWatchingItem) -> Unit)?,
onItemLongPress: ((ContinueWatchingItem) -> Unit)?,
) {
if (!preferences.isVisible) return
if (continueWatchingItems.isNotEmpty()) {
item(key = HOME_CONTINUE_WATCHING_SECTION_KEY) {
HomeContinueWatchingSection(
items = continueWatchingItems,
style = preferences.style,
useEpisodeThumbnails = preferences.useEpisodeThumbnails,
blurNextUp = preferences.blurNextUp,
modifier = Modifier.padding(bottom = HomeContinueWatchingSectionBottomPadding),
sectionPadding = sectionPadding,
layout = layout,
listState = continueWatchingListState,
onItemClick = onItemClick,
onItemLongPress = onItemLongPress,
)
}
}
if (upcomingItems.isNotEmpty()) {
item(key = HOME_UPCOMING_SECTION_KEY) {
HomeContinueWatchingSection(
items = upcomingItems,
style = preferences.style,
useEpisodeThumbnails = preferences.useEpisodeThumbnails,
blurNextUp = preferences.blurNextUp,
modifier = Modifier.padding(bottom = HomeContinueWatchingSectionBottomPadding),
title = stringResource(Res.string.upcoming_section_title),
sectionPadding = sectionPadding,
layout = layout,
listState = upcomingListState,
onItemClick = onItemClick,
onItemLongPress = onItemLongPress,
)
}
}
}
private const val HOME_CATALOG_PREVIEW_LIMIT = 18
private const val HOME_CONTINUE_WATCHING_SECTION_KEY = "home_continue_watching"
private const val HOME_UPCOMING_SECTION_KEY = "home_upcoming"
internal const val HomeContinueWatchingMaxRecentProgressItems = 300
internal const val HomeNextUpInitialResolutionLimit = 32
private const val MILLIS_PER_DAY = 24L * 60L * 60L * 1000L
@ -1461,11 +1537,44 @@ internal fun buildHomeContinueWatchingItems(
}
return when (sortMode) {
ContinueWatchingSortMode.DEFAULT -> deduplicated.map(HomeContinueWatchingCandidate::item)
ContinueWatchingSortMode.DEFAULT,
ContinueWatchingSortMode.SPLIT_UPCOMING,
-> deduplicated.map(HomeContinueWatchingCandidate::item)
ContinueWatchingSortMode.STREAMING_STYLE -> applyStreamingStyleSort(deduplicated, todayIsoDate)
}
}
/**
* Splits unaired next-up episodes into a dedicated row when [ContinueWatchingSortMode.SPLIT_UPCOMING]
* is active. The main row keeps its recency ordering, while Upcoming is ordered by the nearest
* release instant with unknown dates last.
*/
internal fun splitUpcomingItems(
items: List<ContinueWatchingItem>,
mode: ContinueWatchingSortMode,
nowEpochMs: Long = WatchProgressClock.nowEpochMs(),
): Pair<List<ContinueWatchingItem>, List<ContinueWatchingItem>> {
if (mode != ContinueWatchingSortMode.SPLIT_UPCOMING) {
return items to emptyList()
}
val (upcoming, main) = items.partition { item ->
item.isNextUp && parseReleaseDateToEpochMs(item.released)
?.let { releaseEpochMs -> releaseEpochMs > nowEpochMs } == true
}
val sortedUpcoming = upcoming.sortedWith { first, second ->
val firstRelease = parseReleaseDateToEpochMs(first.released)
val secondRelease = parseReleaseDateToEpochMs(second.released)
when {
firstRelease == null && secondRelease == null -> 0
firstRelease == null -> 1
secondRelease == null -> -1
else -> firstRelease.compareTo(secondRelease)
}
}
return main to sortedUpcoming
}
private fun applyStreamingStyleSort(
candidates: List<HomeContinueWatchingCandidate>,
todayIsoDate: String,

View file

@ -26,9 +26,11 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nuvio.app.core.ui.NuvioCardDepthSurface
import com.nuvio.app.core.ui.NuvioShelfSection
import com.nuvio.app.core.ui.PosterLandscapeAspectRatio
import com.nuvio.app.core.ui.landscapePosterWidth
import com.nuvio.app.core.ui.nuvioCardDepth
import com.nuvio.app.core.ui.posterCardClickable
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
import com.nuvio.app.features.collection.Collection
@ -134,7 +136,11 @@ private fun CollectionFolderCard(
Card(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(aspectRatio),
.aspectRatio(aspectRatio)
.nuvioCardDepth(
shape = shapeCorner,
surface = NuvioCardDepthSurface.Posters,
),
shape = shapeCorner,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface,

View file

@ -235,6 +235,7 @@ internal fun HomeContinueWatchingSection(
useEpisodeThumbnails: Boolean = true,
blurNextUp: Boolean = false,
modifier: Modifier = Modifier,
title: String? = null,
sectionPadding: Dp? = null,
layout: ContinueWatchingLayout? = null,
listState: LazyListState = rememberLazyListState(),
@ -250,6 +251,7 @@ internal fun HomeContinueWatchingSection(
useEpisodeThumbnails = useEpisodeThumbnails,
blurNextUp = blurNextUp,
modifier = modifier.fillMaxWidth(),
title = title,
sectionPadding = sectionPadding,
layout = layout,
listState = listState,
@ -264,6 +266,7 @@ internal fun HomeContinueWatchingSection(
useEpisodeThumbnails = useEpisodeThumbnails,
blurNextUp = blurNextUp,
modifier = Modifier.fillMaxWidth(),
title = title,
sectionPadding = homeSectionHorizontalPaddingForWidth(maxWidth.value),
layout = rememberContinueWatchingLayout(maxWidth.value),
listState = listState,
@ -281,6 +284,7 @@ private fun HomeContinueWatchingSectionContent(
useEpisodeThumbnails: Boolean,
blurNextUp: Boolean,
modifier: Modifier,
title: String?,
sectionPadding: Dp,
layout: ContinueWatchingLayout,
listState: LazyListState,
@ -296,7 +300,7 @@ private fun HomeContinueWatchingSectionContent(
val displayEntries = disintegration.sync(items)
NuvioShelfSection(
title = stringResource(Res.string.compose_settings_page_continue_watching),
title = title ?: stringResource(Res.string.compose_settings_page_continue_watching),
entries = displayEntries,
modifier = modifier,
headerHorizontalPadding = sectionPadding,
@ -1023,6 +1027,10 @@ private fun ContinueWatchingPosterCard(
.height(layout.posterCardHeight)
.clip(RoundedCornerShape(layout.cardRadius))
.background(MaterialTheme.colorScheme.surfaceVariant)
.nuvioCardDepth(
shape = RoundedCornerShape(layout.cardRadius),
surface = NuvioCardDepthSurface.ContinueWatching,
)
.posterCardClickable(
onClick = onClick,
onLongClick = onLongClick,

View file

@ -0,0 +1,178 @@
package com.nuvio.app.features.home.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.nuvio.app.core.format.formatReleaseDateForDisplay
import com.nuvio.app.core.ui.NuvioCardDepthSurface
import com.nuvio.app.core.ui.NuvioPosterWatchedOverlay
import com.nuvio.app.core.ui.nuvioCardDepth
import com.nuvio.app.core.ui.posterCardClickable
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
import com.nuvio.app.features.home.MetaPreview
import com.nuvio.app.features.home.PosterShape
import com.nuvio.app.features.watching.application.WatchingState
internal fun posterGridColumnCountForWidth(screenWidth: Dp): Int =
when {
screenWidth >= 1400.dp -> 7
screenWidth >= 1200.dp -> 6
screenWidth >= 1000.dp -> 5
screenWidth >= 840.dp -> 4
else -> 3
}
@Composable
internal fun PosterGridRow(
items: List<MetaPreview>,
columns: Int,
modifier: Modifier = Modifier,
watchedKeys: Set<String> = emptySet(),
fullyWatchedSeriesKeys: Set<String> = emptySet(),
onPosterClick: ((MetaPreview) -> Unit)? = null,
onPosterLongClick: ((MetaPreview) -> Unit)? = null,
) {
val posterCardStyle = rememberPosterCardStyleUiState()
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.Top,
) {
items.forEach { item ->
PosterGridTile(
item = item,
cornerRadiusDp = posterCardStyle.cornerRadiusDp,
hideLabels = posterCardStyle.hideLabelsEnabled,
modifier = Modifier.weight(1f),
isWatched = WatchingState.isPosterWatched(
watchedKeys = watchedKeys,
item = item,
fullyWatchedSeriesKeys = fullyWatchedSeriesKeys,
),
onClick = onPosterClick?.let { { it(item) } },
onLongClick = onPosterLongClick?.let { { it(item) } },
)
}
repeat(columns - items.size) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
@Composable
internal fun PosterGridSkeletonRow(
columns: Int,
modifier: Modifier = Modifier,
) {
val posterCardStyle = rememberPosterCardStyleUiState()
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
repeat(columns) {
Box(
modifier = Modifier
.weight(1f)
.aspectRatio(0.68f)
.clip(RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp))
.background(MaterialTheme.colorScheme.surface),
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun PosterGridTile(
item: MetaPreview,
cornerRadiusDp: Int,
hideLabels: Boolean,
modifier: Modifier = Modifier,
isWatched: Boolean = false,
onClick: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(item.posterShape.posterGridAspectRatio())
.clip(RoundedCornerShape(cornerRadiusDp.dp))
.background(MaterialTheme.colorScheme.surface)
.nuvioCardDepth(
shape = RoundedCornerShape(cornerRadiusDp.dp),
surface = NuvioCardDepthSurface.Posters,
)
.posterCardClickable(
onClick = onClick,
onLongClick = onLongClick,
zoomImageUrl = item.poster,
zoomCornerRadius = cornerRadiusDp.dp,
),
) {
if (item.poster != null) {
AsyncImage(
model = item.poster,
contentDescription = item.name,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop,
)
}
NuvioPosterWatchedOverlay(isWatched = isWatched)
}
if (!hideLabels) {
Text(
text = item.name,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onBackground,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
val detail = item.releaseInfo?.let { formatReleaseDateForDisplay(it) }
if (detail != null) {
Text(
text = detail,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} else {
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
private fun PosterShape.posterGridAspectRatio(): Float =
when (this) {
PosterShape.Poster -> 0.68f
PosterShape.Square -> 1f
PosterShape.Landscape -> 1.2f
}

View file

@ -0,0 +1,251 @@
package com.nuvio.app.features.library
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
enum class LibraryLayoutMode {
HORIZONTAL,
VERTICAL,
}
enum class LibrarySortOption {
DEFAULT,
ADDED_DESC,
ADDED_ASC,
TITLE_ASC,
TITLE_DESC,
}
data class LibraryDisplaySettingsUiState(
val layoutMode: LibraryLayoutMode = LibraryLayoutMode.HORIZONTAL,
val sortOption: LibrarySortOption = LibrarySortOption.DEFAULT,
)
object LibraryDisplaySettingsRepository {
private val _uiState = MutableStateFlow(LibraryDisplaySettingsUiState())
val uiState: StateFlow<LibraryDisplaySettingsUiState> = _uiState.asStateFlow()
private var hasLoaded = false
fun ensureLoaded() {
if (hasLoaded) return
loadFromDisk()
}
fun onProfileChanged() {
loadFromDisk()
}
fun clearLocalState() {
hasLoaded = false
_uiState.value = LibraryDisplaySettingsUiState()
}
fun setLayoutMode(layoutMode: LibraryLayoutMode) {
ensureLoaded()
if (_uiState.value.layoutMode == layoutMode) return
_uiState.value = _uiState.value.copy(layoutMode = layoutMode)
persist()
}
fun setSortOption(sortOption: LibrarySortOption) {
ensureLoaded()
if (_uiState.value.sortOption == sortOption) return
_uiState.value = _uiState.value.copy(sortOption = sortOption)
persist()
}
private fun loadFromDisk() {
hasLoaded = true
_uiState.value = decodeLibraryDisplaySettings(LibraryDisplaySettingsStorage.loadPayload())
}
private fun persist() {
LibraryDisplaySettingsStorage.savePayload(encodeLibraryDisplaySettings(_uiState.value))
}
}
internal data class LibraryVerticalEntry(
val item: LibraryItem,
val section: LibrarySection,
)
internal data class LibraryVerticalProjection(
val availableSections: List<LibrarySection>,
val selectedSectionKey: String?,
val availableTypes: List<String>,
val selectedType: String?,
val entries: List<LibraryVerticalEntry>,
)
internal fun availableLibrarySortOptions(sourceMode: LibrarySourceMode): List<LibrarySortOption> =
if (sourceMode == LibrarySourceMode.TRAKT) {
LibrarySortOption.entries
} else {
LibrarySortOption.entries.filterNot { it == LibrarySortOption.DEFAULT }
}
internal fun effectiveLibrarySortOption(
selected: LibrarySortOption,
sourceMode: LibrarySourceMode,
): LibrarySortOption =
if (selected == LibrarySortOption.DEFAULT && sourceMode == LibrarySourceMode.LOCAL) {
LibrarySortOption.ADDED_DESC
} else {
selected
}
internal fun sortLibraryItems(
items: List<LibraryItem>,
selected: LibrarySortOption,
sourceMode: LibrarySourceMode,
): List<LibraryItem> =
when (effectiveLibrarySortOption(selected, sourceMode)) {
LibrarySortOption.DEFAULT -> items.sortedWith(
compareBy<LibraryItem> { it.traktRank ?: Int.MAX_VALUE }
.thenByDescending { it.savedAtEpochMs }
.thenBy { libraryTitleTieBreakKey(it) }
.thenBy { it.id },
)
LibrarySortOption.ADDED_DESC -> items.sortedWith(
compareByDescending<LibraryItem> { it.savedAtEpochMs }
.thenBy { libraryTitleTieBreakKey(it) }
.thenBy { it.id },
)
LibrarySortOption.ADDED_ASC -> items.sortedWith(
compareBy<LibraryItem> { it.savedAtEpochMs }
.thenBy { libraryTitleTieBreakKey(it) }
.thenBy { it.id },
)
LibrarySortOption.TITLE_ASC -> items.sortedWith(
compareBy<LibraryItem> { libraryTitleSortKey(it) }
.thenBy { it.id },
)
LibrarySortOption.TITLE_DESC -> items.sortedWith(
compareByDescending<LibraryItem> { libraryTitleSortKey(it) }
.thenBy { it.id },
)
}
internal fun sortLibrarySections(
sections: List<LibrarySection>,
selected: LibrarySortOption,
sourceMode: LibrarySourceMode,
): List<LibrarySection> =
sections.map { section ->
section.copy(items = sortLibraryItems(section.items, selected, sourceMode))
}
internal fun buildLibraryVerticalProjection(
sections: List<LibrarySection>,
sourceMode: LibrarySourceMode,
selectedSectionKey: String?,
selectedType: String?,
sortOption: LibrarySortOption,
): LibraryVerticalProjection {
val availableSections = if (sourceMode == LibrarySourceMode.TRAKT) sections else emptyList()
val selectedSection = if (sourceMode == LibrarySourceMode.TRAKT) {
sections.firstOrNull { it.type == selectedSectionKey } ?: sections.firstOrNull()
} else {
null
}
val baseEntries = if (selectedSection != null) {
selectedSection.items.map { item -> LibraryVerticalEntry(item, selectedSection) }
} else {
sections.flatMap { section ->
section.items.map { item -> LibraryVerticalEntry(item, section) }
}
}
val deduplicatedEntries = LinkedHashMap<String, LibraryVerticalEntry>()
baseEntries.forEach { entry ->
val key = libraryDisplayItemKey(entry.item)
if (key !in deduplicatedEntries) {
deduplicatedEntries[key] = entry
}
}
val availableTypes = deduplicatedEntries.values
.map { entry -> entry.item.type.normalizedLibraryType() }
.filter { it.isNotBlank() }
.distinct()
.sorted()
val effectiveType = selectedType
?.normalizedLibraryType()
?.takeIf { it in availableTypes }
val filteredEntries = deduplicatedEntries.values.filter { entry ->
effectiveType == null || entry.item.type.normalizedLibraryType() == effectiveType
}
val entryByKey = filteredEntries.associateBy { entry -> libraryDisplayItemKey(entry.item) }
val sortedEntries = sortLibraryItems(
items = filteredEntries.map { entry -> entry.item },
selected = sortOption,
sourceMode = sourceMode,
).mapNotNull { item -> entryByKey[libraryDisplayItemKey(item)] }
return LibraryVerticalProjection(
availableSections = availableSections,
selectedSectionKey = selectedSection?.type,
availableTypes = availableTypes,
selectedType = effectiveType,
entries = sortedEntries,
)
}
internal fun encodeLibraryDisplaySettings(state: LibraryDisplaySettingsUiState): String =
LibraryDisplaySettingsJson.encodeToString(
StoredLibraryDisplaySettings(
layoutMode = state.layoutMode.name,
sortOption = state.sortOption.name,
),
)
internal fun decodeLibraryDisplaySettings(payload: String?): LibraryDisplaySettingsUiState {
val stored = payload
?.takeIf { it.isNotBlank() }
?.let { value ->
runCatching {
LibraryDisplaySettingsJson.decodeFromString<StoredLibraryDisplaySettings>(value)
}.getOrNull()
}
return LibraryDisplaySettingsUiState(
layoutMode = stored?.layoutMode
?.let { value -> LibraryLayoutMode.entries.firstOrNull { it.name == value } }
?: LibraryLayoutMode.HORIZONTAL,
sortOption = stored?.sortOption
?.let { value -> LibrarySortOption.entries.firstOrNull { it.name == value } }
?: LibrarySortOption.DEFAULT,
)
}
private val LibraryDisplaySettingsJson = Json {
ignoreUnknownKeys = true
encodeDefaults = true
}
private val LeadingLibraryTitleArticle = Regex("^(the|an|a)\\s+", RegexOption.IGNORE_CASE)
private fun libraryTitleSortKey(item: LibraryItem): String =
libraryTitleTieBreakKey(item)
.trim()
.replace(LeadingLibraryTitleArticle, "")
private fun libraryTitleTieBreakKey(item: LibraryItem): String =
item.name
.ifBlank { item.id }
.lowercase()
private fun libraryDisplayItemKey(item: LibraryItem): String =
"${item.type.normalizedLibraryType()}:${item.id.trim()}"
private fun String.normalizedLibraryType(): String = trim().lowercase()
@Serializable
private data class StoredLibraryDisplaySettings(
@SerialName("layout_mode") val layoutMode: String = LibraryLayoutMode.HORIZONTAL.name,
@SerialName("sort_option") val sortOption: String = LibrarySortOption.DEFAULT.name,
)

View file

@ -0,0 +1,6 @@
package com.nuvio.app.features.library
internal expect object LibraryDisplaySettingsStorage {
fun loadPayload(): String?
fun savePayload(payload: String)
}

View file

@ -151,7 +151,7 @@ object LibraryRepository {
TraktSettingsRepository.onProfileChanged()
if (!loadFromDisk(profileId)) return
TraktAuthRepository.onProfileChanged()
TraktAuthRepository.onProfileChanged(profileId)
TraktLibraryRepository.onProfileChanged()
if (TraktAuthRepository.isAuthenticated.value) {
TraktLibraryRepository.preloadListTabsAsync()

View file

@ -0,0 +1,169 @@
package com.nuvio.app.features.library
import androidx.compose.animation.core.tween
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.nuvio.app.core.i18n.localizedMediaTypeLabel
import com.nuvio.app.core.ui.NuvioDropdownChip
import com.nuvio.app.core.ui.NuvioDropdownOption
import com.nuvio.app.features.home.MetaPreview
import com.nuvio.app.features.home.components.PosterGridRow
import com.nuvio.app.features.home.components.PosterGridSkeletonRow
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.library_filter_all_types
import nuvio.composeapp.generated.resources.library_filter_list
import nuvio.composeapp.generated.resources.library_filter_sort
import nuvio.composeapp.generated.resources.library_filter_type
import nuvio.composeapp.generated.resources.library_sort_added_asc
import nuvio.composeapp.generated.resources.library_sort_added_desc
import nuvio.composeapp.generated.resources.library_sort_title_asc
import nuvio.composeapp.generated.resources.library_sort_title_desc
import nuvio.composeapp.generated.resources.library_sort_trakt_order
import org.jetbrains.compose.resources.stringResource
@Composable
internal fun LibrarySavedControls(
layoutMode: LibraryLayoutMode,
sourceMode: LibrarySourceMode,
sortOption: LibrarySortOption,
verticalProjection: LibraryVerticalProjection,
onSectionSelected: (String) -> Unit,
onTypeSelected: (String?) -> Unit,
onSortSelected: (LibrarySortOption) -> Unit,
modifier: Modifier = Modifier,
) {
val sortOptions = availableLibrarySortOptions(sourceMode)
val allTypesLabel = stringResource(Res.string.library_filter_all_types)
Row(
modifier = modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
if (layoutMode == LibraryLayoutMode.VERTICAL && sourceMode == LibrarySourceMode.TRAKT) {
val selectedSection = verticalProjection.availableSections
.firstOrNull { section -> section.type == verticalProjection.selectedSectionKey }
NuvioDropdownChip(
title = stringResource(Res.string.library_filter_list),
label = selectedSection?.displayTitle.orEmpty(),
selectedKey = verticalProjection.selectedSectionKey,
options = verticalProjection.availableSections.map { section ->
NuvioDropdownOption(key = section.type, label = section.displayTitle)
},
enabled = verticalProjection.availableSections.size > 1,
onSelected = { option -> onSectionSelected(option.key) },
)
}
if (layoutMode == LibraryLayoutMode.VERTICAL) {
val typeOptions = buildList {
add(NuvioDropdownOption(key = "", label = allTypesLabel))
addAll(
verticalProjection.availableTypes.map { type ->
NuvioDropdownOption(key = type, label = localizedMediaTypeLabel(type))
},
)
}
NuvioDropdownChip(
title = stringResource(Res.string.library_filter_type),
label = verticalProjection.selectedType
?.let(::localizedMediaTypeLabel)
?: allTypesLabel,
selectedKey = verticalProjection.selectedType.orEmpty(),
options = typeOptions,
enabled = typeOptions.size > 1,
onSelected = { option -> onTypeSelected(option.key.ifBlank { null }) },
)
}
NuvioDropdownChip(
title = stringResource(Res.string.library_filter_sort),
label = librarySortOptionLabel(sortOption),
selectedKey = sortOption.name,
options = sortOptions.map { option ->
NuvioDropdownOption(key = option.name, label = librarySortOptionLabel(option))
},
enabled = sortOptions.size > 1,
onSelected = { option ->
LibrarySortOption.entries
.firstOrNull { it.name == option.key }
?.let(onSortSelected)
},
)
}
}
internal fun LazyListScope.libraryVerticalContent(
projection: LibraryVerticalProjection,
columns: Int,
watchedKeys: Set<String>,
fullyWatchedSeriesKeys: Set<String>,
onPosterClick: ((LibraryItem) -> Unit)?,
onPosterLongClick: ((LibraryItem, LibrarySection) -> Unit)?,
) {
items(
items = projection.entries.chunked(columns),
key = { rowEntries ->
val firstEntry = rowEntries.first()
"library-vertical:${firstEntry.item.type}:${firstEntry.item.id}"
},
) { rowEntries ->
PosterGridRow(
items = rowEntries.map { entry -> entry.item.toMetaPreview() },
columns = columns,
modifier = libraryContentTransitionModifier()
.padding(horizontal = 16.dp),
watchedKeys = watchedKeys,
fullyWatchedSeriesKeys = fullyWatchedSeriesKeys,
onPosterClick = onPosterClick?.let { callback ->
{ preview -> rowEntries.findEntry(preview)?.item?.let(callback) }
},
onPosterLongClick = onPosterLongClick?.let { callback ->
{ preview ->
rowEntries.findEntry(preview)?.let { entry -> callback(entry.item, entry.section) }
}
},
)
}
}
internal fun LazyListScope.libraryVerticalSkeletonItems(columns: Int) {
items(
count = 2,
key = { index -> "library-vertical-skeleton:$index" },
) {
PosterGridSkeletonRow(
columns = columns,
modifier = libraryContentTransitionModifier()
.padding(horizontal = 16.dp),
)
}
}
internal fun LazyItemScope.libraryContentTransitionModifier(): Modifier =
Modifier.animateItem(
fadeInSpec = tween(durationMillis = 160),
placementSpec = tween(durationMillis = 180),
fadeOutSpec = tween(durationMillis = 90),
)
@Composable
private fun librarySortOptionLabel(option: LibrarySortOption): String =
when (option) {
LibrarySortOption.DEFAULT -> stringResource(Res.string.library_sort_trakt_order)
LibrarySortOption.ADDED_DESC -> stringResource(Res.string.library_sort_added_desc)
LibrarySortOption.ADDED_ASC -> stringResource(Res.string.library_sort_added_asc)
LibrarySortOption.TITLE_ASC -> stringResource(Res.string.library_sort_title_asc)
LibrarySortOption.TITLE_DESC -> stringResource(Res.string.library_sort_title_desc)
}
private fun List<LibraryVerticalEntry>.findEntry(preview: MetaPreview): LibraryVerticalEntry? =
firstOrNull { entry -> entry.item.id == preview.id && entry.item.type == preview.type }

View file

@ -1,5 +1,6 @@
package com.nuvio.app.features.library
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
@ -12,10 +13,12 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@ -28,13 +31,18 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.GridView
import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.ViewAgenda
import com.nuvio.app.core.ui.NuvioLoadingIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -77,6 +85,7 @@ import com.nuvio.app.features.home.HomeCatalogSettingsRepository
import com.nuvio.app.features.home.components.HomeEmptyStateCard
import com.nuvio.app.features.home.components.HomePosterCard
import com.nuvio.app.features.home.components.HomeSkeletonRow
import com.nuvio.app.features.home.components.posterGridColumnCountForWidth
import com.nuvio.app.features.profiles.ProfileRepository
import com.nuvio.app.features.watched.WatchedRepository
import com.nuvio.app.features.watching.application.WatchingState
@ -92,7 +101,7 @@ fun LibraryScreen(
scrollToTopRequests: Flow<Unit> = emptyFlow(),
onPosterClick: ((LibraryItem) -> Unit)? = null,
onPosterLongClick: ((LibraryItem, LibrarySection) -> Unit)? = null,
onSectionViewAllClick: ((LibrarySection) -> Unit)? = null,
onSectionViewAllClick: ((LibrarySection, LibrarySortOption) -> Unit)? = null,
onCloudFilePlay: ((CloudLibraryItem, CloudLibraryFile) -> Unit)? = null,
onConnectCloudClick: (() -> Unit)? = null,
) {
@ -109,6 +118,11 @@ fun LibraryScreen(
WatchedRepository.ensureLoaded()
WatchedRepository.uiState
}.collectAsStateWithLifecycle()
val fullyWatchedSeriesKeys by WatchedRepository.fullyWatchedSeriesKeys.collectAsStateWithLifecycle()
val displaySettings by remember {
LibraryDisplaySettingsRepository.ensureLoaded()
LibraryDisplaySettingsRepository.uiState
}.collectAsStateWithLifecycle()
val homeCatalogSettingsUiState by remember {
HomeCatalogSettingsRepository.snapshot()
HomeCatalogSettingsRepository.uiState
@ -121,13 +135,42 @@ fun LibraryScreen(
}
var selectedProviderId by rememberSaveable { mutableStateOf<String?>(null) }
var selectedTypeName by rememberSaveable { mutableStateOf<String?>(null) }
var cloudSearchQuery by rememberSaveable { mutableStateOf("") }
val selectedType = remember(selectedTypeName) {
selectedTypeName?.let { runCatching { CloudLibraryItemType.valueOf(it) }.getOrNull() }
}
var selectedCloudItemKey by rememberSaveable { mutableStateOf<String?>(null) }
var selectedLibrarySectionKey by rememberSaveable { mutableStateOf<String?>(null) }
var selectedLibraryType by rememberSaveable { mutableStateOf<String?>(null) }
val coroutineScope = rememberCoroutineScope()
val listState = rememberLazyListState()
val isTraktSource = uiState.sourceMode == LibrarySourceMode.TRAKT
val effectiveSortOption = effectiveLibrarySortOption(
selected = displaySettings.sortOption,
sourceMode = uiState.sourceMode,
)
val sortedSections = remember(uiState.sections, displaySettings.sortOption, uiState.sourceMode) {
sortLibrarySections(
sections = uiState.sections,
selected = displaySettings.sortOption,
sourceMode = uiState.sourceMode,
)
}
val verticalProjection = remember(
uiState.sections,
uiState.sourceMode,
selectedLibrarySectionKey,
selectedLibraryType,
displaySettings.sortOption,
) {
buildLibraryVerticalProjection(
sections = uiState.sections,
sourceMode = uiState.sourceMode,
selectedSectionKey = selectedLibrarySectionKey,
selectedType = selectedLibraryType,
sortOption = displaySettings.sortOption,
)
}
val retryLibraryLoad: () -> Unit = {
NetworkStatusRepository.requestRefresh(force = true)
coroutineScope.launch {
@ -174,151 +217,231 @@ fun LibraryScreen(
val disintegration = remember { LibraryDisintegrationHolder() }
val librarySectionsDisplay = if (
sourceMode != LibraryViewMode.Cloud && uiState.isLoaded && uiState.sections.isNotEmpty()
sourceMode != LibraryViewMode.Cloud &&
displaySettings.layoutMode == LibraryLayoutMode.HORIZONTAL &&
uiState.isLoaded &&
sortedSections.isNotEmpty()
) {
disintegration.sync(uiState.sections, LIBRARY_SECTION_PREVIEW_LIMIT)
disintegration.sync(sortedSections, LIBRARY_SECTION_PREVIEW_LIMIT)
} else {
disintegration.reset()
emptyList()
}
NuvioScreen(
modifier = modifier,
horizontalPadding = 0.dp,
listState = listState,
) {
stickyHeader {
Box(modifier = Modifier.fillMaxWidth()) {
Box(
modifier = Modifier
.matchParentSize()
.background(MaterialTheme.colorScheme.background)
.nuvioConsumePointerEvents(),
)
androidx.compose.foundation.layout.Column(
modifier = Modifier.fillMaxWidth(),
) {
NuvioScreenHeader(
title = if (sourceMode == LibraryViewMode.Cloud) {
stringResource(Res.string.library_title)
} else if (isTraktSource) {
stringResource(Res.string.library_trakt_title)
} else {
stringResource(Res.string.library_title)
},
modifier = Modifier.padding(horizontal = 16.dp),
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
val gridColumns = remember(maxWidth) { posterGridColumnCountForWidth(maxWidth) }
NuvioScreen(
modifier = Modifier.fillMaxSize(),
horizontalPadding = 0.dp,
listState = listState,
) {
stickyHeader {
Box(modifier = Modifier.fillMaxWidth()) {
Box(
modifier = Modifier
.matchParentSize()
.background(MaterialTheme.colorScheme.background)
.nuvioConsumePointerEvents(),
)
LibrarySourceSwitch(
selectedMode = sourceMode,
onModeSelected = { mode ->
sourceModeName = mode.name
},
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(6.dp))
androidx.compose.foundation.layout.Column(
modifier = Modifier.fillMaxWidth(),
) {
NuvioScreenHeader(
title = if (sourceMode == LibraryViewMode.Cloud) {
stringResource(Res.string.library_title)
} else if (isTraktSource) {
stringResource(Res.string.library_trakt_title)
} else {
stringResource(Res.string.library_title)
},
modifier = Modifier.padding(horizontal = 16.dp),
actions = {
if (sourceMode == LibraryViewMode.Saved) {
val targetLayout = if (displaySettings.layoutMode == LibraryLayoutMode.HORIZONTAL) {
LibraryLayoutMode.VERTICAL
} else {
LibraryLayoutMode.HORIZONTAL
}
IconButton(
onClick = {
LibraryDisplaySettingsRepository.setLayoutMode(targetLayout)
},
) {
Crossfade(
targetState = targetLayout,
animationSpec = tween(durationMillis = 140),
label = "libraryLayoutAction",
) { animatedTargetLayout ->
Icon(
imageVector = if (animatedTargetLayout == LibraryLayoutMode.VERTICAL) {
Icons.Rounded.GridView
} else {
Icons.Rounded.ViewAgenda
},
contentDescription = if (animatedTargetLayout == LibraryLayoutMode.VERTICAL) {
stringResource(Res.string.library_layout_show_vertical)
} else {
stringResource(Res.string.library_layout_show_horizontal)
},
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
},
)
LibrarySourceSwitch(
selectedMode = sourceMode,
onModeSelected = { mode ->
sourceModeName = mode.name
},
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(6.dp))
}
}
}
}
if (sourceMode == LibraryViewMode.Cloud) {
cloudLibraryContent(
uiState = cloudUiState,
selectedProviderId = selectedProviderId,
selectedType = selectedType,
selectedCloudItemKey = selectedCloudItemKey,
onProviderSelected = {
selectedProviderId = it
selectedTypeName = null
selectedCloudItemKey = null
},
onTypeSelected = {
selectedTypeName = it?.name
selectedCloudItemKey = null
},
onItemSelected = { item ->
val playableFiles = item.playableFiles
when {
playableFiles.size == 1 -> onCloudFilePlay?.invoke(item, playableFiles.first())
playableFiles.size > 1 -> selectedCloudItemKey = item.stableKey
}
},
onFileSelected = { item, file -> onCloudFilePlay?.invoke(item, file) },
onBackToItems = { selectedCloudItemKey = null },
onRefresh = { CloudLibraryRepository.refresh() },
onConnectCloudClick = onConnectCloudClick,
)
} else {
when {
!uiState.isLoaded || (uiState.isLoading && uiState.sections.isEmpty()) -> {
items(3) {
HomeSkeletonRow(
modifier = Modifier.padding(horizontal = 16.dp),
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
)
}
}
!uiState.errorMessage.isNullOrBlank() && uiState.sections.isEmpty() -> {
item {
if (networkStatusUiState.isOfflineLike) {
NuvioNetworkOfflineCard(
condition = networkStatusUiState.condition,
modifier = Modifier.padding(horizontal = 16.dp),
onRetry = retryLibraryLoad,
)
if (sourceMode == LibraryViewMode.Cloud) {
cloudLibraryContent(
uiState = cloudUiState,
selectedProviderId = selectedProviderId,
selectedType = selectedType,
selectedCloudItemKey = selectedCloudItemKey,
searchQuery = cloudSearchQuery,
onSearchQueryChange = {
cloudSearchQuery = it
selectedCloudItemKey = null
},
onProviderSelected = {
selectedProviderId = it
selectedTypeName = null
selectedCloudItemKey = null
},
onTypeSelected = {
selectedTypeName = it?.name
selectedCloudItemKey = null
},
onItemSelected = { item ->
val playableFiles = item.playableFiles
when {
playableFiles.size == 1 -> onCloudFilePlay?.invoke(item, playableFiles.first())
playableFiles.size > 1 -> selectedCloudItemKey = item.stableKey
}
},
onFileSelected = { item, file -> onCloudFilePlay?.invoke(item, file) },
onBackToItems = { selectedCloudItemKey = null },
onRefresh = { CloudLibraryRepository.refresh() },
onConnectCloudClick = onConnectCloudClick,
)
} else {
when {
!uiState.isLoaded || (uiState.isLoading && uiState.sections.isEmpty()) -> {
if (displaySettings.layoutMode == LibraryLayoutMode.VERTICAL) {
libraryVerticalSkeletonItems(gridColumns)
} else {
HomeEmptyStateCard(
modifier = Modifier.padding(horizontal = 16.dp),
title = if (isTraktSource) {
stringResource(Res.string.library_trakt_load_failed)
} else {
stringResource(Res.string.library_load_failed)
items(3) {
HomeSkeletonRow(
modifier = Modifier.padding(horizontal = 16.dp),
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
)
}
}
}
!uiState.errorMessage.isNullOrBlank() && uiState.sections.isEmpty() -> {
item {
if (networkStatusUiState.isOfflineLike) {
NuvioNetworkOfflineCard(
condition = networkStatusUiState.condition,
modifier = Modifier.padding(horizontal = 16.dp),
onRetry = retryLibraryLoad,
)
} else {
HomeEmptyStateCard(
modifier = Modifier.padding(horizontal = 16.dp),
title = if (isTraktSource) {
stringResource(Res.string.library_trakt_load_failed)
} else {
stringResource(Res.string.library_load_failed)
},
message = uiState.errorMessage.orEmpty(),
actionLabel = stringResource(Res.string.action_retry),
onActionClick = retryLibraryLoad,
)
}
}
}
uiState.sections.isEmpty() -> {
item {
if (networkStatusUiState.isOfflineLike && isTraktSource) {
NuvioNetworkOfflineCard(
condition = networkStatusUiState.condition,
modifier = Modifier.padding(horizontal = 16.dp),
onRetry = retryLibraryLoad,
)
} else {
HomeEmptyStateCard(
modifier = Modifier.padding(horizontal = 16.dp),
title = if (isTraktSource) {
stringResource(Res.string.library_trakt_empty_title)
} else {
stringResource(Res.string.library_empty_title)
},
message = if (isTraktSource) {
stringResource(Res.string.library_trakt_empty_message)
} else {
stringResource(Res.string.library_empty_message)
},
)
}
}
}
else -> {
item(
key = "library-saved-controls:${uiState.sourceMode}:" +
"${displaySettings.layoutMode}:$effectiveSortOption",
) {
LibrarySavedControls(
layoutMode = displaySettings.layoutMode,
sourceMode = uiState.sourceMode,
sortOption = effectiveSortOption,
verticalProjection = verticalProjection,
onSectionSelected = { sectionKey ->
selectedLibrarySectionKey = sectionKey
selectedLibraryType = null
},
message = uiState.errorMessage.orEmpty(),
actionLabel = stringResource(Res.string.action_retry),
onActionClick = retryLibraryLoad,
onTypeSelected = { type -> selectedLibraryType = type },
onSortSelected = LibraryDisplaySettingsRepository::setSortOption,
modifier = libraryContentTransitionModifier()
.padding(horizontal = 16.dp),
)
}
when (displaySettings.layoutMode) {
LibraryLayoutMode.HORIZONTAL -> librarySections(
displaySections = librarySectionsDisplay,
watchedKeys = watchedUiState.watchedKeys,
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
sortOption = effectiveSortOption,
onPosterClick = onPosterClick,
onSectionViewAllClick = onSectionViewAllClick,
onPosterLongClick = onPosterLongClick,
onDisintegrated = disintegration::onExited,
)
LibraryLayoutMode.VERTICAL -> libraryVerticalContent(
projection = verticalProjection,
columns = gridColumns,
watchedKeys = watchedUiState.watchedKeys,
fullyWatchedSeriesKeys = fullyWatchedSeriesKeys,
onPosterClick = onPosterClick,
onPosterLongClick = onPosterLongClick,
)
}
}
}
uiState.sections.isEmpty() -> {
item {
if (networkStatusUiState.isOfflineLike && isTraktSource) {
NuvioNetworkOfflineCard(
condition = networkStatusUiState.condition,
modifier = Modifier.padding(horizontal = 16.dp),
onRetry = retryLibraryLoad,
)
} else {
HomeEmptyStateCard(
modifier = Modifier.padding(horizontal = 16.dp),
title = if (isTraktSource) {
stringResource(Res.string.library_trakt_empty_title)
} else {
stringResource(Res.string.library_empty_title)
},
message = if (isTraktSource) {
stringResource(Res.string.library_trakt_empty_message)
} else {
stringResource(Res.string.library_empty_message)
},
)
}
}
}
else -> {
librarySections(
displaySections = librarySectionsDisplay,
watchedKeys = watchedUiState.watchedKeys,
showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline,
onPosterClick = onPosterClick,
onSectionViewAllClick = onSectionViewAllClick,
onPosterLongClick = onPosterLongClick,
onDisintegrated = disintegration::onExited,
)
}
}
}
}
@ -329,6 +452,8 @@ private fun LazyListScope.cloudLibraryContent(
selectedProviderId: String?,
selectedType: CloudLibraryItemType?,
selectedCloudItemKey: String?,
searchQuery: String,
onSearchQueryChange: (String) -> Unit,
onProviderSelected: (String?) -> Unit,
onTypeSelected: (CloudLibraryItemType?) -> Unit,
onItemSelected: (CloudLibraryItem) -> Unit,
@ -374,8 +499,19 @@ private fun LazyListScope.cloudLibraryContent(
.distinct()
.sortedBy { type -> type.ordinal }
val effectiveSelectedType = selectedType?.takeIf { type -> type in availableTypes }
val filteredItems = providerItems
val typeFilteredItems = providerItems
.filter { item -> effectiveSelectedType == null || item.type == effectiveSelectedType }
// Local filter over the already-loaded library. Matches the item name or any of its
// file names, since the useful identifier is often in the filename, not the title.
val trimmedQuery = searchQuery.trim()
val filteredItems = if (trimmedQuery.isEmpty()) {
typeFilteredItems
} else {
typeFilteredItems.filter { item ->
item.name.contains(trimmedQuery, ignoreCase = true) ||
item.files.any { file -> file.name.contains(trimmedQuery, ignoreCase = true) }
}
}
val selectedItem = filteredItems.firstOrNull { it.stableKey == selectedCloudItemKey }
if (selectedItem != null) {
@ -400,6 +536,14 @@ private fun LazyListScope.cloudLibraryContent(
)
}
item(key = "cloud-library-search") {
CloudLibrarySearchField(
query = searchQuery,
onQueryChange = onSearchQueryChange,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
uiState.providers
.filter { providerState -> selectedProviderId == null || providerState.providerId == selectedProviderId }
.filter { providerState -> !providerState.errorMessage.isNullOrBlank() && providerState.items.isEmpty() }
@ -443,6 +587,38 @@ private fun LazyListScope.cloudLibraryContent(
}
}
@Composable
private fun CloudLibrarySearchField(
query: String,
onQueryChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
OutlinedTextField(
value = query,
onValueChange = onQueryChange,
modifier = modifier.fillMaxWidth(),
singleLine = true,
shape = RoundedCornerShape(12.dp),
placeholder = { Text(stringResource(Res.string.cloud_library_search_label)) },
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = null,
)
},
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(onClick = { onQueryChange("") }) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = stringResource(Res.string.compose_search_clear),
)
}
}
},
)
}
private fun LazyListScope.cloudLibrarySkeletonItems() {
item(key = "cloud-library-skeleton-toolbar") {
CloudLibrarySkeletonToolbar(
@ -1000,24 +1176,26 @@ private fun LazyListScope.librarySections(
displaySections: List<LibraryDisplaySection>,
watchedKeys: Set<String>,
showHeaderAccent: Boolean,
sortOption: LibrarySortOption,
onPosterClick: ((LibraryItem) -> Unit)?,
onSectionViewAllClick: ((LibrarySection) -> Unit)?,
onSectionViewAllClick: ((LibrarySection, LibrarySortOption) -> Unit)?,
onPosterLongClick: ((LibraryItem, LibrarySection) -> Unit)?,
onDisintegrated: (String) -> Unit,
) {
items(
items = displaySections,
key = { section -> section.type },
key = { section -> "library-horizontal:${section.type}" },
) { section ->
NuvioShelfSection(
title = section.displayTitle,
entries = section.previewEntries,
modifier = libraryContentTransitionModifier(),
headerHorizontalPadding = 16.dp,
rowContentPadding = PaddingValues(horizontal = 16.dp),
showHeaderAccent = showHeaderAccent,
onViewAllClick = section.source
?.takeIf { it.items.size > LIBRARY_SECTION_PREVIEW_LIMIT }
?.let { source -> onSectionViewAllClick?.let { { it(source) } } },
?.let { source -> onSectionViewAllClick?.let { { it(source, sortOption) } } },
viewAllPillSize = NuvioViewAllPillSize.Compact,
key = { entry -> entry.globalKey },
animatePlacement = true,

View file

@ -23,6 +23,7 @@ object MdbListMetadataService {
const val PROVIDER_TRAKT = "trakt"
const val PROVIDER_LETTERBOXD = "letterboxd"
const val PROVIDER_AUDIENCE = "audience"
const val PROVIDER_MAL = "mal"
val PROVIDER_PRIORITY_ORDER = listOf(
PROVIDER_IMDB,
@ -32,6 +33,7 @@ object MdbListMetadataService {
PROVIDER_TRAKT,
PROVIDER_LETTERBOXD,
PROVIDER_AUDIENCE,
PROVIDER_MAL,
)
private val log = Logger.withTag("MdbListMetadata")

View file

@ -10,6 +10,7 @@ data class MdbListSettings(
val useTrakt: Boolean = true,
val useLetterboxd: Boolean = true,
val useAudience: Boolean = true,
val useMal: Boolean = true,
) {
val hasApiKey: Boolean
get() = apiKey.isNotBlank()
@ -23,6 +24,7 @@ data class MdbListSettings(
MdbListMetadataService.PROVIDER_TRAKT -> useTrakt
MdbListMetadataService.PROVIDER_LETTERBOXD -> useLetterboxd
MdbListMetadataService.PROVIDER_AUDIENCE -> useAudience
MdbListMetadataService.PROVIDER_MAL -> useMal
else -> false
}

View file

@ -19,6 +19,7 @@ object MdbListSettingsRepository {
private var useTrakt = true
private var useLetterboxd = true
private var useAudience = true
private var useMal = true
fun ensureLoaded() {
if (hasLoaded) return
@ -88,6 +89,10 @@ object MdbListSettingsRepository {
useAudience = value
MdbListSettingsStorage.saveUseAudience(value)
} else return
MdbListMetadataService.PROVIDER_MAL -> if (useMal != value) {
useMal = value
MdbListSettingsStorage.saveUseMal(value)
} else return
else -> return
}
publish()
@ -105,6 +110,7 @@ object MdbListSettingsRepository {
useTrakt = MdbListSettingsStorage.loadUseTrakt() ?: true
useLetterboxd = MdbListSettingsStorage.loadUseLetterboxd() ?: true
useAudience = MdbListSettingsStorage.loadUseAudience() ?: true
useMal = MdbListSettingsStorage.loadUseMal() ?: true
publish()
}
@ -119,6 +125,7 @@ object MdbListSettingsRepository {
useTrakt = useTrakt,
useLetterboxd = useLetterboxd,
useAudience = useAudience,
useMal = useMal,
)
}
}

View file

@ -21,6 +21,8 @@ internal expect object MdbListSettingsStorage {
fun saveUseLetterboxd(enabled: Boolean)
fun loadUseAudience(): Boolean?
fun saveUseAudience(enabled: Boolean)
fun loadUseMal(): Boolean?
fun saveUseMal(enabled: Boolean)
fun exportToSyncPayload(): JsonObject
fun replaceFromSyncPayload(payload: JsonObject)
}

View file

@ -8,7 +8,35 @@ import kotlinx.coroutines.flow.asStateFlow
data class P2pSettingsUiState(
val p2pEnabled: Boolean = false,
val enableUpload: Boolean = true,
val hideTorrentStats: Boolean = true,
val hideTorrentStats: Boolean = false,
val torrentProfile: P2pTorrentProfile = P2pTorrentProfile.BALANCED,
val cacheSize: P2pCacheSize = P2pCacheSize.GB_2,
)
enum class P2pTorrentProfile {
SOFT,
BALANCED,
FAST,
}
enum class P2pCacheSize(val bytes: Long) {
NONE(0L),
GB_2(2L * 1024L * 1024L * 1024L),
GB_5(5L * 1024L * 1024L * 1024L),
GB_10(10L * 1024L * 1024L * 1024L),
}
data class P2pCacheUiState(
val usedBytes: Long = 0L,
val protectedBytes: Long = 0L,
val isClearing: Boolean = false,
val hasMeasurement: Boolean = false,
)
data class P2pCacheClearResult(
val reclaimedBytes: Long,
val remainingBytes: Long,
val protectedBytes: Long,
)
object P2pSettingsRepository {
@ -21,7 +49,9 @@ object P2pSettingsRepository {
private var hasLoaded = false
private var p2pEnabled = false
private var enableUpload = true
private var hideTorrentStats = true
private var hideTorrentStats = false
private var torrentProfile = P2pTorrentProfile.BALANCED
private var cacheSize = P2pCacheSize.GB_2
fun ensureLoaded() {
if (hasLoaded) return
@ -36,7 +66,9 @@ object P2pSettingsRepository {
hasLoaded = false
p2pEnabled = false
enableUpload = true
hideTorrentStats = true
hideTorrentStats = false
torrentProfile = P2pTorrentProfile.BALANCED
cacheSize = P2pCacheSize.GB_2
publish()
}
@ -64,11 +96,33 @@ object P2pSettingsRepository {
publish()
}
fun setTorrentProfile(profile: P2pTorrentProfile) {
ensureLoaded()
if (torrentProfile == profile) return
torrentProfile = profile
P2pSettingsStorage.saveTorrentProfile(profile.name)
publish()
}
fun setCacheSize(size: P2pCacheSize) {
ensureLoaded()
if (cacheSize == size) return
cacheSize = size
P2pSettingsStorage.saveCacheSize(size.name)
publish()
}
private fun loadFromDisk() {
hasLoaded = true
p2pEnabled = P2pSettingsStorage.loadP2pEnabled() ?: false
enableUpload = P2pSettingsStorage.loadEnableUpload() ?: true
hideTorrentStats = P2pSettingsStorage.loadHideTorrentStats() ?: true
hideTorrentStats = P2pSettingsStorage.loadHideTorrentStats() ?: false
torrentProfile = P2pSettingsStorage.loadTorrentProfile()
?.let { stored -> P2pTorrentProfile.entries.firstOrNull { it.name == stored } }
?: P2pTorrentProfile.BALANCED
cacheSize = P2pSettingsStorage.loadCacheSize()
?.let { stored -> P2pCacheSize.entries.firstOrNull { it.name == stored } }
?: P2pCacheSize.GB_2
publish()
}
@ -77,6 +131,8 @@ object P2pSettingsRepository {
p2pEnabled = p2pEnabled,
enableUpload = enableUpload,
hideTorrentStats = hideTorrentStats,
torrentProfile = torrentProfile,
cacheSize = cacheSize,
)
}
}
@ -88,6 +144,10 @@ internal expect object P2pSettingsStorage {
fun saveEnableUpload(enabled: Boolean)
fun loadHideTorrentStats(): Boolean?
fun saveHideTorrentStats(enabled: Boolean)
fun loadTorrentProfile(): String?
fun saveTorrentProfile(profile: String)
fun loadCacheSize(): String?
fun saveCacheSize(size: String)
}
data class P2pStreamRequest(
@ -97,9 +157,56 @@ data class P2pStreamRequest(
val trackers: List<String> = emptyList(),
)
internal fun canonicalP2pInfoHash(infoHash: String): String {
val canonical = infoHash.trim().lowercase()
require((canonical.length == 40 || canonical.length == 64) &&
canonical.all { it in '0'..'9' || it in 'a'..'f' }) {
"Torrent info hash must be 40 or 64 hexadecimal characters"
}
return canonical
}
internal fun buildP2pMagnetUri(infoHash: String, trackers: List<String>): String {
val canonicalHash = canonicalP2pInfoHash(infoHash)
val topic = if (canonicalHash.length == 40) {
"urn:btih:$canonicalHash"
} else {
"urn:btmh:1220$canonicalHash"
}
val trackerParameters = trackers.filter(String::isNotBlank).distinct().joinToString("") { tracker ->
"&tr=${tracker.encodeP2pQueryValue()}"
}
return "magnet:?xt=$topic$trackerParameters"
}
private fun String.encodeP2pQueryValue(): String = buildString {
for (byte in this@encodeP2pQueryValue.encodeToByteArray()) {
val value = byte.toInt() and 0xff
if ((value in 'a'.code..'z'.code) ||
(value in 'A'.code..'Z'.code) ||
(value in '0'.code..'9'.code) ||
value == '-'.code || value == '.'.code || value == '_'.code || value == '~'.code) {
append(value.toChar())
} else {
append('%')
append(HEX_DIGITS[value ushr 4])
append(HEX_DIGITS[value and 0x0f])
}
}
}
private const val HEX_DIGITS = "0123456789ABCDEF"
sealed class P2pStreamingState {
data object Idle : P2pStreamingState()
data object Connecting : P2pStreamingState()
data class Connecting(
val phase: String = "starting_engine",
val downloadSpeed: Long = 0L,
val uploadSpeed: Long = 0L,
val peers: Int = 0,
val seeds: Int = 0,
) : P2pStreamingState()
data class Streaming(
val localUrl: String,
@ -109,7 +216,9 @@ sealed class P2pStreamingState {
val seeds: Int,
val bufferProgress: Float,
val totalProgress: Float,
val preloadedBytes: Long = 0L,
val downloadedBytes: Long = 0L,
val verifiedBytes: Long = 0L,
val deliveredBytes: Long = 0L,
) : P2pStreamingState()
data class Error(val message: String) : P2pStreamingState()
@ -119,7 +228,9 @@ class P2pStreamingException(message: String) : Exception(message)
expect object P2pStreamingEngine {
val state: StateFlow<P2pStreamingState>
val cacheState: StateFlow<P2pCacheUiState>
suspend fun startStream(request: P2pStreamRequest): String
suspend fun clearCache(): P2pCacheClearResult
fun stopStream()
fun shutdown()
}

View file

@ -65,8 +65,11 @@ expect fun PlatformPlayerSurface(
useYoutubeChunkedPlayback: Boolean = false,
modifier: Modifier = Modifier,
playWhenReady: Boolean = true,
initialPositionMs: Long? = null,
initialPositionRequestKey: String? = null,
resizeMode: PlayerResizeMode = PlayerResizeMode.Fit,
useNativeController: Boolean = false,
onInitialPositionHandled: (key: String, handled: Boolean) -> Unit = { _, _ -> },
onControllerReady: (PlayerEngineController) -> Unit,
onSnapshot: (PlayerPlaybackSnapshot) -> Unit,
onError: (String?) -> Unit,

View file

@ -1,6 +1,5 @@
package com.nuvio.app.features.player
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
@ -247,30 +246,23 @@ internal fun OpeningOverlay(
val showHorizontalProgress = progressActive && logo == null
if (!message.isNullOrBlank() || showHorizontalProgress) {
Spacer(modifier = Modifier.height(16.dp))
Crossfade(
targetState = message?.takeIf { it.isNotBlank() },
animationSpec = tween(durationMillis = 260),
label = "openingOverlayMessageCrossfade",
Box(
modifier = Modifier
.fillMaxWidth()
.height(40.dp),
) { loadingMessage ->
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
if (loadingMessage != null) {
Text(
text = loadingMessage,
color = Color.White.copy(alpha = 0.72f),
textAlign = TextAlign.Center,
maxLines = 2,
style = MaterialTheme.typography.labelMedium,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
)
}
contentAlignment = Alignment.Center,
) {
message?.takeIf { it.isNotBlank() }?.let { loadingMessage ->
Text(
text = loadingMessage,
color = Color.White.copy(alpha = 0.72f),
textAlign = TextAlign.Center,
maxLines = 2,
style = MaterialTheme.typography.labelMedium,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
)
}
}
if (showHorizontalProgress) {

View file

@ -99,6 +99,8 @@ internal fun PlayerScreenRuntime.BindPlayerRuntimeEffects() {
return@LaunchedEffect
}
if (!P2pSettingsRepository.isVisible || !p2pSettingsUiState.p2pEnabled) {
p2pResolvedSourceUrl = null
P2pStreamingEngine.stopStream()
return@LaunchedEffect
}
@ -142,6 +144,11 @@ internal fun PlayerScreenRuntime.BindPlayerRuntimeEffects() {
LaunchedEffect(p2pStreamingState, activeTorrentInfoHash) {
val state = p2pStreamingState
if (activeTorrentInfoHash != null && state is P2pStreamingState.Error) {
p2pResolvedSourceUrl = null
playerController = null
playerControllerSourceUrl = null
playbackSnapshot = PlayerPlaybackSnapshot()
initialLoadCompleted = true
errorMessage = getString(Res.string.player_error_torrent, state.message)
controlsVisible = !playerControlsLocked
}

View file

@ -64,8 +64,8 @@ internal class PlayerScreenRuntime(
lateinit var hapticFeedback: HapticFeedback
var playerSettingsUiState: PlayerSettingsUiState = PlayerSettingsUiState()
var p2pSettingsUiState: P2pSettingsUiState = P2pSettingsUiState()
var p2pStreamingState: P2pStreamingState = P2pStreamingState.Idle
var p2pSettingsUiState by mutableStateOf(P2pSettingsUiState())
var p2pStreamingState by mutableStateOf<P2pStreamingState>(P2pStreamingState.Idle)
var metaScreenSettingsUiState: MetaScreenSettingsUiState = MetaScreenSettingsUiState()
var watchedUiState: WatchedUiState = WatchedUiState()
var watchProgressUiState: WatchProgressUiState = WatchProgressUiState()

View file

@ -25,6 +25,7 @@ internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() {
val isEpisode = activeSeasonNumber != null && activeEpisodeNumber != null
val currentGestureFeedback = liveGestureFeedback ?: gestureFeedback
val isP2pPlaybackActive = activeTorrentInfoHash != null
val p2pConnecting = p2pStreamingState as? P2pStreamingState.Connecting
val p2pStats = p2pStreamingState as? P2pStreamingState.Streaming
val p2pPeerInfo = p2pStats?.let { stats ->
org.jetbrains.compose.resources.stringResource(
@ -34,20 +35,35 @@ internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() {
)
}
val p2pDownloadSpeed = p2pStats?.let { formatP2pSpeed(it.downloadSpeed) }
val p2pLoadingBytes = p2pStats?.let { maxOf(it.downloadedBytes, it.deliveredBytes) } ?: 0L
val connectingPeerInfo = p2pConnecting?.let { state ->
org.jetbrains.compose.resources.stringResource(
nuvio.composeapp.generated.resources.Res.string.player_torrent_peer_info,
state.seeds,
state.peers,
)
}
val p2pInitialLoadingMessage = when {
!isP2pPlaybackActive || initialLoadCompleted -> null
p2pStreamingState is P2pStreamingState.Connecting -> {
org.jetbrains.compose.resources.stringResource(
nuvio.composeapp.generated.resources.Res.string.player_torrent_connecting_peers,
)
p2pConnecting != null -> {
if (p2pSettingsUiState.hideTorrentStats) {
p2pConnectingPhaseLabel(p2pConnecting.phase)
} else {
org.jetbrains.compose.resources.stringResource(
nuvio.composeapp.generated.resources.Res.string.player_torrent_connecting_status,
p2pConnectingPhaseLabel(p2pConnecting.phase),
connectingPeerInfo.orEmpty(),
formatP2pSpeed(p2pConnecting.downloadSpeed),
)
}
}
p2pStats != null -> {
if (p2pSettingsUiState.hideTorrentStats) {
null
} else {
org.jetbrains.compose.resources.stringResource(
nuvio.composeapp.generated.resources.Res.string.player_torrent_buffered_status,
formatP2pMegabytes(p2pStats.preloadedBytes),
nuvio.composeapp.generated.resources.Res.string.player_torrent_loading_status,
formatP2pMegabytes(p2pLoadingBytes),
p2pPeerInfo.orEmpty(),
p2pDownloadSpeed.orEmpty(),
)
@ -57,9 +73,15 @@ internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() {
nuvio.composeapp.generated.resources.Res.string.player_torrent_starting_engine,
)
}
val bufferedAheadMs = (playbackSnapshot.bufferedPositionMs - playbackSnapshot.positionMs)
.coerceAtLeast(0L)
val p2pInitialLoadingProgress = when {
!isP2pPlaybackActive || initialLoadCompleted || p2pStats == null -> null
else -> (p2pStats.preloadedBytes.toFloat() / P2pInitialPreloadTargetBytes.toFloat()).coerceIn(0f, 1f)
else -> p2pInitialLoadingProgress(
bufferedAheadMs = bufferedAheadMs,
downloadedBytes = p2pStats.downloadedBytes,
deliveredBytes = p2pStats.deliveredBytes,
)
}
val showP2pRebufferStats = isP2pPlaybackActive &&
initialLoadCompleted &&
@ -116,6 +138,7 @@ internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() {
),
) {
val playerSurfaceSourceUrl = if (isP2pPlaybackActive) p2pResolvedSourceUrl else activeSourceUrl
val initialPositionRequestKey = currentInitialPositionRequestKey()
if (playerSurfaceSourceUrl != null) {
PlatformPlayerSurface(
sourceUrl = playerSurfaceSourceUrl,
@ -126,7 +149,14 @@ internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() {
streamType = activeStreamType,
modifier = Modifier.fillMaxSize(),
playWhenReady = shouldPlay,
initialPositionMs = activeInitialPositionMs.takeIf { it > 0L },
initialPositionRequestKey = initialPositionRequestKey,
resizeMode = resizeMode,
onInitialPositionHandled = { key, handled ->
if (key == currentInitialPositionRequestKey()) {
initialSeekApplied = handled
}
},
onControllerReady = { controller ->
playerController = controller
playerControllerSourceUrl = activeSourceUrl
@ -187,6 +217,24 @@ internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() {
}
}
@Composable
private fun p2pConnectingPhaseLabel(phase: String): String = when (phase) {
"add_magnet" -> org.jetbrains.compose.resources.stringResource(
nuvio.composeapp.generated.resources.Res.string.player_torrent_fetching_metadata,
)
"prepare_stream", "attach_route" -> org.jetbrains.compose.resources.stringResource(
nuvio.composeapp.generated.resources.Res.string.player_torrent_preparing_stream,
)
else -> org.jetbrains.compose.resources.stringResource(
nuvio.composeapp.generated.resources.Res.string.player_torrent_starting_engine,
)
}
private fun PlayerScreenRuntime.currentInitialPositionRequestKey(): String? {
val positionMs = activeInitialPositionMs.takeIf { it > 0L } ?: return null
return "$activePlaybackIdentity:${activeVideoId.orEmpty()}:$positionMs"
}
@Composable
private fun PlayerScreenRuntime.RenderPlayerControls(displayedPositionMs: Long, isEpisode: Boolean) {
val isInPip = rememberIsInPictureInPicture()

View file

@ -15,9 +15,39 @@ internal const val PlayerVerticalGestureTouchSlopMultiplier = 3f
internal const val PlayerVerticalGestureMinHeightFraction = 0.06f
internal const val PlayerVerticalGestureDominanceRatio = 1.2f
internal const val PlayerSeekProgressSyncDebounceMs = 700L
internal const val P2pInitialPreloadTargetBytes = 5_242_880L
internal const val P2pInitialByteProgressMidpoint = 5_242_880L
internal const val P2pInitialBufferTargetMs = 10_000L
internal const val P2pInitialNetworkStageWeight = 0.45f
internal const val P2pInitialDeliveryStageWeight = 0.30f
internal const val P2pInitialPlayerStageStart = 0.75f
internal const val P2pInitialLoadingMaximum = 0.95f
internal const val NEXT_EPISODE_HARD_TIMEOUT_MS = 120_000L
internal fun p2pInitialLoadingProgress(
bufferedAheadMs: Long,
downloadedBytes: Long,
deliveredBytes: Long,
): Float {
val networkProgress = saturatingProgress(downloadedBytes, P2pInitialByteProgressMidpoint) *
P2pInitialNetworkStageWeight
val deliveryProgress = saturatingProgress(deliveredBytes, P2pInitialByteProgressMidpoint) *
P2pInitialDeliveryStageWeight
val engineProgress = networkProgress + deliveryProgress
val playerProgress = if (bufferedAheadMs > 0L) {
P2pInitialPlayerStageStart +
(bufferedAheadMs.toFloat() / P2pInitialBufferTargetMs.toFloat()).coerceIn(0f, 1f) *
(P2pInitialLoadingMaximum - P2pInitialPlayerStageStart)
} else {
0f
}
return maxOf(engineProgress, playerProgress).coerceIn(0f, P2pInitialLoadingMaximum)
}
private fun saturatingProgress(value: Long, midpoint: Long): Float {
val safeValue = value.coerceAtLeast(0L).toDouble()
return (safeValue / (safeValue + midpoint.toDouble())).toFloat()
}
internal val PlayerSideGestureSystemEdgeExclusion = 72.dp
internal val PlayerSliderOverlayGap = 12.dp
internal val PlayerTimeRowHeight = 36.dp

View file

@ -16,6 +16,7 @@ import com.nuvio.app.features.home.HomeRepository
import com.nuvio.app.core.ui.CardDepthStyleRepository
import com.nuvio.app.core.ui.PosterCardStyleRepository
import com.nuvio.app.features.library.LibraryRepository
import com.nuvio.app.features.library.LibraryDisplaySettingsRepository
import com.nuvio.app.features.mdblist.MdbListSettingsRepository
import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsRepository
import com.nuvio.app.features.p2p.P2pSettingsRepository
@ -154,8 +155,9 @@ object ProfileRepository {
persist()
WatchedRepository.onProfileChanged(profileIndex)
TraktSettingsRepository.onProfileChanged()
TraktAuthRepository.onProfileChanged()
TraktAuthRepository.onProfileChanged(profileIndex)
LibraryRepository.onProfileChanged(profileIndex)
LibraryDisplaySettingsRepository.onProfileChanged()
WatchProgressRepository.onProfileChanged(profileIndex)
AddonRepository.onProfileChanged(profileIndex)
if (com.nuvio.app.core.build.AppFeaturePolicy.pluginsEnabled) {

View file

@ -1,48 +1,32 @@
package com.nuvio.app.features.search
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import com.nuvio.app.core.ui.NuvioLoadingIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.nuvio.app.core.network.NetworkCondition
import com.nuvio.app.core.format.formatReleaseDateForDisplay
import com.nuvio.app.core.ui.NuvioDropdownChip
import com.nuvio.app.core.ui.NuvioDropdownOption
import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
import com.nuvio.app.core.ui.NuvioPosterWatchedOverlay
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
import com.nuvio.app.core.ui.posterCardClickable
import com.nuvio.app.features.home.MetaPreview
import com.nuvio.app.features.home.PosterShape
import com.nuvio.app.features.home.components.PosterGridRow
import com.nuvio.app.features.home.components.PosterGridSkeletonRow
import com.nuvio.app.features.home.components.HomeEmptyStateCard
import com.nuvio.app.features.watching.application.WatchingState
import nuvio.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@ -92,7 +76,7 @@ internal fun LazyListScope.discoverContent(
when {
state.isLoading && state.items.isEmpty() -> {
items(2) {
DiscoverSkeletonRow(
PosterGridSkeletonRow(
columns = columns,
modifier = Modifier.padding(horizontal = 16.dp),
)
@ -113,7 +97,7 @@ internal fun LazyListScope.discoverContent(
else -> {
items(state.items.chunked(columns)) { rowItems ->
DiscoverGridRow(
PosterGridRow(
items = rowItems,
columns = columns,
modifier = Modifier.padding(horizontal = 16.dp),
@ -193,129 +177,6 @@ private fun DiscoverFilterRow(
}
}
@Composable
private fun DiscoverGridRow(
items: List<MetaPreview>,
columns: Int,
modifier: Modifier = Modifier,
watchedKeys: Set<String> = emptySet(),
fullyWatchedSeriesKeys: Set<String> = emptySet(),
onPosterClick: ((MetaPreview) -> Unit)? = null,
onPosterLongClick: ((MetaPreview) -> Unit)? = null,
) {
val posterCardStyle = rememberPosterCardStyleUiState()
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.Top,
) {
items.forEach { item ->
DiscoverPosterTile(
item = item,
cornerRadiusDp = posterCardStyle.cornerRadiusDp,
hideLabels = posterCardStyle.hideLabelsEnabled,
modifier = Modifier.weight(1f),
isWatched = WatchingState.isPosterWatched(
watchedKeys = watchedKeys,
item = item,
fullyWatchedSeriesKeys = fullyWatchedSeriesKeys,
),
onClick = onPosterClick?.let { { it(item) } },
onLongClick = onPosterLongClick?.let { { it(item) } },
)
}
repeat(columns - items.size) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun DiscoverPosterTile(
item: MetaPreview,
cornerRadiusDp: Int,
hideLabels: Boolean,
modifier: Modifier = Modifier,
isWatched: Boolean = false,
onClick: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(item.posterShape.discoverAspectRatio())
.clip(RoundedCornerShape(cornerRadiusDp.dp))
.background(MaterialTheme.colorScheme.surface)
.posterCardClickable(
onClick = onClick,
onLongClick = onLongClick,
zoomImageUrl = item.poster,
zoomCornerRadius = cornerRadiusDp.dp,
),
) {
if (item.poster != null) {
AsyncImage(
model = item.poster,
contentDescription = item.name,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop,
)
}
NuvioPosterWatchedOverlay(isWatched = isWatched)
}
if (!hideLabels) {
Text(
text = item.name,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onBackground,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
val detail = item.releaseInfo?.let { formatReleaseDateForDisplay(it) }
if (detail != null) {
Text(
text = detail,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} else {
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@Composable
private fun DiscoverSkeletonRow(
columns: Int,
modifier: Modifier = Modifier,
) {
val posterCardStyle = rememberPosterCardStyleUiState()
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
repeat(columns) {
Box(
modifier = Modifier
.weight(1f)
.aspectRatio(0.68f)
.clip(RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp))
.background(MaterialTheme.colorScheme.surface),
)
}
}
}
@Composable
private fun CatalogLoadingFooter(modifier: Modifier = Modifier) {
Box(
@ -390,10 +251,3 @@ private fun String.displayTypeLabel(): String =
"tv" -> stringResource(Res.string.media_tv)
else -> replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
}
private fun PosterShape.discoverAspectRatio(): Float =
when (this) {
PosterShape.Poster -> 0.68f
PosterShape.Square -> 1f
PosterShape.Landscape -> 1.2f
}

View file

@ -35,7 +35,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.Alignment
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -56,6 +55,7 @@ import com.nuvio.app.features.home.components.HomeCatalogRowSection
import com.nuvio.app.features.home.components.HomeEmptyStateCard
import com.nuvio.app.features.home.components.homeSectionHorizontalPaddingForWidth
import com.nuvio.app.features.home.components.HomeSkeletonRow
import com.nuvio.app.features.home.components.posterGridColumnCountForWidth
import com.nuvio.app.features.watched.WatchedRepository
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@ -226,7 +226,7 @@ fun SearchScreen(
modifier = modifier.fillMaxSize(),
) {
val discoverColumns = remember(maxWidth) {
discoverColumnCountForWidth(maxWidth)
posterGridColumnCountForWidth(maxWidth)
}
val homeSectionPadding = remember(maxWidth) {
homeSectionHorizontalPaddingForWidth(maxWidth.value)
@ -382,15 +382,6 @@ fun SearchScreen(
}
}
private fun discoverColumnCountForWidth(screenWidth: Dp): Int =
when {
screenWidth >= 1400.dp -> 7
screenWidth >= 1200.dp -> 6
screenWidth >= 1000.dp -> 5
screenWidth >= 840.dp -> 4
else -> 3
}
@Composable
private fun SearchEmptyStateCard(
reason: SearchEmptyStateReason?,

View file

@ -64,8 +64,10 @@ import nuvio.composeapp.generated.resources.settings_appearance_continue_watchin
import nuvio.composeapp.generated.resources.settings_appearance_liquid_glass
import nuvio.composeapp.generated.resources.settings_appearance_liquid_glass_description
import nuvio.composeapp.generated.resources.settings_appearance_poster_customization_description
import nuvio.composeapp.generated.resources.settings_appearance_section_detail_page
import nuvio.composeapp.generated.resources.settings_appearance_section_display
import nuvio.composeapp.generated.resources.settings_appearance_section_home
import nuvio.composeapp.generated.resources.settings_appearance_section_streams
import nuvio.composeapp.generated.resources.settings_appearance_section_theme
import nuvio.composeapp.generated.resources.settings_content_discovery_collections_description
import nuvio.composeapp.generated.resources.settings_content_discovery_homescreen_description
@ -252,7 +254,7 @@ internal fun LazyListScope.appearanceSettingsContent(
}
item {
SettingsSection(
title = stringResource(Res.string.compose_settings_page_streams),
title = stringResource(Res.string.settings_appearance_section_streams),
isTablet = isTablet,
) {
SettingsGroup(isTablet = isTablet) {
@ -267,7 +269,7 @@ internal fun LazyListScope.appearanceSettingsContent(
}
item {
SettingsSection(
title = stringResource(Res.string.compose_settings_page_meta_screen),
title = stringResource(Res.string.settings_appearance_section_detail_page),
isTablet = isTablet,
) {
SettingsGroup(isTablet = isTablet) {

View file

@ -54,6 +54,8 @@ import nuvio.composeapp.generated.resources.settings_continue_watching_sort_mode
import nuvio.composeapp.generated.resources.settings_continue_watching_sort_mode_default_desc
import nuvio.composeapp.generated.resources.settings_continue_watching_sort_mode_streaming
import nuvio.composeapp.generated.resources.settings_continue_watching_sort_mode_streaming_desc
import nuvio.composeapp.generated.resources.settings_continue_watching_sort_mode_split_upcoming
import nuvio.composeapp.generated.resources.settings_continue_watching_sort_mode_split_upcoming_desc
import nuvio.composeapp.generated.resources.settings_continue_watching_sort_mode_title
import nuvio.composeapp.generated.resources.settings_continue_watching_style_card
import nuvio.composeapp.generated.resources.settings_continue_watching_style_card_description
@ -176,6 +178,7 @@ internal fun LazyListScope.continueWatchingSettingsContent(
when (sortMode) {
ContinueWatchingSortMode.DEFAULT -> Res.string.settings_continue_watching_sort_mode_default
ContinueWatchingSortMode.STREAMING_STYLE -> Res.string.settings_continue_watching_sort_mode_streaming
ContinueWatchingSortMode.SPLIT_UPCOMING -> Res.string.settings_continue_watching_sort_mode_split_upcoming
}
)
SettingsNavigationRow(
@ -325,6 +328,11 @@ private fun ContinueWatchingSortModeDialog(
Res.string.settings_continue_watching_sort_mode_streaming,
Res.string.settings_continue_watching_sort_mode_streaming_desc,
),
Triple(
ContinueWatchingSortMode.SPLIT_UPCOMING,
Res.string.settings_continue_watching_sort_mode_split_upcoming,
Res.string.settings_continue_watching_sort_mode_split_upcoming_desc,
),
)
BasicAlertDialog(

View file

@ -34,6 +34,7 @@ import nuvio.composeapp.generated.resources.settings_mdb_section_title
import nuvio.composeapp.generated.resources.source_audience_score
import nuvio.composeapp.generated.resources.source_imdb
import nuvio.composeapp.generated.resources.source_letterboxd
import nuvio.composeapp.generated.resources.source_mal
import nuvio.composeapp.generated.resources.source_metacritic
import nuvio.composeapp.generated.resources.source_rotten_tomatoes
import nuvio.composeapp.generated.resources.source_tmdb
@ -117,6 +118,7 @@ private fun ProviderRows(
MdbListMetadataService.PROVIDER_TRAKT to Res.string.source_trakt,
MdbListMetadataService.PROVIDER_LETTERBOXD to Res.string.source_letterboxd,
MdbListMetadataService.PROVIDER_AUDIENCE to Res.string.source_audience_score,
MdbListMetadataService.PROVIDER_MAL to Res.string.source_mal,
)
providers.forEachIndexed { index, (providerId, providerLabelRes) ->

View file

@ -75,7 +75,12 @@ import com.nuvio.app.features.player.formatPlaybackSpeedLabel
import com.nuvio.app.features.player.languageLabelForCode
import com.nuvio.app.features.player.toStorageHexString
import com.nuvio.app.features.p2p.P2pConsentDialog
import com.nuvio.app.features.p2p.P2pCacheClearResult
import com.nuvio.app.features.p2p.P2pCacheSize
import com.nuvio.app.features.p2p.P2pSettingsRepository
import com.nuvio.app.features.p2p.P2pStreamingEngine
import com.nuvio.app.features.p2p.P2pStreamingState
import com.nuvio.app.features.p2p.P2pTorrentProfile
import com.nuvio.app.features.plugins.PluginsUiState
import com.nuvio.app.features.plugins.PluginRepository
import com.nuvio.app.features.streams.StreamAutoPlayMode
@ -144,6 +149,31 @@ private fun formatStep(value: Float): String {
}
}
@Composable
private fun p2pProfileLabel(profile: P2pTorrentProfile): String = when (profile) {
P2pTorrentProfile.SOFT -> stringResource(Res.string.settings_p2p_profile_soft)
P2pTorrentProfile.BALANCED -> stringResource(Res.string.settings_p2p_profile_balanced)
P2pTorrentProfile.FAST -> stringResource(Res.string.settings_p2p_profile_fast)
}
@Composable
private fun p2pCacheSizeLabel(size: P2pCacheSize): String = when (size) {
P2pCacheSize.NONE -> stringResource(Res.string.settings_p2p_cache_none)
P2pCacheSize.GB_2 -> stringResource(Res.string.settings_p2p_cache_2_gb)
P2pCacheSize.GB_5 -> stringResource(Res.string.settings_p2p_cache_5_gb)
P2pCacheSize.GB_10 -> stringResource(Res.string.settings_p2p_cache_10_gb)
}
private fun formatP2pCacheBytes(bytes: Long): String {
val gibibyte = 1024.0 * 1024.0 * 1024.0
val mebibyte = 1024.0 * 1024.0
return if (bytes >= gibibyte) {
"${kotlin.math.round(bytes / gibibyte * 10.0) / 10.0} GB"
} else {
"${kotlin.math.round(bytes / mebibyte * 10.0) / 10.0} MB"
}
}
@Composable
private fun addonSubtitleStartupModeLabel(mode: AddonSubtitleStartupMode): String =
when (mode) {
@ -298,12 +328,19 @@ private fun PlaybackSettingsSection(
var showAutoPlayPluginSelectionDialog by remember { mutableStateOf(false) }
var showAutoPlayRegexDialog by remember { mutableStateOf(false) }
var showP2pConsentDialog by remember { mutableStateOf(false) }
var showP2pProfileDialog by remember { mutableStateOf(false) }
var showP2pCacheSizeDialog by remember { mutableStateOf(false) }
var p2pCacheClearResult by remember { mutableStateOf<P2pCacheClearResult?>(null) }
var p2pCacheClearFailed by remember { mutableStateOf(false) }
val pluginsEnabled = AppFeaturePolicy.pluginsEnabled
val autoPlayPlayerSettings by PlayerSettingsRepository.uiState.collectAsStateWithLifecycle()
val p2pSettings by remember {
P2pSettingsRepository.ensureLoaded()
P2pSettingsRepository.uiState
}.collectAsStateWithLifecycle()
val p2pCacheState by P2pStreamingEngine.cacheState.collectAsStateWithLifecycle()
val p2pStreamingState by P2pStreamingEngine.state.collectAsStateWithLifecycle()
val coroutineScope = rememberCoroutineScope()
val availableExternalPlayers = ExternalPlayerPlatform.availablePlayers()
val selectedExternalPlayer = availableExternalPlayers.firstOrNull {
it.id == autoPlayPlayerSettings.externalPlayerId
@ -616,7 +653,7 @@ private fun PlaybackSettingsSection(
if (P2pSettingsRepository.isVisible) {
SettingsSection(
title = stringResource(Res.string.settings_p2p_title),
title = stringResource(Res.string.settings_playback_section_p2p),
isTablet = isTablet,
) {
SettingsGroup(isTablet = isTablet) {
@ -641,6 +678,56 @@ private fun PlaybackSettingsSection(
isTablet = isTablet,
onCheckedChange = P2pSettingsRepository::setHideTorrentStats,
)
SettingsGroupDivider(isTablet = isTablet)
SettingsNavigationRow(
title = stringResource(Res.string.settings_p2p_profile_title),
description = p2pProfileLabel(p2pSettings.torrentProfile),
isTablet = isTablet,
onClick = { showP2pProfileDialog = true },
)
SettingsGroupDivider(isTablet = isTablet)
SettingsNavigationRow(
title = stringResource(Res.string.settings_p2p_cache_size_title),
description = p2pCacheSizeLabel(p2pSettings.cacheSize),
isTablet = isTablet,
onClick = { showP2pCacheSizeDialog = true },
)
SettingsGroupDivider(isTablet = isTablet)
val cacheClearAvailable = p2pStreamingState !is P2pStreamingState.Connecting &&
p2pStreamingState !is P2pStreamingState.Streaming &&
!p2pCacheState.isClearing
SettingsNavigationRow(
title = stringResource(Res.string.settings_p2p_clear_cache_title),
description = when {
p2pCacheState.isClearing ->
stringResource(Res.string.settings_p2p_clear_cache_clearing)
!cacheClearAvailable ->
stringResource(Res.string.settings_p2p_clear_cache_playback_active)
p2pCacheClearFailed ->
stringResource(Res.string.settings_p2p_clear_cache_failed)
p2pCacheClearResult != null -> stringResource(
Res.string.settings_p2p_clear_cache_done,
formatP2pCacheBytes(p2pCacheClearResult!!.reclaimedBytes),
)
!p2pCacheState.hasMeasurement ->
stringResource(Res.string.settings_p2p_clear_cache_usage_pending)
else -> stringResource(
Res.string.settings_p2p_clear_cache_usage,
formatP2pCacheBytes(p2pCacheState.usedBytes),
)
},
enabled = cacheClearAvailable,
isTablet = isTablet,
onClick = {
p2pCacheClearResult = null
p2pCacheClearFailed = false
coroutineScope.launch {
runCatching { P2pStreamingEngine.clearCache() }
.onSuccess { p2pCacheClearResult = it }
.onFailure { p2pCacheClearFailed = true }
}
},
)
}
}
}
@ -1182,6 +1269,45 @@ private fun PlaybackSettingsSection(
)
}
if (showP2pProfileDialog) {
IosEnumSelectionDialog(
title = stringResource(Res.string.settings_p2p_profile_title),
options = P2pTorrentProfile.entries,
selected = p2pSettings.torrentProfile,
label = { p2pProfileLabel(it) },
description = { profile ->
when (profile) {
P2pTorrentProfile.SOFT ->
stringResource(Res.string.settings_p2p_profile_soft_description)
P2pTorrentProfile.BALANCED ->
stringResource(Res.string.settings_p2p_profile_balanced_description)
P2pTorrentProfile.FAST ->
stringResource(Res.string.settings_p2p_profile_fast_description)
}
},
onSelect = { profile ->
P2pSettingsRepository.setTorrentProfile(profile)
showP2pProfileDialog = false
},
onDismiss = { showP2pProfileDialog = false },
)
}
if (showP2pCacheSizeDialog) {
IosEnumSelectionDialog(
title = stringResource(Res.string.settings_p2p_cache_size_title),
options = P2pCacheSize.entries,
selected = p2pSettings.cacheSize,
label = { p2pCacheSizeLabel(it) },
onSelect = { size ->
P2pSettingsRepository.setCacheSize(size)
p2pCacheClearResult = null
showP2pCacheSizeDialog = false
},
onDismiss = { showP2pCacheSizeDialog = false },
)
}
if (showSecondaryAudioDialog) {
val originalHint = stringResource(Res.string.settings_playback_option_original_hint)
LanguageSelectionDialog(
@ -2143,7 +2269,7 @@ private fun <T> IosEnumSelectionDialog(
title: String,
options: List<T>,
selected: T,
label: (T) -> String,
label: @Composable (T) -> String,
description: @Composable (T) -> String? = { null },
onSelect: (T) -> Unit,
onDismiss: () -> Unit,

View file

@ -129,6 +129,12 @@ internal fun LazyListScope.streamsSettingsContent(isTablet: Boolean) {
}
}
Spacer(
modifier = Modifier.height(
if (isTablet) NuvioTokens.Space.s18 else MaterialTheme.nuvio.spacing.listGap,
),
)
SettingsSection(
title = stringResource(Res.string.settings_stream_display_section),
isTablet = isTablet,

View file

@ -48,33 +48,37 @@ object TraktAuthRepository {
val isAuthenticated: StateFlow<Boolean> = _isAuthenticated.asStateFlow()
private var hasLoaded = false
private var currentProfileId: Int = 1
private var profileGeneration: Long = 0L
private var authState = TraktAuthState()
fun ensureLoaded() {
if (hasLoaded) return
loadFromDisk()
fun ensureLoaded(profileId: Int = ProfileRepository.activeProfileId) {
if (hasLoaded && currentProfileId == profileId) return
loadFromDisk(profileId)
}
fun onProfileChanged() {
loadFromDisk()
fun onProfileChanged(profileId: Int) {
loadFromDisk(profileId)
}
fun clearLocalState() {
hasLoaded = false
currentProfileId = 1
profileGeneration += 1L
authState = TraktAuthState()
publish()
}
fun snapshot(): TraktAuthUiState {
ensureLoaded()
fun snapshot(profileId: Int = ProfileRepository.activeProfileId): TraktAuthUiState {
ensureLoaded(profileId)
return _uiState.value
}
fun hasRequiredCredentials(): Boolean =
TraktConfig.CLIENT_ID.isNotBlank() && TraktConfig.CLIENT_SECRET.isNotBlank()
fun onConnectRequested(): String? {
ensureLoaded()
fun onConnectRequested(profileId: Int = ProfileRepository.activeProfileId): String? {
ensureLoaded(profileId)
if (!hasRequiredCredentials()) {
publish(errorMessage = localizedString(Res.string.trakt_missing_credentials))
return null
@ -85,7 +89,7 @@ object TraktAuthRepository {
pendingAuthorizationState = oauthState,
pendingAuthorizationStartedAtMillis = TraktPlatformClock.nowEpochMs(),
)
persist()
persist(profileId)
publish(
statusMessage = localizedString(Res.string.trakt_complete_sign_in_browser),
errorMessage = null,
@ -94,21 +98,21 @@ object TraktAuthRepository {
return buildAuthorizationUrl(oauthState)
}
fun pendingAuthorizationUrl(): String? {
ensureLoaded()
fun pendingAuthorizationUrl(profileId: Int = ProfileRepository.activeProfileId): String? {
ensureLoaded(profileId)
val oauthState = authState.pendingAuthorizationState ?: return null
return buildAuthorizationUrl(oauthState)
}
fun onCancelAuthorization() {
ensureLoaded()
fun onCancelAuthorization(profileId: Int = ProfileRepository.activeProfileId) {
ensureLoaded(profileId)
clearPendingAuthorization()
persist()
persist(profileId)
publish(statusMessage = null, errorMessage = null)
}
fun onCancelDeviceFlow() {
onCancelAuthorization()
fun onCancelDeviceFlow(profileId: Int = ProfileRepository.activeProfileId) {
onCancelAuthorization(profileId)
}
fun onAuthLaunchFailed(reason: String) {
@ -116,7 +120,8 @@ object TraktAuthRepository {
}
fun onAuthCallbackReceived(callbackUrl: String) {
ensureLoaded()
val profileId = ProfileRepository.activeProfileId
ensureLoaded(profileId)
if (!callbackUrl.startsWith("${TraktConfig.REDIRECT_URI}?", ignoreCase = true) &&
!callbackUrl.equals(TraktConfig.REDIRECT_URI, ignoreCase = true)
) {
@ -124,15 +129,15 @@ object TraktAuthRepository {
}
scope.launch {
completeAuthorizationFromCallback(callbackUrl)
completeAuthorizationFromCallback(callbackUrl, profileId)
}
}
suspend fun authorizedHeaders(): Map<String, String>? {
ensureLoaded()
suspend fun authorizedHeaders(profileId: Int = currentProfileId): Map<String, String>? {
ensureLoaded(profileId)
if (!authState.isAuthenticated) return null
val hasValidToken = refreshTokenIfNeeded(force = false)
val hasValidToken = refreshTokenIfNeeded(force = false, profileId = profileId)
if (!hasValidToken) return null
val accessToken = authState.accessToken?.trim().orEmpty()
@ -145,9 +150,9 @@ object TraktAuthRepository {
)
}
suspend fun refreshUserSettings(): String? {
ensureLoaded()
val headers = authorizedHeaders() ?: return null
suspend fun refreshUserSettings(profileId: Int = currentProfileId): String? {
ensureLoaded(profileId)
val headers = authorizedHeaders(profileId) ?: return null
val response = runCatching {
httpGetTextWithHeaders(
url = "$BASE_URL/users/settings",
@ -166,19 +171,19 @@ object TraktAuthRepository {
username = parsed.user?.username,
userSlug = parsed.user?.ids?.slug,
)
persist()
persist(profileId)
publish()
return authState.username
}
fun onDisconnectRequested() {
ensureLoaded()
fun onDisconnectRequested(profileId: Int = currentProfileId) {
ensureLoaded(profileId)
scope.launch {
disconnect()
disconnect(profileId)
}
}
private suspend fun completeAuthorizationFromCallback(callbackUrl: String) {
private suspend fun completeAuthorizationFromCallback(callbackUrl: String, profileId: Int = currentProfileId) {
publish(isLoading = true, errorMessage = null)
val parsedUrl = runCatching { Url(callbackUrl) }
@ -189,7 +194,7 @@ object TraktAuthRepository {
if (parsedUrl == null) {
clearPendingAuthorization()
persist()
persist(profileId)
publish(
isLoading = false,
errorMessage = localizedString(Res.string.trakt_invalid_callback),
@ -202,7 +207,7 @@ object TraktAuthRepository {
val errorDescription = parsedUrl.parameters["error_description"]
?: localizedString(Res.string.trakt_authorization_denied)
clearPendingAuthorization()
persist()
persist(profileId)
publish(
isLoading = false,
errorMessage = errorDescription,
@ -213,7 +218,7 @@ object TraktAuthRepository {
val code = parsedUrl.parameters["code"].orEmpty().trim()
if (code.isBlank()) {
clearPendingAuthorization()
persist()
persist(profileId)
publish(
isLoading = false,
errorMessage = localizedString(Res.string.trakt_missing_auth_code),
@ -225,7 +230,7 @@ object TraktAuthRepository {
val callbackState = parsedUrl.parameters["state"].orEmpty().trim()
if (!expectedState.isNullOrBlank() && callbackState != expectedState) {
clearPendingAuthorization()
persist()
persist(profileId)
publish(
isLoading = false,
errorMessage = localizedString(Res.string.trakt_invalid_callback_state),
@ -233,10 +238,10 @@ object TraktAuthRepository {
return
}
exchangeAuthorizationCode(code)
exchangeAuthorizationCode(code, profileId)
}
private suspend fun exchangeAuthorizationCode(code: String) {
private suspend fun exchangeAuthorizationCode(code: String, profileId: Int = currentProfileId) {
val body = json.encodeToString(
TraktAuthorizationCodeRequest(
code = code,
@ -259,7 +264,7 @@ object TraktAuthRepository {
if (response == null) {
clearPendingAuthorization()
persist()
persist(profileId)
publish(isLoading = false, errorMessage = localizedString(Res.string.trakt_sign_in_complete_failed))
return
}
@ -270,7 +275,7 @@ object TraktAuthRepository {
if (parsed == null) {
clearPendingAuthorization()
persist()
persist(profileId)
publish(isLoading = false, errorMessage = localizedString(Res.string.trakt_invalid_token_response))
return
}
@ -284,8 +289,8 @@ object TraktAuthRepository {
pendingAuthorizationState = null,
pendingAuthorizationStartedAtMillis = null,
)
persist()
refreshUserSettings()
persist(profileId)
refreshUserSettings(profileId)
publish(
isLoading = false,
statusMessage = localizedString(Res.string.trakt_connected_status),
@ -293,7 +298,7 @@ object TraktAuthRepository {
)
}
private suspend fun disconnect() {
private suspend fun disconnect(profileId: Int = currentProfileId) {
publish(isLoading = true, errorMessage = null)
val token = authState.accessToken?.takeIf { it.isNotBlank() }
@ -317,9 +322,9 @@ object TraktAuthRepository {
}
}
TraktCredentialSync.deleteRemote()
TraktCredentialSync.deleteRemote(profileId)
authState = TraktAuthState()
persist()
persist(profileId)
publish(
isLoading = false,
statusMessage = localizedString(Res.string.trakt_disconnected_status),
@ -327,9 +332,8 @@ object TraktAuthRepository {
)
}
private suspend fun refreshTokenIfNeeded(force: Boolean): Boolean = refreshMutex.withLock {
private suspend fun refreshTokenIfNeeded(force: Boolean, profileId: Int = currentProfileId): Boolean = refreshMutex.withLock {
if (!hasRequiredCredentials()) return@withLock false
val profileId = ProfileRepository.activeProfileId
val refreshToken = authState.refreshToken?.takeIf { it.isNotBlank() }
?: return@withLock false
@ -391,14 +395,14 @@ object TraktAuthRepository {
createdAt = parsed.createdAt,
expiresIn = parsed.expiresIn,
)
persist()
persist(profileId)
publish()
true
}
private suspend fun invalidateCredentials(profileId: Int) {
authState = TraktAuthState()
persist()
persist(profileId)
publish(
isLoading = false,
statusMessage = null,
@ -407,9 +411,11 @@ object TraktAuthRepository {
TraktCredentialSync.deleteRemote(profileId)
}
private fun loadFromDisk() {
private fun loadFromDisk(profileId: Int) {
currentProfileId = profileId
profileGeneration += 1L
hasLoaded = true
val payload = TraktAuthStorage.loadPayload().orEmpty().trim()
val payload = TraktAuthStorage.loadPayload(profileId).orEmpty().trim()
authState = if (payload.isBlank()) {
TraktAuthState()
} else {
@ -460,8 +466,8 @@ object TraktAuthRepository {
)
}
private fun persist() {
TraktAuthStorage.savePayload(json.encodeToString(authState))
private fun persist(profileId: Int = currentProfileId) {
TraktAuthStorage.savePayload(profileId, json.encodeToString(authState))
}
private fun buildAuthorizationUrl(state: String): String {
@ -486,6 +492,7 @@ object TraktAuthRepository {
return nowSeconds >= (expiresAtSeconds - 60)
}
private fun localizedString(resource: StringResource): String = runBlocking { getString(resource) }
}
internal enum class TraktTokenRefreshResponseAction {
@ -549,4 +556,3 @@ private data class TraktUserDto(
private data class TraktUserIdsDto(
val slug: String? = null,
)
private fun localizedString(resource: StringResource): String = runBlocking { getString(resource) }

View file

@ -1,6 +1,6 @@
package com.nuvio.app.features.trakt
internal expect object TraktAuthStorage {
fun loadPayload(): String?
fun savePayload(payload: String)
fun loadPayload(profileId: Int): String?
fun savePayload(profileId: Int, payload: String)
}

View file

@ -270,7 +270,7 @@ object WatchedRepository {
)
suspend fun pullFromServer(profileId: Int) {
TraktAuthRepository.ensureLoaded()
TraktAuthRepository.ensureLoaded(profileId)
TraktSettingsRepository.ensureLoaded()
refreshForSource(
profileId = profileId,
@ -283,7 +283,7 @@ object WatchedRepository {
}
suspend fun forceSnapshotRefreshFromServer(profileId: Int) {
TraktAuthRepository.ensureLoaded()
TraktAuthRepository.ensureLoaded(profileId)
TraktSettingsRepository.ensureLoaded()
refreshForSource(
profileId = profileId,
@ -300,7 +300,7 @@ object WatchedRepository {
source: WatchProgressSource,
forceSnapshot: Boolean = true,
): Boolean {
TraktAuthRepository.ensureLoaded()
TraktAuthRepository.ensureLoaded(profileId)
TraktSettingsRepository.ensureLoaded()
if (ProfileRepository.activeProfileId != profileId) {
log.d { "Skipping watched refresh for inactive profile $profileId" }

View file

@ -24,6 +24,7 @@ enum class ContinueWatchingSectionStyle {
enum class ContinueWatchingSortMode {
DEFAULT,
STREAMING_STYLE,
SPLIT_UPCOMING,
}
@Serializable

View file

@ -9,7 +9,6 @@ internal class NuvioNavigator(
private val onExternalNavigate: ((AppRoute, launchSingleTop: Boolean) -> Unit)? = null,
private val onExternalBack: (() -> Unit)? = null,
private val onExternalReplace: ((AppRoute) -> Unit)? = null,
private val onRouteRemoved: (AppRoute) -> Unit = {},
) {
val currentRoute: AppRoute?
get() = backStack.lastOrNull() as? AppRoute
@ -37,12 +36,7 @@ internal class NuvioNavigator(
val firstRemovedIndex = if (resolvedOptions.popUpToInclusive) targetIndex else targetIndex + 1
if (firstRemovedIndex <= backStack.lastIndex) {
val removedRoutes = backStack
.subList(firstRemovedIndex, backStack.size)
.filterIsInstance<AppRoute>()
.toList()
backStack.subList(firstRemovedIndex, backStack.size).clear()
removedRoutes.forEach(onRouteRemoved)
}
}
}
@ -54,7 +48,7 @@ internal class NuvioNavigator(
fun popBackStack(expectedRoute: AppRoute? = null): Boolean {
if (expectedRoute != null && currentRoute != expectedRoute) return false
if (backStack.size > 1) {
(backStack.removeAt(backStack.lastIndex) as? AppRoute)?.let(onRouteRemoved)
backStack.removeAt(backStack.lastIndex)
return true
}
onExternalBack?.invoke()

View file

@ -0,0 +1,35 @@
package com.nuvio.app.navigation
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavEntryDecorator
internal class RouteDisposalNavEntryDecorator<T : Any> private constructor(
private val registry: RouteDisposalRegistry<T>,
) : NavEntryDecorator<T>(
onPop = registry::dispose,
decorate = { entry -> entry.Content() },
) {
constructor(onDispose: (T) -> Unit) : this(RouteDisposalRegistry(onDispose))
fun register(key: T, entry: NavEntry<T>): NavEntry<T> = entry.also {
registry.register(contentKey = entry.contentKey, key = key)
}
}
internal class RouteDisposalRegistry<T : Any>(
private val onDispose: (T) -> Unit,
) {
private val keysByContentKey = mutableMapOf<Any, T>()
fun register(contentKey: Any, key: T) {
val existingKey = keysByContentKey[contentKey]
check(existingKey == null || existingKey == key) {
"Navigation content key $contentKey is already registered to a different route"
}
keysByContentKey[contentKey] = key
}
fun dispose(contentKey: Any) {
keysByContentKey.remove(contentKey)?.let(onDispose)
}
}

View file

@ -10,6 +10,7 @@ import com.nuvio.app.features.debrid.DebridProviders
import com.nuvio.app.features.watchprogress.CachedInProgressItem
import com.nuvio.app.features.watchprogress.CachedNextUpItem
import com.nuvio.app.features.watchprogress.ContinueWatchingItem
import com.nuvio.app.features.watchprogress.ContinueWatchingSortMode
import com.nuvio.app.features.watchprogress.WatchProgressEntry
import com.nuvio.app.features.watchprogress.WatchProgressSourceTraktHistory
import com.nuvio.app.features.watchprogress.nextUpDismissKey
@ -162,6 +163,72 @@ class HomeScreenTest {
assertEquals("S1E4 • Current", result.single().subtitle)
}
@Test
fun `split upcoming mode moves only unaired next up episodes and sorts them by release`() {
val nowEpochMs = requireNotNull(parseReleaseDateToEpochMs("2026-07-19T12:00:00Z"))
val inProgress = progressEntry(
videoId = "movie-1",
title = "Movie",
lastUpdatedEpochMs = 500L,
seasonNumber = null,
episodeNumber = null,
episodeTitle = null,
).toContinueWatchingItem()
val aired = continueWatchingItem(
videoId = "aired:1:2",
subtitle = "S1E2 • Aired",
).copy(released = "2026-07-19T11:59:59Z")
val unknownRelease = continueWatchingItem(
videoId = "unknown:1:2",
subtitle = "S1E2 • Unknown",
)
val laterUpcoming = continueWatchingItem(
videoId = "later:1:2",
subtitle = "S1E2 • Later",
).copy(released = "2026-07-21T00:00:00Z")
val soonerUpcoming = continueWatchingItem(
videoId = "sooner:1:2",
subtitle = "S1E2 • Sooner",
).copy(released = "2026-07-20T00:00:00Z")
val (main, upcoming) = splitUpcomingItems(
items = listOf(laterUpcoming, inProgress, aired, unknownRelease, soonerUpcoming),
mode = ContinueWatchingSortMode.SPLIT_UPCOMING,
nowEpochMs = nowEpochMs,
)
assertEquals(
listOf("movie-1", "aired:1:2", "unknown:1:2"),
main.map(ContinueWatchingItem::videoId),
)
assertEquals(
listOf("sooner:1:2", "later:1:2"),
upcoming.map(ContinueWatchingItem::videoId),
)
}
@Test
fun `non split sort modes keep upcoming episodes in continue watching`() {
val futureItem = continueWatchingItem(
videoId = "future:1:2",
subtitle = "S1E2 • Future",
).copy(released = "2099-01-01T00:00:00Z")
listOf(
ContinueWatchingSortMode.DEFAULT,
ContinueWatchingSortMode.STREAMING_STYLE,
).forEach { mode ->
val (main, upcoming) = splitUpcomingItems(
items = listOf(futureItem),
mode = mode,
nowEpochMs = 0L,
)
assertEquals(listOf(futureItem), main)
assertTrue(upcoming.isEmpty())
}
}
@Test
fun `build home continue watching items enriches cloud title from library file`() {
val file = CloudLibraryFile(id = "8", name = "GOAT.2026.2160p.UHD.mkv")

View file

@ -0,0 +1,153 @@
package com.nuvio.app.features.library
import kotlin.test.Test
import kotlin.test.assertEquals
class LibraryDisplaySettingsTest {
@Test
fun `local default resolves to recently added while Trakt uses rank order`() {
assertEquals(
LibrarySortOption.ADDED_DESC,
effectiveLibrarySortOption(LibrarySortOption.DEFAULT, LibrarySourceMode.LOCAL),
)
assertEquals(
LibrarySortOption.DEFAULT,
effectiveLibrarySortOption(LibrarySortOption.DEFAULT, LibrarySourceMode.TRAKT),
)
val input = listOf(
item("ranked-second", savedAt = 3L, traktRank = 2),
item("unranked", savedAt = 4L),
item("ranked-first", savedAt = 1L, traktRank = 1),
item("ranked-first-newer", savedAt = 2L, traktRank = 1),
)
assertEquals(
listOf("ranked-first-newer", "ranked-first", "ranked-second", "unranked"),
sortLibraryItems(input, LibrarySortOption.DEFAULT, LibrarySourceMode.TRAKT).map { it.id },
)
assertEquals(
listOf("unranked", "ranked-second", "ranked-first-newer", "ranked-first"),
sortLibraryItems(input, LibrarySortOption.DEFAULT, LibrarySourceMode.LOCAL).map { it.id },
)
}
@Test
fun `added sorting works in both directions`() {
val input = listOf(
item("middle", savedAt = 2L),
item("oldest", savedAt = 1L),
item("newest", savedAt = 3L),
)
assertEquals(
listOf("newest", "middle", "oldest"),
sortLibraryItems(input, LibrarySortOption.ADDED_DESC, LibrarySourceMode.LOCAL).map { it.id },
)
assertEquals(
listOf("oldest", "middle", "newest"),
sortLibraryItems(input, LibrarySortOption.ADDED_ASC, LibrarySourceMode.TRAKT).map { it.id },
)
}
@Test
fun `title sorting ignores leading English articles`() {
val input = listOf(
item("batman", name = "The Batman"),
item("arrival", name = "Arrival"),
item("quiet", name = "A Quiet Place"),
)
assertEquals(
listOf("arrival", "batman", "quiet"),
sortLibraryItems(input, LibrarySortOption.TITLE_ASC, LibrarySourceMode.LOCAL).map { it.id },
)
assertEquals(
listOf("quiet", "batman", "arrival"),
sortLibraryItems(input, LibrarySortOption.TITLE_DESC, LibrarySourceMode.LOCAL).map { it.id },
)
}
@Test
fun `horizontal sections sort independently without changing section order`() {
val sections = listOf(
LibrarySection(
type = "movie",
displayTitle = "Movies",
items = listOf(item("z", name = "Zulu"), item("a", name = "Alpha")),
),
LibrarySection(
type = "series",
displayTitle = "Series",
items = listOf(item("y", type = "series", name = "Yellow"), item("b", type = "series", name = "Beta")),
),
)
val sorted = sortLibrarySections(sections, LibrarySortOption.TITLE_ASC, LibrarySourceMode.LOCAL)
assertEquals(listOf("movie", "series"), sorted.map { it.type })
assertEquals(listOf("a", "z"), sorted[0].items.map { it.id })
assertEquals(listOf("b", "y"), sorted[1].items.map { it.id })
}
@Test
fun `vertical Trakt projection selects one list then filters and sorts its items`() {
val watchlist = LibrarySection(
type = "watchlist",
displayTitle = "Watchlist",
items = listOf(
item("z", name = "Zulu"),
item("series", type = "series", name = "Series"),
item("a", name = "Alpha"),
),
)
val personal = LibrarySection(
type = "personal:1",
displayTitle = "Favorites",
items = listOf(item("favorite", name = "Favorite")),
)
val projection = buildLibraryVerticalProjection(
sections = listOf(watchlist, personal),
sourceMode = LibrarySourceMode.TRAKT,
selectedSectionKey = "missing",
selectedType = "movie",
sortOption = LibrarySortOption.TITLE_ASC,
)
assertEquals("watchlist", projection.selectedSectionKey)
assertEquals(listOf("movie", "series"), projection.availableTypes)
assertEquals("movie", projection.selectedType)
assertEquals(listOf("a", "z"), projection.entries.map { it.item.id })
assertEquals(listOf("watchlist", "watchlist"), projection.entries.map { it.section.type })
}
@Test
fun `display settings payload round trips and invalid values fall back safely`() {
val state = LibraryDisplaySettingsUiState(
layoutMode = LibraryLayoutMode.VERTICAL,
sortOption = LibrarySortOption.TITLE_DESC,
)
assertEquals(state, decodeLibraryDisplaySettings(encodeLibraryDisplaySettings(state)))
assertEquals(
LibraryDisplaySettingsUiState(),
decodeLibraryDisplaySettings("""{"layout_mode":"unknown","sort_option":"unknown"}"""),
)
}
private fun item(
id: String,
type: String = "movie",
name: String = id,
savedAt: Long = 0L,
traktRank: Int? = null,
): LibraryItem =
LibraryItem(
id = id,
type = type,
name = name,
savedAtEpochMs = savedAt,
traktRank = traktRank,
)
}

View file

@ -0,0 +1,32 @@
package com.nuvio.app.features.p2p
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class P2pMagnetTest {
@Test
fun v1HashAndTrackersAreCanonicalEncodedAndDeduplicated() {
val hash = "ABCDEF0123456789ABCDEF0123456789ABCDEF01"
val tracker = "udp://tracker.example:80/announce?key=hello world"
val magnet = buildP2pMagnetUri(hash, listOf("", tracker, " ", tracker))
assertTrue(magnet.startsWith("magnet:?xt=urn:btih:${hash.lowercase()}"))
assertEquals(1, magnet.windowed("&tr=".length).count { it == "&tr=" })
assertTrue(magnet.endsWith("udp%3A%2F%2Ftracker.example%3A80%2Fannounce%3Fkey%3Dhello%20world"))
}
@Test
fun v2HashUsesMultihashTopic() {
val hash = "a".repeat(64)
assertTrue(buildP2pMagnetUri(hash, emptyList()).contains("xt=urn:btmh:1220$hash"))
}
@Test
fun invalidHashIsRejectedBeforeNativeWork() {
assertFailsWith<IllegalArgumentException> {
buildP2pMagnetUri("not-a-hash", emptyList())
}
}
}

View file

@ -0,0 +1,39 @@
package com.nuvio.app.features.p2p
import com.nuvio.app.features.player.p2pInitialLoadingProgress
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
class P2pTelemetryTest {
@Test
fun torrentStatsAreVisibleByDefault() {
assertFalse(P2pSettingsUiState().hideTorrentStats)
}
@Test
fun connectingStateCarriesStartupTelemetry() {
val state = P2pStreamingState.Connecting(
phase = "add_magnet",
downloadSpeed = 2_000_000L,
peers = 12,
seeds = 7,
)
assertEquals("add_magnet", state.phase)
assertEquals(2_000_000L, state.downloadSpeed)
assertEquals(12, state.peers)
assertEquals(7, state.seeds)
}
@Test
fun initialProgressUsesReadinessStagesWithoutCompletingDuringBuffering() {
val target = 5_242_880L
assertEquals(0f, p2pInitialLoadingProgress(0L, 0L, 0L))
assertEquals(0.225f, p2pInitialLoadingProgress(0L, target, 0L))
assertEquals(0.375f, p2pInitialLoadingProgress(0L, target, target))
assertEquals(0.85f, p2pInitialLoadingProgress(5_000L, target, target))
assertEquals(0.95f, p2pInitialLoadingProgress(15_000L, Long.MAX_VALUE, Long.MAX_VALUE))
}
}

View file

@ -0,0 +1,33 @@
package com.nuvio.app.navigation
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class RouteDisposalRegistryTest {
@Test
fun disposesRegisteredRouteExactlyOnceWhenNav3ReportsPop() {
val disposedRoutes = mutableListOf<String>()
val registry = RouteDisposalRegistry<String>(disposedRoutes::add)
registry.register(contentKey = "stream-entry", key = "stream-route")
assertTrue(disposedRoutes.isEmpty())
registry.dispose(contentKey = "stream-entry")
registry.dispose(contentKey = "stream-entry")
assertEquals(listOf("stream-route"), disposedRoutes)
}
@Test
fun rejectsAContentKeySharedByDifferentRoutes() {
val registry = RouteDisposalRegistry<String> {}
registry.register(contentKey = "shared-entry", key = "first-route")
assertFailsWith<IllegalStateException> {
registry.register(contentKey = "shared-entry", key = "second-route")
}
}
}

View file

@ -7,6 +7,8 @@ import kotlinx.coroutines.flow.asStateFlow
actual object P2pStreamingEngine {
private val _state = MutableStateFlow<P2pStreamingState>(P2pStreamingState.Idle)
actual val state: StateFlow<P2pStreamingState> = _state.asStateFlow()
private val _cacheState = MutableStateFlow(P2pCacheUiState())
actual val cacheState: StateFlow<P2pCacheUiState> = _cacheState.asStateFlow()
actual suspend fun startStream(request: P2pStreamRequest): String {
val message = "P2P streaming is not available on this platform"
@ -14,6 +16,9 @@ actual object P2pStreamingEngine {
throw P2pStreamingException(message)
}
actual suspend fun clearCache(): P2pCacheClearResult =
P2pCacheClearResult(reclaimedBytes = 0L, remainingBytes = 0L, protectedBytes = 0L)
actual fun stopStream() {
_state.value = P2pStreamingState.Idle
}

View file

@ -5,7 +5,7 @@ actual object AppFeaturePolicy {
actual val supportersContributorsPageEnabled: Boolean = true
actual val accountDeletionEnabled: Boolean = false
actual val personalMediaAddonCopyEnabled: Boolean = false
actual val p2pEnabled: Boolean = false
actual val p2pEnabled: Boolean = true
actual val trailerPlaybackMode: TrailerPlaybackMode = TrailerPlaybackMode.IN_APP
actual val heroTrailerPlaybackSupported: Boolean = false
actual val inAppUpdaterEnabled: Boolean = false

View file

@ -0,0 +1,563 @@
@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
package com.nuvio.app.features.p2p
import cnames.structs.nuvio_engine
import com.nuvio.app.features.p2p.native.NUVIO_ENGINE_EVENT_DISK_CACHE_RECLAIMED
import com.nuvio.app.features.p2p.native.NUVIO_ENGINE_EVENT_STREAM_PREPARED
import com.nuvio.app.features.p2p.native.NUVIO_ENGINE_EVENT_STREAM_STOPPED
import com.nuvio.app.features.p2p.native.NUVIO_ENGINE_EVENT_TORRENT_ERROR
import com.nuvio.app.features.p2p.native.NUVIO_ENGINE_EVENT_TORRENT_METADATA_READY
import com.nuvio.app.features.p2p.native.NUVIO_ENGINE_STATUS_NO_EVENT
import com.nuvio.app.features.p2p.native.NUVIO_ENGINE_STATUS_OK
import com.nuvio.app.features.p2p.native.NUVIO_ENGINE_TORRENT_PROFILE_BALANCED
import com.nuvio.app.features.p2p.native.NUVIO_ENGINE_TORRENT_PROFILE_FAST
import com.nuvio.app.features.p2p.native.NUVIO_ENGINE_TORRENT_PROFILE_SOFT
import com.nuvio.app.features.p2p.native.NUVIO_ENGINE_UPLOAD_DISABLED
import com.nuvio.app.features.p2p.native.NUVIO_ENGINE_UPLOAD_UNLIMITED
import com.nuvio.app.features.p2p.native.nuvio_engine_add_torrent
import com.nuvio.app.features.p2p.native.nuvio_engine_config
import com.nuvio.app.features.p2p.native.nuvio_engine_config_init_sized
import com.nuvio.app.features.p2p.native.nuvio_engine_create
import com.nuvio.app.features.p2p.native.nuvio_engine_destroy
import com.nuvio.app.features.p2p.native.nuvio_engine_event
import com.nuvio.app.features.p2p.native.nuvio_engine_event_init_sized
import com.nuvio.app.features.p2p.native.nuvio_engine_get_stats
import com.nuvio.app.features.p2p.native.nuvio_engine_get_stream_stats
import com.nuvio.app.features.p2p.native.nuvio_engine_poll_event
import com.nuvio.app.features.p2p.native.nuvio_engine_prepare_stream
import com.nuvio.app.features.p2p.native.nuvio_engine_reclaim_disk_cache
import com.nuvio.app.features.p2p.native.nuvio_engine_stats
import com.nuvio.app.features.p2p.native.nuvio_engine_stats_init_sized
import com.nuvio.app.features.p2p.native.nuvio_engine_status_message
import com.nuvio.app.features.p2p.native.nuvio_engine_stop_stream
import com.nuvio.app.features.p2p.native.nuvio_engine_stream_request
import com.nuvio.app.features.p2p.native.nuvio_engine_stream_request_init_sized
import com.nuvio.app.features.p2p.native.nuvio_engine_stream_stats
import com.nuvio.app.features.p2p.native.nuvio_engine_stream_stats_init_sized
import com.nuvio.app.features.p2p.native.nuvio_engine_torrent_request
import com.nuvio.app.features.p2p.native.nuvio_engine_torrent_request_init_sized
import kotlinx.cinterop.CPointer
import kotlinx.cinterop.CPointerVar
import kotlinx.cinterop.ULongVar
import kotlinx.cinterop.alloc
import kotlinx.cinterop.cstr
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.sizeOf
import kotlinx.cinterop.toKString
import kotlinx.cinterop.value
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import platform.Foundation.NSFileManager
import platform.Foundation.NSHomeDirectory
import platform.Foundation.NSLog
private const val StatsPollIntervalMs = 250L
private const val StartupStatsPollIntervalMs = 1_000L
private const val MemoryCacheCapacityBytes = 64L * 1024L * 1024L
actual object P2pStreamingEngine {
private data class EngineConfigurationKey(
val uploadEnabled: Boolean,
val torrentProfile: P2pTorrentProfile,
val diskCacheCapacityBytes: Long,
)
private data class NativeEvent(
val type: UInt,
val requestId: ULong,
val torrentId: String?,
val message: String?,
val fileIndex: UInt?,
val fileSize: ULong,
val streamId: String?,
val streamUrl: String?,
)
private data class NativeStream(
val id: String,
val url: String,
val torrentId: String,
val fileIndex: Int,
val fileSize: Long,
)
private data class AggregateStats(
val peers: Int,
val seeds: Int,
val downloadSpeed: Long,
val uploadSpeed: Long,
val payloadDownloaded: Long,
val diskUsed: Long,
val diskProtected: Long,
)
private data class StreamStats(
val fileSize: Long,
val contiguousReady: Long,
val verified: Long,
val delivered: Long,
)
private val _state = MutableStateFlow<P2pStreamingState>(P2pStreamingState.Idle)
actual val state: StateFlow<P2pStreamingState> = _state.asStateFlow()
private val _cacheState = MutableStateFlow(P2pCacheUiState())
actual val cacheState: StateFlow<P2pCacheUiState> = _cacheState.asStateFlow()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val lifecycleMutex = Mutex()
private var engine: CPointer<nuvio_engine>? = null
private var engineConfigurationKey: EngineConfigurationKey? = null
private var currentTorrentId: String? = null
private var currentStream: NativeStream? = null
private var statsJob: Job? = null
private var generation = 0L
private val knownTorrentIds = mutableSetOf<String>()
actual suspend fun startStream(request: P2pStreamRequest): String = lifecycleMutex.withLock {
stopLocked(shutdownEngine = false)
generation += 1
val streamGeneration = generation
_state.value = P2pStreamingState.Connecting()
var phase = "build_magnet"
var startupStatsJob: Job? = null
var preparedStream: NativeStream? = null
try {
val magnet = buildP2pMagnetUri(
request.infoHash,
(DefaultTrackers + request.trackers).distinct(),
)
phase = "ensure_engine"
val activeEngine = ensureEngine()
val baseline = readAggregateStats(activeEngine).payloadDownloaded
startupStatsJob = startStartupStatsPolling(activeEngine, streamGeneration) { phase }
phase = "add_magnet"
val canonicalHash = canonicalP2pInfoHash(request.infoHash)
val torrentId = if (canonicalHash in knownTorrentIds) {
canonicalHash
} else {
addMagnet(activeEngine, magnet).also(knownTorrentIds::add)
}
currentCoroutineContext().ensureActive()
phase = "prepare_stream"
val stream = prepareStream(
activeEngine,
torrentId,
request.fileIdx,
request.filename,
)
preparedStream = stream
currentCoroutineContext().ensureActive()
currentTorrentId = torrentId
currentStream = stream
phase = "attach_route"
val aggregate = readAggregateStats(activeEngine)
publishStreaming(streamGeneration, stream, aggregate, baseline, null)
startStatsPolling(activeEngine, stream, streamGeneration, baseline)
log("stream ready fileIndex=${stream.fileIndex} fileBytes=${stream.fileSize}")
stream.url
} catch (cancellation: CancellationException) {
withContext(NonCancellable) {
preparedStream?.let { runCatching { stopNativeStream(engine, it.id) } }
}
currentTorrentId = null
currentStream = null
_state.value = P2pStreamingState.Idle
throw cancellation
} catch (error: Throwable) {
preparedStream?.let { runCatching { stopNativeStream(engine, it.id) } }
currentTorrentId = null
currentStream = null
val message = error.message ?: "Unable to start torrent stream"
_state.value = P2pStreamingState.Error(message)
log("stream failed phase=$phase error=$message")
throw P2pStreamingException(message)
} finally {
startupStatsJob?.cancel()
}
}
actual suspend fun clearCache(): P2pCacheClearResult = lifecycleMutex.withLock {
check(_state.value !is P2pStreamingState.Connecting &&
_state.value !is P2pStreamingState.Streaming) {
"Torrent cache cannot be cleared during active playback"
}
_cacheState.value = _cacheState.value.copy(isClearing = true)
try {
val activeEngine = ensureEngine()
val before = readAggregateStats(activeEngine)
reclaimDiskCache(activeEngine)
val after = readAggregateStats(activeEngine)
updateCacheState(after)
P2pCacheClearResult(
reclaimedBytes = (before.diskUsed - after.diskUsed).coerceAtLeast(0L),
remainingBytes = after.diskUsed,
protectedBytes = after.diskProtected,
)
} finally {
_cacheState.value = _cacheState.value.copy(isClearing = false)
}
}
actual fun stopStream() {
scope.launch {
lifecycleMutex.withLock { stopLocked(shutdownEngine = false) }
}
}
actual fun shutdown() {
scope.launch {
lifecycleMutex.withLock { stopLocked(shutdownEngine = true) }
}
}
private suspend fun stopLocked(shutdownEngine: Boolean) {
generation += 1
statsJob?.cancel()
statsJob = null
val stream = currentStream
currentStream = null
currentTorrentId = null
_state.value = P2pStreamingState.Idle
stream?.let { runCatching { stopNativeStream(engine, it.id) } }
if (shutdownEngine) closeEngine()
}
private fun startStartupStatsPolling(
activeEngine: CPointer<nuvio_engine>,
streamGeneration: Long,
phase: () -> String,
): Job = scope.launch {
while (isActive && generation == streamGeneration) {
runCatching { readAggregateStats(activeEngine) }.getOrNull()?.let { stats ->
if (_state.value is P2pStreamingState.Connecting) {
_state.value = P2pStreamingState.Connecting(
phase = phase(),
downloadSpeed = stats.downloadSpeed,
uploadSpeed = stats.uploadSpeed,
peers = stats.peers,
seeds = stats.seeds,
)
updateCacheState(stats)
}
}
delay(StartupStatsPollIntervalMs)
}
}
private fun startStatsPolling(
activeEngine: CPointer<nuvio_engine>,
stream: NativeStream,
streamGeneration: Long,
payloadBaseline: Long,
) {
statsJob?.cancel()
statsJob = scope.launch {
while (isActive && generation == streamGeneration) {
try {
val aggregate = readAggregateStats(activeEngine)
val route = readStreamStats(activeEngine, stream.id)
updateCacheState(aggregate)
publishStreaming(streamGeneration, stream, aggregate, payloadBaseline, route)
} catch (error: Throwable) {
if (error is CancellationException) throw error
if (generation == streamGeneration) {
_state.value = P2pStreamingState.Error(
error.message ?: "Torrent stream stopped unexpectedly"
)
}
return@launch
}
delay(StatsPollIntervalMs)
}
}
}
private fun publishStreaming(
streamGeneration: Long,
stream: NativeStream,
aggregate: AggregateStats,
payloadBaseline: Long,
route: StreamStats?,
) {
if (generation != streamGeneration || currentStream?.id != stream.id) return
_state.value = P2pStreamingState.Streaming(
localUrl = stream.url,
downloadSpeed = aggregate.downloadSpeed,
uploadSpeed = aggregate.uploadSpeed,
peers = aggregate.peers,
seeds = aggregate.seeds,
bufferProgress = ratio(route?.contiguousReady ?: 0L, route?.fileSize ?: stream.fileSize),
totalProgress = ratio(route?.verified ?: 0L, route?.fileSize ?: stream.fileSize),
downloadedBytes = (aggregate.payloadDownloaded - payloadBaseline).coerceAtLeast(0L),
verifiedBytes = route?.verified ?: 0L,
deliveredBytes = route?.delivered ?: 0L,
)
}
private suspend fun ensureEngine(): CPointer<nuvio_engine> {
P2pSettingsRepository.ensureLoaded()
val settings = P2pSettingsRepository.uiState.value
val configuration = EngineConfigurationKey(
uploadEnabled = settings.enableUpload,
torrentProfile = settings.torrentProfile,
diskCacheCapacityBytes = settings.cacheSize.bytes,
)
engine?.takeIf { engineConfigurationKey == configuration }?.let { return it }
closeEngine()
val stateDirectory = "${NSHomeDirectory()}/Library/Application Support/NuvioEngine/state"
val cacheDirectory = "${NSHomeDirectory()}/Library/Caches/NuvioEngine/payload"
createDirectory(stateDirectory)
createDirectory(cacheDirectory)
val created = memScoped {
val config = alloc<nuvio_engine_config>()
nuvio_engine_config_init_sized(config.ptr, sizeOf<nuvio_engine_config>().toUInt())
config.data_directory = stateDirectory.cstr.getPointer(this)
config.cache_directory = cacheDirectory.cstr.getPointer(this)
config.memory_cache_capacity_bytes = MemoryCacheCapacityBytes.toULong()
config.disk_cache_capacity_bytes = configuration.diskCacheCapacityBytes.toULong()
config.upload_mode = if (configuration.uploadEnabled) {
NUVIO_ENGINE_UPLOAD_UNLIMITED
} else {
NUVIO_ENGINE_UPLOAD_DISABLED
}
config.upload_limit_bytes_per_second = 0uL
config.stream_inactivity_timeout_milliseconds = 0u
config.warm_torrent_timeout_milliseconds = 60_000u
config.tls_ca_bundle_path = null
config.torrent_profile = when (configuration.torrentProfile) {
P2pTorrentProfile.SOFT -> NUVIO_ENGINE_TORRENT_PROFILE_SOFT
P2pTorrentProfile.BALANCED -> NUVIO_ENGINE_TORRENT_PROFILE_BALANCED
P2pTorrentProfile.FAST -> NUVIO_ENGINE_TORRENT_PROFILE_FAST
}
val output = alloc<CPointerVar<nuvio_engine>>()
checkStatus(nuvio_engine_create(config.ptr, output.ptr))
output.value ?: throw P2pStreamingException("Nuvio Engine returned an empty handle")
}
engine = created
engineConfigurationKey = configuration
log("engine created configuration=$configuration")
return created
}
private fun closeEngine() {
val activeEngine = engine ?: return
engine = null
engineConfigurationKey = null
knownTorrentIds.clear()
nuvio_engine_destroy(activeEngine)
}
private suspend fun addMagnet(activeEngine: CPointer<nuvio_engine>, magnet: String): String {
val requestId = memScoped {
val request = alloc<nuvio_engine_torrent_request>()
nuvio_engine_torrent_request_init_sized(
request.ptr,
sizeOf<nuvio_engine_torrent_request>().toUInt(),
)
request.magnet_uri = magnet.cstr.getPointer(this)
request.source_type = 0u
val output = alloc<ULongVar>()
checkStatus(nuvio_engine_add_torrent(activeEngine, request.ptr, output.ptr))
output.value
}
return awaitEvent(activeEngine, requestId, NUVIO_ENGINE_EVENT_TORRENT_METADATA_READY)
.torrentId
?: throw P2pStreamingException("Torrent metadata event omitted its identifier")
}
private suspend fun prepareStream(
activeEngine: CPointer<nuvio_engine>,
torrentId: String,
fileIndex: Int?,
filename: String?,
): NativeStream {
val requestId = memScoped {
val request = alloc<nuvio_engine_stream_request>()
nuvio_engine_stream_request_init_sized(
request.ptr,
sizeOf<nuvio_engine_stream_request>().toUInt(),
)
request.torrent_id = torrentId.cstr.getPointer(this)
request.file_index = fileIndex?.toUInt() ?: UInt.MAX_VALUE
request.filename_hint = filename?.cstr?.getPointer(this)
val output = alloc<ULongVar>()
checkStatus(nuvio_engine_prepare_stream(activeEngine, request.ptr, output.ptr))
output.value
}
val event = awaitEvent(activeEngine, requestId, NUVIO_ENGINE_EVENT_STREAM_PREPARED)
return NativeStream(
id = event.streamId
?: throw P2pStreamingException("Stream event omitted its identifier"),
url = event.streamUrl
?: throw P2pStreamingException("Stream event omitted its URL"),
torrentId = event.torrentId ?: torrentId,
fileIndex = event.fileIndex?.toInt() ?: fileIndex ?: -1,
fileSize = event.fileSize.toLong(),
)
}
private suspend fun stopNativeStream(activeEngine: CPointer<nuvio_engine>?, streamId: String) {
if (activeEngine == null) return
val requestId = memScoped {
val output = alloc<ULongVar>()
checkStatus(nuvio_engine_stop_stream(activeEngine, streamId, output.ptr))
output.value
}
awaitEvent(activeEngine, requestId, NUVIO_ENGINE_EVENT_STREAM_STOPPED)
}
private suspend fun reclaimDiskCache(activeEngine: CPointer<nuvio_engine>) {
val requestId = memScoped {
val output = alloc<ULongVar>()
checkStatus(nuvio_engine_reclaim_disk_cache(activeEngine, 0uL, output.ptr))
output.value
}
awaitEvent(activeEngine, requestId, NUVIO_ENGINE_EVENT_DISK_CACHE_RECLAIMED)
}
private suspend fun awaitEvent(
activeEngine: CPointer<nuvio_engine>,
requestId: ULong,
expectedType: UInt,
): NativeEvent {
while (true) {
currentCoroutineContext().ensureActive()
when (val event = pollEvent(activeEngine)) {
null -> delay(20L)
else -> {
if (event.requestId == requestId && event.type == NUVIO_ENGINE_EVENT_TORRENT_ERROR) {
throw P2pStreamingException(event.message ?: "Torrent engine command failed")
}
if (event.requestId == requestId && event.type == expectedType) return event
}
}
}
}
private fun pollEvent(activeEngine: CPointer<nuvio_engine>): NativeEvent? = memScoped {
val event = alloc<nuvio_engine_event>()
nuvio_engine_event_init_sized(event.ptr, sizeOf<nuvio_engine_event>().toUInt())
val status = nuvio_engine_poll_event(activeEngine, event.ptr)
if (status == NUVIO_ENGINE_STATUS_NO_EVENT) return@memScoped null
checkStatus(status)
NativeEvent(
type = event.type,
requestId = event.request_id,
torrentId = event.torrent_id.toKString().ifBlank { null },
message = event.message.toKString().ifBlank { null },
fileIndex = event.file_index.takeUnless { it == UInt.MAX_VALUE },
fileSize = event.file_size,
streamId = event.stream_id.toKString().ifBlank { null },
streamUrl = event.stream_url.toKString().ifBlank { null },
)
}
private fun readAggregateStats(activeEngine: CPointer<nuvio_engine>): AggregateStats = memScoped {
val stats = alloc<nuvio_engine_stats>()
nuvio_engine_stats_init_sized(stats.ptr, sizeOf<nuvio_engine_stats>().toUInt())
checkStatus(nuvio_engine_get_stats(activeEngine, stats.ptr))
AggregateStats(
peers = stats.connected_peers.toInt(),
seeds = stats.connected_seeds.toInt(),
downloadSpeed = stats.download_rate_bytes_per_second.toLong(),
uploadSpeed = stats.upload_rate_bytes_per_second.toLong(),
payloadDownloaded = stats.total_payload_download_bytes.toLong(),
diskUsed = stats.disk_cache_used_bytes.toLong(),
diskProtected = stats.disk_cache_protected_bytes.toLong(),
)
}
private fun readStreamStats(
activeEngine: CPointer<nuvio_engine>,
streamId: String,
): StreamStats = memScoped {
val stats = alloc<nuvio_engine_stream_stats>()
nuvio_engine_stream_stats_init_sized(
stats.ptr,
sizeOf<nuvio_engine_stream_stats>().toUInt(),
)
checkStatus(nuvio_engine_get_stream_stats(activeEngine, streamId, stats.ptr))
StreamStats(
fileSize = stats.file_size.toLong(),
contiguousReady = stats.contiguous_ready_bytes.toLong(),
verified = stats.verified_file_bytes.toLong(),
delivered = stats.delivered_bytes.toLong(),
)
}
private fun updateCacheState(stats: AggregateStats) {
_cacheState.value = _cacheState.value.copy(
usedBytes = stats.diskUsed,
protectedBytes = stats.diskProtected,
hasMeasurement = true,
)
}
private fun checkStatus(status: UInt) {
if (status == NUVIO_ENGINE_STATUS_OK) return
val message = nuvio_engine_status_message(status)?.toKString()
?: "Nuvio Engine status $status"
throw P2pStreamingException(message)
}
private fun createDirectory(path: String) {
val manager = NSFileManager.defaultManager
check(manager.createDirectoryAtPath(
path,
withIntermediateDirectories = true,
attributes = null,
error = null,
)) { "Could not create Nuvio Engine directory" }
}
private fun ratio(value: Long, total: Long): Float =
if (total <= 0L) 0f else (value.toDouble() / total.toDouble()).toFloat().coerceIn(0f, 1f)
private fun log(message: String) {
NSLog("NuvioP2PDiag: $message")
}
private val DefaultTrackers = listOf(
"udp://zer0day.ch:1337/announce",
"udp://tracker.publictracker.xyz:6969/announce",
"udp://tracker.opentrackr.org:1337/announce",
"udp://open.demonii.com:1337/announce",
"udp://open.stealth.si:80/announce",
"http://tracker.renfei.net:8080/announce",
"udp://udp.tracker.projectk.org:23333/announce",
"udp://tracker.tryhackx.org:6969/announce",
"udp://tracker.torrent.eu.org:451/announce",
"udp://tracker.theoks.net:6969/announce",
"udp://tracker.startwork.cv:1337/announce",
"udp://tracker.qu.ax:6969/announce",
"udp://tracker.plx.im:6969/announce",
"udp://tracker.nyaa.vc:6969/announce",
"udp://tracker.iperson.xyz:6969/announce",
"udp://tracker.gmi.gd:6969/announce",
"udp://tracker.fnix.net:6969/announce",
"udp://tracker.flatuslifir.is:6969/announce",
"udp://tracker.ducks.party:1984/announce",
"udp://tracker.bluefrog.pw:2710/announce",
)
}

View file

@ -51,9 +51,11 @@ internal actual object PlatformLocalAccountDataCleaner {
"mdblist_use_trakt",
"mdblist_use_letterboxd",
"mdblist_use_audience",
"mdblist_use_mal",
"trakt_auth_payload",
"trakt_library_payload",
"trakt_settings_payload",
"library_display_settings_payload",
"pending_watch_progress_source",
"collection_mobile_settings_payload",
"collections_payload",

View file

@ -0,0 +1,15 @@
package com.nuvio.app.features.library
import com.nuvio.app.core.storage.ProfileScopedKey
import platform.Foundation.NSUserDefaults
actual object LibraryDisplaySettingsStorage {
private const val payloadKey = "library_display_settings_payload"
actual fun loadPayload(): String? =
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(payloadKey))
actual fun savePayload(payload: String) {
NSUserDefaults.standardUserDefaults.setObject(payload, forKey = ProfileScopedKey.of(payloadKey))
}
}

View file

@ -20,6 +20,7 @@ actual object MdbListSettingsStorage {
private const val useTraktKey = "mdblist_use_trakt"
private const val useLetterboxdKey = "mdblist_use_letterboxd"
private const val useAudienceKey = "mdblist_use_audience"
private const val useMalKey = "mdblist_use_mal"
private val syncKeys = listOf(
enabledKey,
apiKey,
@ -30,6 +31,7 @@ actual object MdbListSettingsStorage {
useTraktKey,
useLetterboxdKey,
useAudienceKey,
useMalKey,
)
actual fun loadEnabled(): Boolean? = loadBoolean(enabledKey)
@ -87,6 +89,12 @@ actual object MdbListSettingsStorage {
saveBoolean(useAudienceKey, enabled)
}
actual fun loadUseMal(): Boolean? = loadBoolean(useMalKey)
actual fun saveUseMal(enabled: Boolean) {
saveBoolean(useMalKey, enabled)
}
private fun loadBoolean(key: String): Boolean? {
val defaults = NSUserDefaults.standardUserDefaults
val scopedKey = ProfileScopedKey.of(key)
@ -111,6 +119,7 @@ actual object MdbListSettingsStorage {
loadUseTrakt()?.let { put(useTraktKey, encodeSyncBoolean(it)) }
loadUseLetterboxd()?.let { put(useLetterboxdKey, encodeSyncBoolean(it)) }
loadUseAudience()?.let { put(useAudienceKey, encodeSyncBoolean(it)) }
loadUseMal()?.let { put(useMalKey, encodeSyncBoolean(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
@ -127,5 +136,6 @@ actual object MdbListSettingsStorage {
payload.decodeSyncBoolean(useTraktKey)?.let(::saveUseTrakt)
payload.decodeSyncBoolean(useLetterboxdKey)?.let(::saveUseLetterboxd)
payload.decodeSyncBoolean(useAudienceKey)?.let(::saveUseAudience)
payload.decodeSyncBoolean(useMalKey)?.let(::saveUseMal)
}
}

View file

@ -7,6 +7,8 @@ internal actual object P2pSettingsStorage {
private const val p2pEnabledKey = "p2p_enabled"
private const val enableUploadKey = "enable_upload"
private const val hideTorrentStatsKey = "hide_torrent_stats"
private const val torrentProfileKey = "torrent_profile"
private const val cacheSizeKey = "cache_size"
actual fun loadP2pEnabled(): Boolean? =
loadBoolean(p2pEnabledKey)
@ -29,6 +31,18 @@ internal actual object P2pSettingsStorage {
saveBoolean(hideTorrentStatsKey, enabled)
}
actual fun loadTorrentProfile(): String? = loadString(torrentProfileKey)
actual fun saveTorrentProfile(profile: String) {
saveString(torrentProfileKey, profile)
}
actual fun loadCacheSize(): String? = loadString(cacheSizeKey)
actual fun saveCacheSize(size: String) {
saveString(cacheSizeKey, size)
}
private fun loadBoolean(keyBase: String): Boolean? {
val defaults = NSUserDefaults.standardUserDefaults
val key = ProfileScopedKey.of(keyBase)
@ -38,4 +52,11 @@ internal actual object P2pSettingsStorage {
private fun saveBoolean(keyBase: String, value: Boolean) {
NSUserDefaults.standardUserDefaults.setBool(value, forKey = ProfileScopedKey.of(keyBase))
}
private fun loadString(keyBase: String): String? =
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(keyBase))
private fun saveString(keyBase: String, value: String) {
NSUserDefaults.standardUserDefaults.setObject(value, forKey = ProfileScopedKey.of(keyBase))
}
}

View file

@ -51,8 +51,11 @@ actual fun PlatformPlayerSurface(
useYoutubeChunkedPlayback: Boolean,
modifier: Modifier,
playWhenReady: Boolean,
initialPositionMs: Long?,
initialPositionRequestKey: String?,
resizeMode: PlayerResizeMode,
useNativeController: Boolean,
onInitialPositionHandled: (key: String, handled: Boolean) -> Unit,
onControllerReady: (PlayerEngineController) -> Unit,
onSnapshot: (PlayerPlaybackSnapshot) -> Unit,
onError: (String?) -> Unit,

Some files were not shown because too many files have changed in this diff Show more