mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
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:
commit
e1811f0e5e
107 changed files with 4576 additions and 1257 deletions
193
.github/workflows/android-release.yml
vendored
Normal file
193
.github/workflows/android-release.yml
vendored
Normal 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[@]}"
|
||||
|
|
@ -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"))
|
||||
|
|
|
|||
BIN
composeApp/libs/lib-nuvio-engine-android-0.1.0.aar
Normal file
BIN
composeApp/libs/lib-nuvio-engine-android-0.1.0.aar
Normal file
Binary file not shown.
5
composeApp/proguard-rules.pro
vendored
5
composeApp/proguard-rules.pro
vendored
|
|
@ -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 { *; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -426,12 +426,14 @@
|
|||
<string name="compose_settings_page_licenses_attributions">Licence & 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">ΔΕΔΟΜΕΝΑ & ΥΠΗΡΕΣΙΕΣ</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 l’art, des crédits, des métadonnées d’épisodes et plus depuis TMDB.</string>
|
||||
<string name="settings_integrations_mdblist_description">Ajoutez des notes externes d’IMDb, Rotten Tomatoes, Metacritic et d’autres aux pages de détails.</string>
|
||||
<string name="settings_integrations_debrid_description">Connectez des comptes pour les liens et l’accè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 d’abord 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 n’est 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 n’appuyez pas sur Regarder, car les liens sont préparés à l’avance.</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 l’addon</string>
|
||||
<string name="settings_stream_addon_logo_description">Affiche le logo et le nom de l’addon à 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 l’espace 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 l’audio spatial et de la sortie multicanal.</string>
|
||||
<string name="settings_playback_ios_audio_output_audiounit_desc">Utilise l’ancienne 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 & 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>
|
||||
|
|
|
|||
|
|
@ -404,6 +404,7 @@
|
|||
<string name="compose_settings_page_licenses_attributions">Lisensi & 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 & 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>
|
||||
|
|
|
|||
|
|
@ -404,6 +404,7 @@
|
|||
<string name="compose_settings_page_licenses_attributions">Lisensi & 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 & 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -392,6 +392,7 @@
|
|||
<string name="compose_settings_page_licenses_attributions">Lisenser & 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 & 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -393,6 +393,7 @@
|
|||
<string name="compose_settings_page_licenses_attributions">Licenças & 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 & 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -394,6 +394,7 @@
|
|||
<string name="compose_settings_page_licenses_attributions">Lisanslar & 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">Açıklama şablonu</string>
|
||||
|
|
|
|||
|
|
@ -425,12 +425,14 @@
|
|||
<string name="compose_settings_page_licenses_attributions">Giấy phép & 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ợ & 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>
|
||||
|
|
|
|||
|
|
@ -427,12 +427,14 @@
|
|||
<string name="compose_settings_page_licenses_attributions">Licenses & 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 & 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 & 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 A–Z</string>
|
||||
<string name="library_sort_title_desc">Title Z–A</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) -->
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -427,7 +427,7 @@ object SyncManager {
|
|||
continue
|
||||
}
|
||||
|
||||
TraktAuthRepository.ensureLoaded()
|
||||
TraktAuthRepository.ensureLoaded(profileId)
|
||||
TraktSettingsRepository.ensureLoaded()
|
||||
|
||||
val traktAuthenticated = TraktAuthRepository.isAuthenticated.value
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.nuvio.app.features.library
|
||||
|
||||
internal expect object LibraryDisplaySettingsStorage {
|
||||
fun loadPayload(): String?
|
||||
fun savePayload(payload: String)
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) ->
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ enum class ContinueWatchingSectionStyle {
|
|||
enum class ContinueWatchingSortMode {
|
||||
DEFAULT,
|
||||
STREAMING_STYLE,
|
||||
SPLIT_UPCOMING,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue