diff --git a/.github/images/download-cloudstream-pre.png b/.github/images/download-cloudstream-pre.png deleted file mode 100644 index db1f90552..000000000 Binary files a/.github/images/download-cloudstream-pre.png and /dev/null differ diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml index 056022d22..b5960d5d9 100644 --- a/.github/workflows/build_to_archive.yml +++ b/.github/workflows/build_to_archive.yml @@ -71,10 +71,7 @@ jobs: SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} - TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - MAL_KEY: ${{ secrets.MAL_KEY }} - ANILIST_KEY: ${{ secrets.ANILIST_KEY }} - uses: actions/checkout@v6 with: diff --git a/.github/workflows/instrumented-tests.yml b/.github/workflows/instrumented-tests.yml deleted file mode 100644 index 34c6eaf0b..000000000 --- a/.github/workflows/instrumented-tests.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Instrumented Tests - -on: - issue_comment: - types: [created] - -permissions: - contents: read - pull-requests: write - -jobs: - instrumented-tests: - runs-on: ubuntu-latest - if: | - github.event.issue.pull_request && - github.event.comment.body == '/run-tests' - steps: - - name: Check permission - uses: actions/github-script@v9 - with: - script: | - const login = context.payload.comment.user.login; - const association = context.payload.comment.author_association; - const allowed = ['OWNER', 'MEMBER']; - - const isAllowed = - login === 'Luna712' || - allowed.includes(association); - - if (!isAllowed) { - core.setFailed(`User ${login} is not permitted to trigger this workflow.`); - } - - - uses: actions/checkout@v6 - with: - ref: refs/pull/${{ github.event.issue.number }}/head - - - name: Post started comment - uses: actions/github-script@v9 - with: - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'Instrumented tests are running. [View live progress](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})' - }) - - - name: Set up JDK 17 - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 17 - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 - with: - cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - cache-read-only: false - - - name: Get target SDK - id: sdk - run: | - TARGET_SDK=$(grep 'targetSdk' gradle/libs.versions.toml | grep -o '[0-9]\+' | head -1) - echo "version=$TARGET_SDK" >> $GITHUB_OUTPUT - - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: Run Instrumented Tests - id: tests - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ steps.sdk.outputs.version }} - arch: x86_64 - profile: Nexus 6 - script: ./gradlew connectedPrereleaseDebugAndroidTest - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v7 - with: - name: instrumented-test-results - path: '**/build/reports/androidTests/' - - - name: Post finished comment - if: always() - uses: actions/github-script@v9 - with: - script: | - const success = '${{ steps.tests.outcome }}' === 'success'; - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: success - ? 'Instrumented tests passed. [View results](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})' - : 'Instrumented tests failed. [View results](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})' - }) diff --git a/.github/workflows/issue_action.yml b/.github/workflows/issue_action.yml new file mode 100644 index 000000000..e354d657d --- /dev/null +++ b/.github/workflows/issue_action.yml @@ -0,0 +1,98 @@ +name: Issue automatic actions + +on: + issues: + types: [opened] + +permissions: + contents: read + issues: write + +jobs: + issue-moderator: + runs-on: ubuntu-latest + steps: + - name: Generate access token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.GH_APP_ID }} + private_key: ${{ secrets.GH_APP_KEY }} + + - name: Similarity analysis + id: similarity + uses: actions-cool/issues-similarity-analysis@v1 + with: + token: ${{ steps.generate_token.outputs.token }} + filter-threshold: 0.60 + title-excludes: '' + comment-title: | + ### Your issue looks similar to these issues: + Please close if duplicate. + comment-body: '${index}. ${similarity} #${number}' + + - name: Label if possible duplicate + if: steps.similarity.outputs.similar-issues-found =='true' + uses: actions/github-script@v9 + with: + github-token: ${{ steps.generate_token.outputs.token }} + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ["possible duplicate"] + }) + + - uses: actions/checkout@v6 + + - name: Automatically close issues that dont follow the issue template + uses: lucasbento/auto-close-issues@v1.0.2 + with: + github-token: ${{ steps.generate_token.outputs.token }} + issue-close-message: | + @${issue.user.login}: hello! :wave: + This issue is being automatically closed because it does not follow the issue template." + closed-issues-label: "invalid" + + - name: Check if issue mentions a provider + id: provider_check + env: + GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}" + run: | + wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py" + pip3 install httpx + RES="$(python3 ./check_issue.py)" + echo "name=${RES}" >> $GITHUB_OUTPUT + + - name: Comment if issue mentions a provider + if: steps.provider_check.outputs.name != 'none' + uses: actions-cool/issues-helper@v3 + with: + actions: 'create-comment' + token: ${{ steps.generate_token.outputs.token }} + body: | + Hello ${{ github.event.issue.user.login }}. + Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM). + + Found provider name: `${{ steps.provider_check.outputs.name }}` + + - name: Label if mentions provider + if: steps.provider_check.outputs.name != 'none' + uses: actions/github-script@v9 + with: + github-token: ${{ steps.generate_token.outputs.token }} + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ["possible provider issue"] + }) + + - name: Add eyes reaction to all issues + uses: actions-cool/emoji-helper@v1.0.0 + with: + type: 'issue' + token: ${{ steps.generate_token.outputs.token }} + emoji: 'eyes' diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index f089afa8f..d9a20a04b 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -62,10 +62,7 @@ jobs: SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} - TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - MAL_KEY: ${{ secrets.MAL_KEY }} - ANILIST_KEY: ${{ secrets.ANILIST_KEY }} - name: Create pre-release uses: marvinpinto/action-automatic-releases@latest diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 433816e0f..675ce3b2f 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -26,13 +26,8 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} cache-read-only: false - - name: Ensure binary compatibility - # This is to ensure that your code is backwards compatible. - # If this fails you need to add a @Prerelease annotation to new code. - run: ./gradlew library:checkKotlinAbi - - name: Run Gradle - run: ./gradlew assemblePrereleaseDebug lint check + run: ./gradlew assemblePrereleaseDebug lint - name: Upload Artifact uses: actions/upload-artifact@v7 diff --git a/COMPOSE.md b/COMPOSE.md deleted file mode 100644 index 8d83a50ae..000000000 --- a/COMPOSE.md +++ /dev/null @@ -1,21 +0,0 @@ -# Migration guide to Compose - -### 1. MVI instead of MVVM - -The current design of CloudStream loosely uses the MVVM architecture. - -This means that the UI invokes the viewmodel with function calls, and it responds with LiveData fields that are observed. While this has worked, it generates a lot of boilerplate and has created some friction. - -To make it easier to work with Compose, the new architecture will be based on MVI. In short this means that the viewmodel exposes a singular immutable class that is observed, and receives all UI events with a singular event that is a sealed class. All the UI should be able to be recreated based on this singular state class, and all interactions should be able to be replayed using only the event callback. - -For a more detailed overview, see: https://www.youtube.com/watch?v=b2z1jvD4VMQ - -This is part of the effort to make CloudStream cross platform, as it allows us to decouple UI and logic. - -### 2. KMP-compatible libraries - -We plan to leverage Kotlin's KMP project to compile our code to different architectures. However, this requires us to only use KMP-compatible libraries, no Java. Therefore any pull requests must ensure that they use KMP-compatible libraries only. - -### 3. UI Changes - -While migrating to the new compose UI, you also have the opportunity to change the UI. However, this should only be to freshen up the UI, not completely redesign it. It is also important to stress that this process should not lose any features of the old UI, and be very conservative with adding new features. \ No newline at end of file diff --git a/README.md b/README.md index 237d7d8fc..c2492c5d8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # CloudStream **⚠️ Warning: By default, this app doesn't provide any video sources; you have to install extensions to add functionality to the app.** -

Download Cloudstream Pre-Release

[![Discord](https://invidget.switchblade.xyz/5Hus6fM)](https://discord.gg/5Hus6fM) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c714368ee..ae5301929 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,9 +8,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile plugins { alias(libs.plugins.android.application) alias(libs.plugins.dokka) - alias(libs.plugins.kotlin.serialization) - alias(libs.plugins.compose.multiplatform) - alias(libs.plugins.compose.compiler) } val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) @@ -106,8 +103,8 @@ android { applicationId = "com.lagradost.cloudstream3" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = libs.versions.versionCode.get().toInt() - versionName = libs.versions.versionName.get() + versionCode = 68 + versionName = "4.7.0" manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get() @@ -129,16 +126,6 @@ android { "SIMKL_CLIENT_SECRET", "\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\"" ) - buildConfigField( - "String", - "MAL_KEY", - "\"" + (System.getenv("MAL_KEY") ?: localProperties["mal.key"]) + "\"" - ) - buildConfigField( - "String", - "ANILIST_KEY", - "\"" + (System.getenv("ANILIST_KEY") ?: localProperties["anilist.key"]) + "\"" - ) testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -219,11 +206,9 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.json) androidTestImplementation(libs.core) - androidTestImplementation(libs.espresso.core) + implementation(libs.junit.ktx) androidTestImplementation(libs.ext.junit) - androidTestImplementation(libs.instancio.core) - androidTestImplementation(libs.junit.ktx) - androidTestImplementation(libs.kotlin.test) + androidTestImplementation(libs.espresso.core) // Android Core & Lifecycle implementation(libs.core.ktx) @@ -234,7 +219,6 @@ dependencies { implementation(libs.bundles.lifecycle) implementation(libs.bundles.navigation) implementation(libs.kotlinx.collections.immutable) - implementation(libs.kotlinx.serialization.json) // JSON Parser // Design & UI implementation(libs.preference.ktx) @@ -270,19 +254,14 @@ dependencies { // Extensions & Other Libs implementation(libs.jsoup) // HTML Parser - implementation(libs.ksoup) // HTML Parser implementation(libs.rhino) // Run JavaScript + implementation(libs.fuzzywuzzy) // Library/Ext Searching with Levenshtein Distance implementation(libs.safefile) // To Prevent the URI File Fu*kery coreLibraryDesugaring(libs.desugar.jdk.libs.nio) // NIO Flavor Needed for NewPipeExtractor implementation(libs.conscrypt.android) // To Fix SSL Fu*kery on Android 9 implementation(libs.jackson.module.kotlin) // JSON Parser implementation(libs.zipline) - // Temp/deprecated; will be removed once extensions have time to migrate from using it - implementation("com.google.code.gson:gson:2.11.0") - // Deprecated; will be removed once extensions have time to migrate from using it - implementation("me.xdrop:fuzzywuzzy:1.4.0") - // Torrent Support implementation(libs.torrentserver) @@ -290,12 +269,7 @@ dependencies { implementation(libs.work.runtime.ktx) implementation(libs.nicehttp) // HTTP Lib - implementation(libs.bundles.compose) - implementation(libs.activity.compose) - implementation(libs.kotlinx.io.core) // Logcat parser - implementation(project(":library")) - implementation(project(":shared")) } tasks.register("androidSourcesJar") { @@ -332,6 +306,7 @@ tasks.withType { compilerOptions { jvmTarget.set(javaTarget) jvmDefault.set(JvmDefaultMode.ENABLE) + freeCompilerArgs.add("-Xannotation-default-target=param-property") optIn.addAll( "com.lagradost.cloudstream3.InternalAPI", "com.lagradost.cloudstream3.Prerelease", diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index 1d596ccee..4c5cdea5b 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -55,6 +55,12 @@ class ExampleInstrumentedTest { return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView } } + @Test + fun providersExist() { + Assert.assertTrue(getAllProviders().isNotEmpty()) + println("Done providersExist") + } + @Throws private inline fun testAllLayouts( activity: Activity, @@ -163,7 +169,7 @@ class ExampleInstrumentedTest { TestingUtils.getDeferredProviderTests( this, getAllProviders(), - ) { _, _, _ -> } + ) { _, _ -> } } } } diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt deleted file mode 100644 index 84ef1fee0..000000000 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.lagradost.cloudstream3 - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.lagradost.cloudstream3.SkipSerializationTest -import com.lagradost.cloudstream3.utils.AppUtils.toJson -import dalvik.system.DexFile -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.InternalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.serializer -import kotlinx.serialization.serializerOrNull -import org.instancio.Instancio -import org.junit.Test -import org.junit.runner.RunWith -import kotlin.reflect.KClass -import kotlin.reflect.jvm.jvmName -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -@RunWith(AndroidJUnit4::class) -class SerializationClassTester { - // Same as app, or using app reference - val jacksonMapper = mapper - val kotlinxMapper = json - - @Test - fun isIdenticalSerialization() { - val serializableClasses = findSerializableClasses("com.lagradost") - println("Number of serializable classes: ${serializableClasses.size}") - - val failures = mutableListOf() - - serializableClasses.forEach { kClass -> - runCatching { - val instance = Instancio.of(kClass.java).withMaxDepth(10).create() - - val jacksonJson = jacksonMapper.writeValueAsString(instance) - val kotlinxJson = serializeWithKotlinx(kClass, instance) - - assertEquals( - jacksonJson, - kotlinxJson, - """ - Serialization mismatch for: - ${kClass.qualifiedName} - - Jackson: - $jacksonJson - - Kotlinx: - $kotlinxJson - - """.trimIndent() - ) - println("Identical serialization for: ${kClass.jvmName}") - }.onFailure { e -> - failures.add("FAILED ${kClass.qualifiedName}: ${e.message}") - } - } - - if (failures.isNotEmpty()) { - throw AssertionError("${failures.size} class(es) failed:\n${failures.joinToString("\n")}") - } - } - - @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) - @Test - fun isIdenticalDeserialization() { - val serializableClasses = findSerializableClasses("com.lagradost") - println("Number of serializable classes: ${serializableClasses.size}") - - val failures = mutableListOf() - - serializableClasses.forEach { kClass -> - runCatching { - val instance = Instancio.of(kClass.java).withMaxDepth(10).create() - // Convert to JSON to get example JSON object - // We prefer jackson here because the app may have many jackson JSON strings in local storage - val originalJson = jacksonMapper.writeValueAsString(instance) - - // Create an object from the JSON using kotlinx - val serializer = - kClass.serializerOrNull() ?: kotlinxMapper.serializersModule.getContextual(kClass) - assertNotNull(serializer, "The class: ${kClass.jvmName} must be serializable!") - val kotlinxDecoded = kotlinxMapper.decodeFromString(serializer, originalJson) - - // Create an object from the JSON using jackson - val mapperDecoded = jacksonMapper.readValue(originalJson, kClass.java) - - // Deep inspect both object using the mapper toJson function. - // This deep equality check can be performed using other methods, but this just works. - val jacksonJson = mapperDecoded.toJson() - val kotlinxJson = kotlinxDecoded.toJson() - - assertEquals( - jacksonJson, - kotlinxJson, - """ - Serialization mismatch for: - ${kClass.qualifiedName} - - Jackson: - $jacksonJson - - Kotlinx: - $kotlinxJson - - """.trimIndent() - ) - println("Identical deserialization for: ${kClass.jvmName}") - }.onFailure { e -> - failures.add("FAILED ${kClass.qualifiedName}: ${e.message}") - } - } - - if (failures.isNotEmpty()) { - throw AssertionError("${failures.size} class(es) failed:\n${failures.joinToString("\n")}") - } - } - - // DEX files are the best solution to read all our classes dynamically. - // classgraph could be used instead, but it only gives results on the JVM, not Android. - @Suppress("DEPRECATION") - private fun findSerializableClasses(packageName: String): List> { - val context = InstrumentationRegistry - .getInstrumentation() - .targetContext - - val dexFile = DexFile(context.packageCodePath) - return dexFile.entries() - .toList() - .filter { it.startsWith(packageName) } - .mapNotNull { - runCatching { Class.forName(it).kotlin }.getOrNull() - }.filter { kClass -> - // Not possible to use .hasAnnotation() on newer Android versions. - kClass.java.annotations.any { it is Serializable } - && kClass.java.annotations.none { it is SkipSerializationTest } - && !kClass.isAbstract - } - } - - @OptIn(InternalSerializationApi::class) - @Suppress("UNCHECKED_CAST") - private fun serializeWithKotlinx( - kClass: KClass<*>, - value: Any - ): String { - val serializer = kClass.serializer() as KSerializer - return kotlinxMapper.encodeToString(serializer, value) - } -} diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/UriSerializerTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/UriSerializerTest.kt deleted file mode 100644 index 3ffd37124..000000000 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/UriSerializerTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.lagradost.cloudstream3.utils.serializers - -import android.net.Uri -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJson -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test - -@Serializable -data class UriData( - @Serializable(with = UriSerializer::class) - @SerialName("uri") val uri: Uri = Uri.EMPTY, -) - -class UriSerializerTest { - - @Test - fun uriSerializerSerializesUriToString() { - val data = UriData(uri = Uri.parse("https://example.com/path?query=1")) - val result = data.toJson() - assertTrue(result.contains("https://example.com/path?query=1")) - } - - @Test - fun uriSerializerDeserializesStringToUri() { - val input = """{"uri":"https://example.com/path?query=1"}""" - val result = parseJson(input) - assertEquals(Uri.parse("https://example.com/path?query=1"), result.uri) - } - - @Test - fun uriSerializerRoundtripsCorrectly() { - val data = UriData(uri = Uri.parse("https://example.com/path?query=1")) - val encoded = data.toJson() - val decoded = parseJson(encoded) - assertEquals(data.uri, decoded.uri) - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3ff59515e..b2c7091b0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,47 +22,6 @@ android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -253,10 +212,6 @@ - - setKey(path: String, value: T) = CloudStreamApp.setKey(path, value) @@ -38,7 +38,7 @@ class AcraApplication { @Deprecated( message = "AcraApplication is deprecated, use CloudStreamApp instead", replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.setKey(folder, path, value)"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.WARNING ) fun setKey(folder: String, path: String, value: T) = CloudStreamApp.setKey(folder, path, value) @@ -46,7 +46,7 @@ class AcraApplication { @Deprecated( message = "AcraApplication is deprecated, use CloudStreamApp instead", replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(path, defVal)"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.WARNING ) inline fun getKey(path: String, defVal: T?): T? = CloudStreamApp.getKey(path, defVal) @@ -54,7 +54,7 @@ class AcraApplication { @Deprecated( message = "AcraApplication is deprecated, use CloudStreamApp instead", replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(path)"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.WARNING ) inline fun getKey(path: String): T? = CloudStreamApp.getKey(path) @@ -62,7 +62,7 @@ class AcraApplication { @Deprecated( message = "AcraApplication is deprecated, use CloudStreamApp instead", replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(folder, path)"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.WARNING ) inline fun getKey(folder: String, path: String): T? = CloudStreamApp.getKey(folder, path) @@ -70,7 +70,7 @@ class AcraApplication { @Deprecated( message = "AcraApplication is deprecated, use CloudStreamApp instead", replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(folder, path, defVal)"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.WARNING ) inline fun getKey(folder: String, path: String, defVal: T?): T? = CloudStreamApp.getKey(folder, path, defVal) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt index 6ff3ab54d..a9cd9c01e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt @@ -113,7 +113,7 @@ class CloudStreamApp : Application(), SingletonImageLoader.Factory { get() = _context?.get() private set(value) { _context = WeakReference(value) - setContext(value) + setContext(WeakReference(value)) } fun getKeyClass(path: String, valueType: Class): T? { @@ -170,11 +170,11 @@ class CloudStreamApp : Application(), SingletonImageLoader.Factory { } /** Will fall back to WebView if in TV or emulator layout. */ - fun openBrowser(url: String, activity: Activity?) { + fun openBrowser(url: String, activity: FragmentActivity?) { openBrowser( url, isLayout(TV or EMULATOR), - (activity as? FragmentActivity)?.supportFragmentManager?.fragments?.lastOrNull() + activity?.supportFragmentManager?.fragments?.lastOrNull() ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 8daf168c1..ed0aaf9b7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -52,7 +52,6 @@ import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.updateTv import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod @@ -267,9 +266,7 @@ object CommonActivity { } // Ask for notification permissions on Android 13 - // Not while biometric locked, the dialog cancels the biometric prompt if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - !isAuthEnabled(componentActivity) && ContextCompat.checkSelfPermission( componentActivity, Manifest.permission.POST_NOTIFICATIONS @@ -582,10 +579,8 @@ object CommonActivity { // TODO: Figure out why removing the check for SearchAutoComplete seems // to break focus on TV as it shouldn't need to be used. - // Also handle KEYCODE_ENTER here because some remotes (e.g. LG Magic Remote) - // send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER when clicking the OK button. @SuppressLint("RestrictedApi") - if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) && + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) ) { showInputMethod(act.currentFocus?.findFocus()) @@ -606,4 +601,4 @@ object CommonActivity { } return null } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index c13b2600c..8a98bd297 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -171,19 +171,13 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.setNavigationBarColorCompat -import com.lagradost.cloudstream3.utils.UIHelper.showProgress import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API -import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager import com.lagradost.cloudstream3.utils.setText import com.lagradost.cloudstream3.utils.setTextHtml import com.lagradost.cloudstream3.utils.txt -import com.lagradost.cloudstream4.compose.CloudStreamThemeAppSettings -import com.lagradost.cloudstream4.theme.CloudStreamTheme import com.lagradost.safefile.SafeFile -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.io.File @@ -193,8 +187,10 @@ import java.net.URLDecoder import java.nio.charset.Charset import kotlin.math.abs import kotlin.math.absoluteValue -import kotlin.reflect.full.createInstance import kotlin.system.exitProcess +import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCallback { companion object { @@ -412,10 +408,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa return true } - val matchedApi = apis.filter { str.startsWith(it.mainUrl) }.firstOrNull() - if (matchedApi != null) { - loadResult(str, matchedApi.name, "") - return true + synchronized(apis) { + for (api in apis) { + if (str.startsWith(api.mainUrl)) { + loadResult(str, api.name, "") + return true + } + } } } } @@ -788,6 +787,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } } + val builder = NavOptions.Builder().setLaunchSingleTop(true).setRestoreState(true) .setEnterAnim(R.anim.enter_anim) .setExitAnim(R.anim.exit_anim) @@ -809,33 +809,32 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } } + private val pluginsLock = Mutex() private fun onAllPluginsLoaded(success: Boolean = false) { ioSafe { pluginsLock.withLock { - allProviders.withLock { + synchronized(allProviders) { // Load cloned sites after plugins have been loaded since clones depend on plugins. try { getKey>(USER_PROVIDER_API)?.let { list -> list.forEach { custom -> - allProviders.firstOrNull { - it::class.simpleName == custom.parentClassName - }?.let { - allProviders.add( - it::class.createInstance().apply { - name = custom.name - lang = custom.lang - mainUrl = custom.url.trimEnd('/') - canBeOverridden = false - } - ) - } + allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } + ?.let { + allProviders.add( + it.javaClass.getDeclaredConstructor().newInstance() + .apply { + name = custom.name + lang = custom.lang + mainUrl = custom.url.trimEnd('/') + canBeOverridden = false + }) + } } } // it.hashCode() is not enough to make sure they are distinct - apis = allProviders.distinctBy { - it.lang + it.name + it.mainUrl + it::class.qualifiedName - } + apis = + allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name } APIHolder.apiMap = null } catch (e: Exception) { logError(e) @@ -1217,7 +1216,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa // backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting? safe { val appVer = BuildConfig.VERSION_NAME - val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: "" + val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: "" if (appVer != lastAppAutoBackup) { setKey("VERSION_NAME", BuildConfig.VERSION_NAME) if (lastAppAutoBackup.isEmpty()) return@safe @@ -1299,14 +1298,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa padRight = false, padTop = false ) - - composeView.apply { - setContent { - CloudStreamThemeAppSettings { - MainActivityScreen.Content() - } - } - } } // overscan @@ -1379,7 +1370,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } //Automatically download not existing plugins, using mode specified. - /*val autoDownloadPlugin = AutoDownloadMode.getEnum( + val autoDownloadPlugin = AutoDownloadMode.getEnum( settingsManager.getInt( getString(R.string.auto_download_plugins_key), 0 @@ -1390,7 +1381,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa this@MainActivity, autoDownloadPlugin ) - }*/ + } } ioSafe { @@ -1439,9 +1430,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa else -> { resultviewPreviewBookmark.isEnabled = false - resultviewPreviewBookmark.showProgress() - //resultviewPreviewBookmark.setIconResource(R.drawable.ic_baseline_bookmark_border_24) - //resultviewPreviewBookmark.setText(R.string.loading) + resultviewPreviewBookmark.setIconResource(R.drawable.ic_baseline_bookmark_border_24) + resultviewPreviewBookmark.setText(R.string.loading) } } } @@ -1667,7 +1657,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ioSafe { initAll() // No duplicates (which can happen by registerMainAPI) - apis = allProviders.distinctBy { it } + apis = synchronized(allProviders) { + allProviders.distinctBy { it } + } } // val navView: BottomNavigationView = findViewById(R.id.nav_view) @@ -1975,7 +1967,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa if (BuildConfig.DEBUG) { var providersAndroidManifestString = "Current androidmanifest should be:\n" - allProviders.withLock { + synchronized(allProviders) { for (api in allProviders) { providersAndroidManifestString += "(HAS_DONE_SETUP_KEY, false) != true) { + if (getKey(HAS_DONE_SETUP_KEY, false) != true) { navController.navigate(R.id.navigation_setup_language) // If no plugins bring up extensions screen } else if (PluginManager.getPluginsOnline().isEmpty() diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivityScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivityScreen.kt deleted file mode 100644 index 9bab11562..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivityScreen.kt +++ /dev/null @@ -1,266 +0,0 @@ -package com.lagradost.cloudstream3 - -import android.text.format.Formatter.formatFileSize -import androidx.activity.ComponentActivity -import androidx.activity.compose.LocalActivity -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.viewmodel.compose.viewModel -import com.lagradost.cloudstream3.mvvm.getStackTracePretty -import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.ui.settings.ApkUpdater -import com.lagradost.cloudstream3.ui.settings.GithubReleases -import com.lagradost.cloudstream3.ui.settings.GithubReleases.APK_CONTENT_TYPE -import com.lagradost.cloudstream3.ui.settings.GithubReleases.APK_PRERELEASE -import com.lagradost.cloudstream3.ui.settings.GithubReleases.APK_REPOSITORY -import com.lagradost.cloudstream3.ui.settings.GithubReleases.APK_USERNAME -import com.lagradost.cloudstream3.utils.GitInfo.currentCommitHash -import com.lagradost.cloudstream3.utils.parseMarkdown -import com.lagradost.cloudstream4.compose.BlackButton -import com.lagradost.cloudstream4.compose.Screen -import com.lagradost.cloudstream4.compose.WhiteButton -import com.lagradost.cloudstream4.rememberAppSettings -import com.lagradost.cloudstream4.viewmodels.GithubAction -import com.lagradost.cloudstream4.viewmodels.GithubAction.AutoSearchForUpdate -import com.lagradost.cloudstream4.viewmodels.GithubAction.Dismiss -import com.lagradost.cloudstream4.viewmodels.GithubAction.SearchForUpdate -import com.lagradost.cloudstream4.viewmodels.GithubAction.SkipUpdate -import com.lagradost.cloudstream4.viewmodels.GithubAction.Update -import com.lagradost.cloudstream4.viewmodels.GithubState -import com.lagradost.cloudstream4.viewmodels.GithubUpdateDialogState -import com.lagradost.cloudstream4.viewmodels.GithubViewModel -import com.mihon.material.padding - -object MainActivityScreen : Screen { - private var hasSearchedForUpdate: Boolean = false - - @Composable - fun githubViewModel(): GithubViewModel? { - // By having the ComponentActivity as the owner we can have a singleton viewmodel - val activity = LocalActivity.current as? ComponentActivity ?: return null - val settings = rememberAppSettings() - return viewModel(viewModelStoreOwner = activity) { - GithubViewModel( - remoteUserName = APK_USERNAME, - remoteRepository = APK_REPOSITORY, - remotePrereleaseTag = APK_PRERELEASE, - remoteContentType = APK_CONTENT_TYPE, - versionName = BuildConfig.VERSION_NAME, - isPrerelease = BuildConfig.FLAVOR == "prerelease", - isDebug = BuildConfig.DEBUG, - buildSha = activity.currentCommitHash(), - settings = settings, - updater = ApkUpdater, - repository = GithubReleases, - ).apply { - /** Only search once per session when not in safe mode */ - if (!hasSearchedForUpdate && !PluginManager.isSafeMode()) { - hasSearchedForUpdate = true - onAction(AutoSearchForUpdate) - } - } - } - } - - @Composable - override fun Content() { - GithubView() - } - - @Composable - fun GithubView() { - val updateViewModel = githubViewModel() ?: return - val state by updateViewModel.state.collectAsState() - GithubState(state, updateViewModel::onAction) - } - - @Composable - fun GithubState(updaterState: GithubState, onAction: (GithubAction) -> Unit) { - val dialog = updaterState.dialog ?: return - - val title: String - val body: @Composable () -> Unit - val confirmButton: @Composable () -> Unit - val dismissButton: @Composable () -> Unit - val cancelable: Boolean - - when (val state = dialog.state) { - is GithubUpdateDialogState.Error -> { - // Only show error if it is initialized by the user - if (!dialog.isFromUser) return - - cancelable = true - title = stringResource(R.string.download_failed) - body = { Text(text = state.error.getStackTracePretty()) } - confirmButton = { - WhiteButton(text = stringResource(R.string.check_for_update), onClick = { - onAction(SearchForUpdate) - }) - } - dismissButton = { - BlackButton(text = stringResource(R.string.ok), onClick = { - onAction(Dismiss) - }) - } - } - - GithubUpdateDialogState.Loading -> { - // Only show loading if it is initialized by the user - if (!dialog.isFromUser) return - - cancelable = true - title = stringResource(R.string.loading) - body = { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.onBackground, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - ) - } - confirmButton = {} - dismissButton = { - BlackButton(text = stringResource(R.string.cancel), onClick = { - onAction(Dismiss) - }) - } - } - - GithubUpdateDialogState.NoUpdateFound -> { - // Only show error if it is initialized by the user - if (!dialog.isFromUser) return - - cancelable = true - title = stringResource(R.string.no_update_found) - body = {} - confirmButton = { - WhiteButton(text = stringResource(R.string.ok), onClick = { - onAction(Dismiss) - }) - } - dismissButton = {} - } - - is GithubUpdateDialogState.UpdateFound -> { - cancelable = true - title = stringResource( - R.string.new_update_format, - (state.oldSha ?: BuildConfig.VERSION_NAME), - (state.newSha ?: state.file.displayName) - ) - body = { - Text( - text = parseMarkdown(state.file.changeLog), - modifier = Modifier.verticalScroll(rememberScrollState()) - ) - } - confirmButton = { - WhiteButton(text = stringResource(R.string.update), onClick = { - onAction(Update(state.file)) - }) - } - dismissButton = { - BlackButton(text = stringResource(R.string.skip_update), onClick = { - onAction(SkipUpdate(state.file)) - onAction(Dismiss) - }) - BlackButton(text = stringResource(R.string.cancel), onClick = { - onAction(Dismiss) - }) - } - } - - is GithubUpdateDialogState.DownloadProgress -> { - cancelable = false - title = stringResource(R.string.update_notification_downloading) - body = { - val total = state.total - if (total != null) { - val progress = - (state.progress.toFloat() / total.toFloat()).coerceIn(0.0f, 1.0f) - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - // Round down the progress to avoid 100% - text = "${(progress * 100.0f).toInt()}% (${ - formatFileSize( - LocalContext.current, - state.progress - ) - } / ${ - formatFileSize( - LocalContext.current, - total - ) - })" - ) - Spacer(modifier = Modifier.height(MaterialTheme.padding.medium)) - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.onBackground, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - progress = { progress } - ) - } - } else { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - text = formatFileSize( - LocalContext.current, - state.progress - ) - ) - Spacer(modifier = Modifier.height(MaterialTheme.padding.medium)) - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.onBackground, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - ) - } - } - } - confirmButton = {} - dismissButton = { - BlackButton(text = stringResource(R.string.cancel), onClick = { - onAction(Dismiss) - }) - } - } - } - - AlertDialog( - properties = DialogProperties(usePlatformDefaultWidth = false), - containerColor = MaterialTheme.colorScheme.background, - onDismissRequest = { - if (cancelable) { - onAction(Dismiss) - } - }, - title = { Text(text = title) }, - text = body, - confirmButton = confirmButton, - dismissButton = dismissButton - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/VideoClickAction.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/VideoClickAction.kt index 0fdccb476..4843b7617 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/VideoClickAction.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/actions/VideoClickAction.kt @@ -20,10 +20,8 @@ import com.lagradost.cloudstream3.actions.temp.MpvExPackage import com.lagradost.cloudstream3.actions.temp.MpvKtPackage import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage import com.lagradost.cloudstream3.actions.temp.MpvPackage -import com.lagradost.cloudstream3.actions.temp.MpvRxPackage import com.lagradost.cloudstream3.actions.temp.MpvYTDLPackage import com.lagradost.cloudstream3.actions.temp.NextPlayerPackage -import com.lagradost.cloudstream3.actions.temp.OnlyPlayer import com.lagradost.cloudstream3.actions.temp.PlayInBrowserAction import com.lagradost.cloudstream3.actions.temp.PlayMirrorAction import com.lagradost.cloudstream3.actions.temp.ViewM3U8Action @@ -34,17 +32,18 @@ import com.lagradost.cloudstream3.actions.temp.fcast.FcastAction import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.result.LinkLoadingResult import com.lagradost.cloudstream3.ui.result.ResultEpisode -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.UiText import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.util.concurrent.Callable import java.util.concurrent.FutureTask +import kotlin.reflect.jvm.jvmName object VideoClickActionHolder { - val allVideoClickActions = atomicListOf( + val allVideoClickActions = threadSafeListOf( // Default PlayInBrowserAction(), CopyClipboardAction(), @@ -65,8 +64,6 @@ object VideoClickActionHolder { MpvYTDLPackage(), MpvKtPackage(), MpvKtPreviewPackage(), - OnlyPlayer(), - MpvRxPackage(), // Always Ask option AlwaysAskAction(), // added by plugins @@ -98,7 +95,7 @@ object VideoClickActionHolder { ?.second } - fun getPlayers(context: Context? = null) = allVideoClickActions.filter { it.isPlayer && it.shouldShowSafe(context, null) } + fun getPlayers(activity: Activity? = null) = allVideoClickActions.filter { it.isPlayer && it.shouldShowSafe(activity, null) } } abstract class VideoClickAction { @@ -160,7 +157,7 @@ abstract class VideoClickAction { } } - fun uniqueId() = "$sourcePlugin:${this::class.qualifiedName}" + fun uniqueId() = "$sourcePlugin:${this::class.jvmName}" @Throws abstract fun shouldShow(context: Context?, video: ResultEpisode?): Boolean diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CloudStreamPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CloudStreamPackage.kt index a2bb53a16..d414b6117 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CloudStreamPackage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CloudStreamPackage.kt @@ -7,7 +7,6 @@ import android.net.Uri import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.actions.OpenInAppAction import com.lagradost.cloudstream3.BuildConfig -import com.lagradost.cloudstream3.SkipSerializationTest import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleOrigin @@ -23,10 +22,7 @@ import com.lagradost.cloudstream3.utils.newExtractorLink import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF -import com.lagradost.cloudstream3.utils.serializers.UriSerializer import com.lagradost.cloudstream3.utils.txt -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable /** * If you want to support CloudStream 3 as an external player, then this shows how to play any video link @@ -53,17 +49,19 @@ class CloudStreamPackage : OpenInAppAction( const val DURATION_EXTRA: String = "dur" // Duration time in MS (Long) } - @Serializable - @SkipSerializationTest //.Uri has issues with Jackson data class MinimalVideoLink( - @JsonProperty("uri") @SerialName("uri") - @Serializable(with = UriSerializer::class) + @JsonProperty("uri") val uri: Uri?, - @JsonProperty("url") @SerialName("url") val url: String?, - @JsonProperty("mimeType") @SerialName("mimeType") val mimeType: String = "video/mp4", - @JsonProperty("name") @SerialName("name") val name: String?, - @JsonProperty("headers") @SerialName("headers") var headers: Map = mapOf(), - @JsonProperty("quality") @SerialName("quality") val quality: Int?, + @JsonProperty("url") + val url: String?, + @JsonProperty("mimeType") + val mimeType: String = "video/mp4", + @JsonProperty("name") + val name: String?, + @JsonProperty("headers") + var headers: Map = mapOf(), + @JsonProperty("quality") + val quality: Int?, ) { companion object { fun fromExtractor(link: ExtractorLink): MinimalVideoLink = MinimalVideoLink( @@ -99,12 +97,16 @@ class CloudStreamPackage : OpenInAppAction( } } - @Serializable + data class MinimalSubtitleLink( - @JsonProperty("url") @SerialName("url") val url: String, - @JsonProperty("mimeType") @SerialName("mimeType") val mimeType: String = "text/vtt", - @JsonProperty("name") @SerialName("name") val name: String?, - @JsonProperty("headers") @SerialName("headers") var headers: Map = mapOf(), + @JsonProperty("url") + val url: String, + @JsonProperty("mimeType") + val mimeType: String = "text/vtt", + @JsonProperty("name") + val name: String?, + @JsonProperty("headers") + var headers: Map = mapOf(), ) { companion object { fun fromSubtitle(sub: SubtitleData): MinimalSubtitleLink = MinimalSubtitleLink( @@ -157,4 +159,4 @@ class CloudStreamPackage : OpenInAppAction( override fun onResult(activity: Activity, intent: Intent?) { // No results yet } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvRxPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvRxPackage.kt deleted file mode 100644 index e8bb93a99..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvRxPackage.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.lagradost.cloudstream3.actions.temp - -import android.app.Activity -import android.content.Context -import android.content.Intent -import androidx.core.net.toUri -import com.lagradost.api.Log -import com.lagradost.cloudstream3.actions.OpenInAppAction -import com.lagradost.cloudstream3.actions.updateDurationAndPosition -import com.lagradost.cloudstream3.isEpisodeBased -import com.lagradost.cloudstream3.ui.result.LinkLoadingResult -import com.lagradost.cloudstream3.ui.result.ResultEpisode -import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.txt - -/** https://github.com/Riteshp2001/mpvRx - * - * https://github.com/Riteshp2001/mpvRx/blob/00e0c5e803ab53e5757426cbf2248448ba1f49bf/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt#L132 - * https://github.com/Riteshp2001/mpvRx/blob/00e0c5e803ab53e5757426cbf2248448ba1f49bf/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt#L56 - * */ -class MpvRxPackage : OpenInAppAction( - appName = txt("mpvRx"), - packageName = "app.gyrolet.mpvrx", - intentClass = "app.gyrolet.mpvrx.ui.player.PlayerActivity" -) { - override val oneSource = true - override suspend fun putExtra( - context: Context, - intent: Intent, - video: ResultEpisode, - result: LinkLoadingResult, - index: Int? - ) { - intent.apply { - putExtra("title", video.name) - val link = result.links[index!!] - val headers = link.headers - - setData(link.url.toUri()) - if (headers.isNotEmpty()) { - // PlayerActivity expects a flat array: [key1, value1, key2, value2, ...] - val flat = headers.entries.flatMap { listOf(it.key, it.value) }.toTypedArray() - intent.putExtra("headers", flat) - } - /*val subs = result.subs // disabled due to https://github.com/Riteshp2001/mpvRx/issues/146 - intent.putExtra("subs", subs.map { it.url.toUri() }.toTypedArray()) - intent.putExtra( - "subs.titles", - subs.map { it.name }.toTypedArray(), - ) - intent.putExtra( - "subs.langs", - subs.map { it.languageCode }.toTypedArray(), - ) - val selected = subs.firstOrNull { it.matchesLanguageCode("en") }?.url?.toUri() - intent.putExtra("subs.enable", selected?.let { arrayOf(it) } ?: arrayOf() )*/ - - if (video.tvType.isEpisodeBased()) { - video.season?.let { intent.putExtra("introdb_season", it) } - video.episode.let { intent.putExtra("introdb_episode", it) } - } - - val position = getViewPos(video.id)?.position - if (position != null) - putExtra("position", position.toInt()) - } - } - - override fun onResult(activity: Activity, intent: Intent?) { - val position = intent?.getIntExtra("position", -1) ?: -1 - val duration = intent?.getIntExtra("duration", -1) ?: -1 - Log.d("MPV", "Position: $position, Duration: $duration") - updateDurationAndPosition(position.toLong(), duration.toLong()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt deleted file mode 100644 index 348be440a..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.lagradost.cloudstream3.actions.temp - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.core.net.toUri -import com.lagradost.cloudstream3.actions.OpenInAppAction -import com.lagradost.cloudstream3.ui.result.LinkLoadingResult -import com.lagradost.cloudstream3.ui.result.ResultEpisode -import com.lagradost.cloudstream3.utils.txt - -/** https://github.com/Kindness-Kismet/only_player/tree/main - * https://github.com/Kindness-Kismet/only_player/blob/main/feature/player/src/main/java/one/only/player/feature/player/PlayerActivity.kt */ -class OnlyPlayer : OpenInAppAction( - txt("Only Player"), - "one.only.player", - intentClass = "one.only.player.feature.player.PlayerActivity" -) { - override val oneSource = true - override suspend fun putExtra( - context: Context, - intent: Intent, - video: ResultEpisode, - result: LinkLoadingResult, - index: Int? - ) { - /** https://github.com/Kindness-Kismet/only_player/blob/d3f55049a2913fa762d31b311146073cc2da46cb/app/src/main/java/one/only/player/navigation/CloudNavGraph.kt#L39 */ - intent.apply { - val link = result.links[index!!] - setData(link.url.toUri()) - - putExtra("headers", Bundle().apply { - for ((key, value) in link.headers) { - putExtra(key, value) - } - }) - } - } - - override fun onResult(activity: Activity, intent: Intent?) { - /* onResult does not get called */ - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/VlcPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/VlcPackage.kt index b6478b1d9..46b46a2c2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/VlcPackage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/VlcPackage.kt @@ -60,7 +60,7 @@ open class VlcPackage: OpenInAppAction( intent.putExtra("secure_uri", true) intent.putExtra("title", video.name) - val subsLang = getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en" + val subsLang = getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en" result.subs.firstOrNull { subsLang == it.languageCode }?.let { @@ -74,4 +74,4 @@ open class VlcPackage: OpenInAppAction( Log.d("VLC", "Position: $position, Duration: $duration") updateDurationAndPosition(position, duration) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/Packets.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/Packets.kt index d54791e89..26f5cec53 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/Packets.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/Packets.kt @@ -1,9 +1,5 @@ package com.lagradost.cloudstream3.actions.temp.fcast -import com.fasterxml.jackson.annotation.JsonProperty -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - // See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 enum class Opcode(val value: Byte) { None(0), @@ -22,18 +18,18 @@ enum class Opcode(val value: Byte) { Pong(13); } -@Serializable + data class PlayMessage( - @JsonProperty("container") @SerialName("container") val container: String, - @JsonProperty("url") @SerialName("url") val url: String? = null, - @JsonProperty("content") @SerialName("content") val content: String? = null, - @JsonProperty("time") @SerialName("time") val time: Double? = null, - @JsonProperty("speed") @SerialName("speed") val speed: Double? = null, - @JsonProperty("headers") @SerialName("headers") val headers: Map? = null, + val container: String, + val url: String? = null, + val content: String? = null, + val time: Double? = null, + val speed: Double? = null, + val headers: Map? = null ) data class SeekMessage( - val time: Double, + val time: Double ) data class PlaybackUpdateMessage( @@ -41,26 +37,26 @@ data class PlaybackUpdateMessage( val time: Double, val duration: Double, val state: Int, - val speed: Double, + val speed: Double ) data class VolumeUpdateMessage( val generationTime: Long, - val volume: Double, + val volume: Double ) data class PlaybackErrorMessage( - val message: String, + val message: String ) data class SetSpeedMessage( - val speed: Double, + val speed: Double ) data class SetVolumeMessage( - val volume: Double, + val volume: Double ) data class VersionMessage( - val version: Long, + val version: Long ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt index e49245904..6234297d0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt @@ -22,6 +22,7 @@ fun Requests.initClient(context: Context) { } /** Only use ignoreSSL if you know what you are doing*/ +@Prerelease fun Requests.initClient(context: Context, ignoreSSL: Boolean = false) { this.baseClient = buildDefaultClient(context, ignoreSSL) } @@ -33,11 +34,12 @@ fun buildDefaultClient(context: Context): OkHttpClient { } /** Only use ignoreSSL if you know what you are doing*/ +@Prerelease fun buildDefaultClient(context: Context, ignoreSSL: Boolean = false): OkHttpClient { safe { Security.insertProviderAt(Conscrypt.newProvider(), 1) } val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) - val dns = settingsManager.getInt(context.getString(R.string.dns_key), 0) + val dns = settingsManager.getInt(context.getString(R.string.dns_pref), 0) val baseClient = OkHttpClient.Builder() .followRedirects(true) .followSslRedirects(true) diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index e1496db06..efa028d14 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -7,6 +7,7 @@ import com.lagradost.cloudstream3.actions.VideoClickAction import com.lagradost.cloudstream3.actions.VideoClickActionHolder import kotlin.Throws + abstract class Plugin : BasePlugin() { /** * Called when your Plugin is loaded @@ -25,7 +26,9 @@ abstract class Plugin : BasePlugin() { fun registerVideoClickAction(element: VideoClickAction) { Log.i(PLUGIN_TAG, "Adding ${element.name} VideoClickAction") element.sourcePlugin = this.filename - VideoClickActionHolder.allVideoClickActions.add(element) + synchronized(VideoClickActionHolder.allVideoClickActions) { + VideoClickActionHolder.allVideoClickActions.add(element) + } } /** @@ -37,4 +40,4 @@ abstract class Plugin : BasePlugin() { * This will add a button in the settings allowing you to add custom settings */ var openSettings: ((context: Context) -> Unit)? = null -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 7ea736416..eae14a6c0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -61,8 +61,6 @@ import com.lagradost.cloudstream3.utils.txt import dalvik.system.PathClassLoader import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.io.File import java.io.InputStreamReader @@ -75,13 +73,12 @@ const val EXTENSIONS_CHANNEL_NAME = "Extensions" const val EXTENSIONS_CHANNEL_DESCRIPT = "Extension notification channel" // Data class for internal storage -@Serializable data class PluginData( - @JsonProperty("internalName") @SerialName("internalName") val internalName: String, - @JsonProperty("url") @SerialName("url") val url: String?, - @JsonProperty("isOnline") @SerialName("isOnline") val isOnline: Boolean, - @JsonProperty("filePath") @SerialName("filePath") val filePath: String, - @JsonProperty("version") @SerialName("version") val version: Int, + @JsonProperty("internalName") val internalName: String, + @JsonProperty("url") val url: String?, + @JsonProperty("isOnline") val isOnline: Boolean, + @JsonProperty("filePath") val filePath: String, + @JsonProperty("version") val version: Int, ) { @WorkerThread fun toSitePlugin(): SitePlugin { @@ -176,11 +173,11 @@ object PluginManager { fun getPluginsOnline(): Array { - return getKey>(PLUGINS_KEY) ?: emptyArray() + return getKey(PLUGINS_KEY) ?: emptyArray() } fun getPluginsLocal(): Array { - return getKey>(PLUGINS_KEY_LOCAL) ?: emptyArray() + return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray() } private val CLOUD_STREAM_FOLDER = @@ -224,17 +221,17 @@ object PluginManager { // Helper class for updateAllOnlinePluginsAndLoadThem data class OnlinePluginData( val savedData: PluginData, - val onlineData: PluginWrapper, + val onlineData: Pair, ) { val isOutdated = - onlineData.plugin.version > savedData.version || onlineData.plugin.version == PLUGIN_VERSION_ALWAYS_UPDATE - val isDisabled = onlineData.plugin.status == PROVIDER_STATUS_DOWN + onlineData.second.version > savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE + val isDisabled = onlineData.second.status == PROVIDER_STATUS_DOWN fun validOnlineData(context: Context): Boolean { return getPluginPath( context, savedData.internalName, - onlineData.repositoryData.url + onlineData.first ).absolutePath == savedData.filePath } } @@ -282,19 +279,19 @@ object PluginManager { ?: emptyArray()) + PREBUILT_REPOSITORIES val onlinePlugins = urls.toList().amap { - getRepoPlugins(it) ?: emptyList() - }.flatten().distinctBy { it.plugin.url } + getRepoPlugins(it.url)?.toList() ?: emptyList() + }.flatten().distinctBy { it.second.url } // Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated val outdatedPlugins = getPluginsOnline().map { savedData -> onlinePlugins - .filter { onlineData -> savedData.internalName == onlineData.plugin.internalName } + .filter { onlineData -> savedData.internalName == onlineData.second.internalName } .map { onlineData -> OnlinePluginData(savedData, onlineData) }.filter { it.validOnlineData(activity) } - }.flatten().distinctBy { it.onlineData.plugin.url } + }.flatten().distinctBy { it.onlineData.second.url } debugPrint { "Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}" @@ -309,14 +306,14 @@ object PluginManager { } else if (pluginData.isOutdated) { downloadPlugin( activity, - pluginData.onlineData.plugin.url, - pluginData.onlineData.plugin.fileHash, + pluginData.onlineData.second.url, + pluginData.onlineData.second.fileHash, pluginData.savedData.internalName, File(pluginData.savedData.filePath), true ).let { success -> if (success) - updatedPlugins.add(pluginData.onlineData.plugin.name) + updatedPlugins.add(pluginData.onlineData.second.name) } } } @@ -359,15 +356,15 @@ object PluginManager { val urls = (getKey>(REPOSITORIES_KEY) ?: emptyArray()) + PREBUILT_REPOSITORIES val onlinePlugins = urls.toList().amap { - getRepoPlugins(it)?.toList() ?: emptyList() - }.flatten().distinctBy { it.plugin.url } + getRepoPlugins(it.url)?.toList() ?: emptyList() + }.flatten().distinctBy { it.second.url } val providerLang = activity.getApiProviderLangSettings() //Log.i(TAG, "providerLang => ${providerLang.toJson()}") // Iterate online repos and returns not downloaded plugins val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData -> - val sitePlugin = onlineData.plugin + val sitePlugin = onlineData.second val tvtypes = sitePlugin.tvTypes ?: listOf() //Don't include empty urls @@ -379,7 +376,7 @@ object PluginManager { } //Omit already existing plugins - if (getPluginPath(activity, sitePlugin.internalName, onlineData.repositoryData.url).exists()) { + if (getPluginPath(activity, sitePlugin.internalName, onlineData.first).exists()) { Log.i(TAG, "Skip > ${sitePlugin.internalName}") return@mapNotNull null } @@ -421,14 +418,14 @@ object PluginManager { notDownloadedPlugins.amap { pluginData -> downloadPlugin( activity, - pluginData.onlineData.plugin.url, - pluginData.onlineData.plugin.fileHash, + pluginData.onlineData.second.url, + pluginData.onlineData.second.fileHash, pluginData.savedData.internalName, - pluginData.onlineData.repositoryData.url, + pluginData.onlineData.first, !pluginData.isDisabled ).let { success -> if (success) - newDownloadPlugins.add(pluginData.onlineData.plugin.name) + newDownloadPlugins.add(pluginData.onlineData.second.name) } } @@ -512,9 +509,6 @@ object PluginManager { val res = dir.mkdirs() if (!res) { Log.w(TAG, "Failed to create local directories") - // We have tried to load local plugins, but exit early. - // This needs to be true to prevent the downloader waiting for plugins. - loadedLocalPlugins = true return } } @@ -590,7 +584,7 @@ object PluginManager { /** * @return True if successful, false if not * */ - internal suspend fun loadPlugin(context: Context, file: File, data: PluginData): Boolean { + private suspend fun loadPlugin(context: Context, file: File, data: PluginData): Boolean { val fileName = file.nameWithoutExtension val filePath = file.absolutePath currentlyLoading = fileName @@ -616,7 +610,7 @@ object PluginManager { return false } InputStreamReader(stream).use { reader -> - manifest = parseJson(reader.readText()) + manifest = parseJson(reader, BasePlugin.Manifest::class.java) } } @@ -657,15 +651,9 @@ object PluginManager { context.resources.configuration ) } - synchronized(plugins) { - plugins[filePath] = pluginInstance - } - synchronized(classLoaders) { - classLoaders[loader] = pluginInstance - } - synchronized(urlPlugins) { - urlPlugins[data.url ?: filePath] = pluginInstance - } + plugins[filePath] = pluginInstance + classLoaders[loader] = pluginInstance + urlPlugins[data.url ?: filePath] = pluginInstance if (pluginInstance is Plugin) { pluginInstance.load(context) } else { @@ -701,20 +689,21 @@ object PluginManager { } // remove all registered apis - APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { - removePluginMapping(it) + synchronized(APIHolder.apis) { + APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { + removePluginMapping(it) + } + } + synchronized(APIHolder.allProviders) { + APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename } } - APIHolder.allProviders.withLock { - APIHolder.allProviders.removeAll { provider -> provider.sourcePlugin == plugin.filename } + synchronized(extractorApis) { + extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename } } - extractorApis.withLock { - extractorApis.removeAll { provider -> provider.sourcePlugin == plugin.filename } - } - - VideoClickActionHolder.allVideoClickActions.withLock { - VideoClickActionHolder.allVideoClickActions.removeAll { action -> action.sourcePlugin == plugin.filename } + synchronized(VideoClickActionHolder.allVideoClickActions) { + VideoClickActionHolder.allVideoClickActions.removeIf { action: VideoClickAction -> action.sourcePlugin == plugin.filename } } synchronized(classLoaders) { @@ -838,16 +827,16 @@ object PluginManager { val urls = (getKey>(REPOSITORIES_KEY) ?: emptyArray()) + PREBUILT_REPOSITORIES val onlinePlugins = urls.toList().amap { - getRepoPlugins(it) ?: emptyList() - }.flatten().distinctBy { it.plugin.url } + getRepoPlugins(it.url)?.toList() ?: emptyList() + }.flatten().distinctBy { it.second.url } val allPlugins = getPluginsOnline().flatMap { savedData -> onlinePlugins - .filter { it.plugin.internalName == savedData.internalName } + .filter { it.second.internalName == savedData.internalName } .mapNotNull { onlineData -> OnlinePluginData(savedData, onlineData).takeIf { it.validOnlineData(activity) } } - }.distinctBy { it.onlineData.plugin.url } + }.distinctBy { it.onlineData.second.url } val updatedPlugins = mutableListOf() @@ -855,7 +844,7 @@ object PluginManager { if (pluginData.isDisabled) { Log.e( "PluginManager", - "Unloading disabled plugin: ${pluginData.onlineData.plugin.name}" + "Unloading disabled plugin: ${pluginData.onlineData.second.name}" ) unloadPlugin(pluginData.savedData.filePath) } else { @@ -864,14 +853,14 @@ object PluginManager { if (downloadPlugin( activity, - pluginData.onlineData.plugin.url, - pluginData.onlineData.plugin.fileHash, + pluginData.onlineData.second.url, + pluginData.onlineData.second.fileHash, pluginData.savedData.internalName, existingFile, true ) ) { - updatedPlugins.add(pluginData.onlineData.plugin.name) + updatedPlugins.add(pluginData.onlineData.second.name) } } }.also { diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt index 910d84735..07d6aaa37 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -16,27 +16,26 @@ import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileNa import com.lagradost.cloudstream3.plugins.PluginManager.unloadPlugin import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.io.File import java.nio.file.AtomicMoveNotSupportedException import java.nio.file.Files import java.nio.file.StandardCopyOption import java.security.MessageDigest -import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger /** * Comes with the app, always available in the app, non removable. - */ -@Serializable + * */ + data class Repository( - @JsonProperty("iconUrl") @SerialName("iconUrl") val iconUrl: String?, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("description") @SerialName("description") val description: String?, - @JsonProperty("manifestVersion") @SerialName("manifestVersion") val manifestVersion: Int, - @JsonProperty("pluginLists") @SerialName("pluginLists") val pluginLists: List, + @JsonProperty("iconUrl") val iconUrl: String?, + @JsonProperty("name") val name: String, + @JsonProperty("description") val description: String?, + @JsonProperty("manifestVersion") val manifestVersion: Int, + @JsonProperty("pluginLists") val pluginLists: List ) /** @@ -45,60 +44,40 @@ data class Repository( * 1: Ok * 2: Slow * 3: Beta only - */ -@Serializable + * */ data class SitePlugin( // Url to the .cs3 file - @JsonProperty("url") @SerialName("url") val url: String, + @JsonProperty("url") val url: String, // Status to remotely disable the provider - @JsonProperty("status") @SerialName("status") val status: Int, + @JsonProperty("status") val status: Int, // Integer over 0, any change of this will trigger an auto update - @JsonProperty("version") @SerialName("version") val version: Int, + @JsonProperty("version") val version: Int, // Unused currently, used to make the api backwards compatible? // Set to 1 - @JsonProperty("apiVersion") @SerialName("apiVersion") val apiVersion: Int, + @JsonProperty("apiVersion") val apiVersion: Int, // Name to be shown in app - @JsonProperty("name") @SerialName("name") val name: String, + @JsonProperty("name") val name: String, // Name to be referenced internally. Separate to make name and url changes possible - @JsonProperty("internalName") @SerialName("internalName") val internalName: String, - @JsonProperty("authors") @SerialName("authors") val authors: List, - @JsonProperty("description") @SerialName("description") val description: String?, + @JsonProperty("internalName") val internalName: String, + @JsonProperty("authors") val authors: List, + @JsonProperty("description") val description: String?, // Might be used to go directly to the plugin repo in the future - @JsonProperty("repositoryUrl") @SerialName("repositoryUrl") val repositoryUrl: String?, + @JsonProperty("repositoryUrl") val repositoryUrl: String?, // These types are yet to be mapped and used, ignore for now - @JsonProperty("tvTypes") @SerialName("tvTypes") val tvTypes: List?, + @JsonProperty("tvTypes") val tvTypes: List?, // Most often a language tag like "en" or "zh-TW" - @JsonProperty("language") @SerialName("language") val language: String?, - @JsonProperty("iconUrl") @SerialName("iconUrl") val iconUrl: String?, + @JsonProperty("language") val language: String?, + @JsonProperty("iconUrl") val iconUrl: String?, // Automatically generated by the gradle plugin - @JsonProperty("fileSize") @SerialName("fileSize") val fileSize: Long?, - @JsonProperty("fileHash") @SerialName("fileHash") val fileHash: String?, + @JsonProperty("fileSize") val fileSize: Long?, + @JsonProperty("fileHash") val fileHash: String?, ) -@Serializable -data class PluginWrapper( - @JsonProperty("repository") @SerialName("repository") val repository: Repository, - @JsonProperty("repositoryData") @SerialName("repositoryData") val repositoryData: RepositoryData, - @JsonProperty("plugin") @SerialName("plugin") val plugin: SitePlugin -) { - companion object { - private val localRepository = Repository("", "", "", 1, emptyList()) - private val localRepositoryData = RepositoryData("", "", "") - fun getLocalPluginWrapper(plugin: SitePlugin): PluginWrapper { - return PluginWrapper( - localRepository, - localRepositoryData, - plugin - ) - } - } -} - object RepositoryManager { const val ONLINE_PLUGINS_FOLDER = "Extensions" val PREBUILT_REPOSITORIES: Array by lazy { - getKey>("PREBUILT_REPOSITORIES") ?: emptyArray() + getKey("PREBUILT_REPOSITORIES") ?: emptyArray() } private val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$") @@ -141,18 +120,12 @@ object RepositoryManager { } } else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) { safeAsync { - if (fixedUrl.startsWith("!")) { - val response = app.get("https://py.md/${fixedUrl.removePrefix("!")}", allowRedirects = false) - val url = response.headers["Location"] ?: return@safeAsync null - if (url.startsWith("https://py.md/404")) return@safeAsync null - if (url.removeSuffix("/") == "https://py.md") return@safeAsync null - return@safeAsync url - } else { - val response = app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false) - val url = response.headers["Location"] ?: return@safeAsync null - if (url.startsWith("https://cutt.ly/404")) return@safeAsync null - if (url.removeSuffix("/") == "https://cutt.ly") return@safeAsync null - return@safeAsync url + app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false).let { it2 -> + it2.headers["Location"]?.let { url -> + if (url.startsWith("https://cutt.ly/404")) return@safeAsync null + if (url.removeSuffix("/") == "https://cutt.ly") return@safeAsync null + return@safeAsync url + } } } } else null @@ -161,16 +134,17 @@ object RepositoryManager { suspend fun parseRepository(url: String): Repository? { return safeAsync { // Take manifestVersion and such into account later - app.get(convertRawGitUrl(url), cacheTime = 15, cacheUnit = TimeUnit.MINUTES) - .parsedSafe() + app.get(convertRawGitUrl(url)).parsedSafe() } } private suspend fun parsePlugins(pluginUrls: String): List { // Take manifestVersion and such into account later return try { - app.get(convertRawGitUrl(pluginUrls), cacheTime = 15, cacheUnit = TimeUnit.MINUTES) - .parsed>().toList() + val response = app.get(convertRawGitUrl(pluginUrls)) + // Normal parsed function not working? + // return response.parsedSafe() + tryParseJson>(response.text)?.toList() ?: emptyList() } catch (t: Throwable) { logError(t) emptyList() @@ -179,17 +153,17 @@ object RepositoryManager { /** * Gets all plugins from repositories and pairs them with the repository url - */ - suspend fun getRepoPlugins(repositoryData: RepositoryData): List? { - val repo = parseRepository(repositoryData.url) ?: return null - val list = repo.pluginLists.amap { url -> + * */ + suspend fun getRepoPlugins(repositoryUrl: String): List>? { + val repo = parseRepository(repositoryUrl) ?: return null + return repo.pluginLists.amap { url -> parsePlugins(url).map { - PluginWrapper(repo, repositoryData, it) + repositoryUrl to it } }.flatten() - return list } + suspend fun downloadPluginToFile( context: Context, pluginUrl: String, @@ -240,29 +214,22 @@ object RepositoryManager { } fun getRepositories(): Array { - return getKey>(REPOSITORIES_KEY) ?: emptyArray() + return getKey(REPOSITORIES_KEY) ?: emptyArray() } // Don't want to read before we write in another thread private val repoLock = Mutex() suspend fun addRepository(repository: RepositoryData) { repoLock.withLock { - // No duplicates, but we want the new repository instead of the old one - var currentRepos = getRepositories() - val index = currentRepos.indexOfFirst { it.url == repository.url } - if(index == -1) { - currentRepos += repository - } else { - /** We want to keep the ordering here, as that is the addition order */ - currentRepos[index] = repository - } - setKey(REPOSITORIES_KEY, currentRepos) + val currentRepos = getRepositories() + // No duplicates + setKey(REPOSITORIES_KEY, (currentRepos + repository).distinctBy { it.url }) } } /** * Also deletes downloaded repository plugins - */ + * */ suspend fun removeRepository(context: Context, repository: RepositoryData) { val extensionsDir = File(context.filesDir, ONLINE_PLUGINS_FOLDER) diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt index 57f6f4750..85a806f0b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.plugins import android.util.Log import android.widget.Toast -import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.CloudStreamApp.Companion.context import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey @@ -12,10 +11,9 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.Coroutines.main import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable object VotingApi { + private const val LOGKEY = "VotingApi" private const val API_DOMAIN = "https://api.countify.xyz" @@ -52,8 +50,8 @@ object VotingApi { votesCache[pluginUrl] = it } - fun hasVoted(pluginUrl: String): Boolean = - getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false + fun hasVoted(pluginUrl: String) = + getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false fun canVote(pluginUrl: String): Boolean = PluginManager.urlPlugins.contains(pluginUrl) @@ -93,9 +91,8 @@ object VotingApi { } } - @Serializable private data class CountifyResult( - @JsonProperty("id") @SerialName("id") val id: String? = null, - @JsonProperty("count") @SerialName("count") val count: Int? = null, + val id: String? = null, + val count: Int? = null ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/receivers/PackageInstallerStatusReceiver.kt b/app/src/main/java/com/lagradost/cloudstream3/receivers/PackageInstallerStatusReceiver.kt deleted file mode 100644 index 1739b8147..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/receivers/PackageInstallerStatusReceiver.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.lagradost.cloudstream3.receivers - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.pm.PackageInstaller -import com.lagradost.cloudstream3.utils.getSafeParcelableExtra - -/** https://medium.com/@solrudev/painless-building-of-an-android-package-installer-app-d5a09b5df432 */ -class PackageInstallerStatusReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) { - PackageInstaller.STATUS_PENDING_USER_ACTION -> { - intent.getSafeParcelableExtra(Intent.EXTRA_INTENT)?.let { userAction-> - userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(userAction) - } - } - PackageInstaller.STATUS_SUCCESS -> { - // do something on success - } - else -> { - val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - println("PackageInstallerStatusReceiver: status=$status, message=$message") - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt b/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt index e07747a86..028356e76 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt @@ -36,7 +36,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet @@ -187,16 +186,6 @@ class DownloadQueueService : Service() { debugAssert({ timeTaken == null }, { "Downloader startup should not time out" }) totalDownloadFlow - .debounce { (instances, queue) -> - // Filter away incorrect transient queue states. - // For example when we pop the queue and add a download instance there exists a transient state where - // there is no queue and no download instances (leading to an early exit) - if (instances.isEmpty() && queue.isEmpty()) { - 500.milliseconds - } else { - 0.milliseconds - } - } .takeWhile { (instances, queue) -> // Stop if destroyed isRunning diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index 68fba9777..3bc5f2733 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -70,8 +70,7 @@ abstract class AccountManager { SubtitleRepo(openSubtitlesApi), SubtitleRepo(addic7ed), SubtitleRepo(subDlApi), - PlainAuthRepo(animeSkipApi), - SubtitleRepo(subSourceApi) + PlainAuthRepo(animeSkipApi) ) fun updateAccountIds() { @@ -121,8 +120,7 @@ abstract class AccountManager { val subtitleProviders = arrayOf( SubtitleRepo(openSubtitlesApi), SubtitleRepo(addic7ed), - SubtitleRepo(subDlApi), - SubtitleRepo(subSourceApi) + SubtitleRepo(subDlApi) ) val syncApis = arrayOf( SyncRepo(malApi), diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt index c0f0e4a03..83a7a0984 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt @@ -1,18 +1,50 @@ package com.lagradost.cloudstream3.syncproviders -import com.fasterxml.jackson.annotation.JsonAlias +import android.util.Base64 +import androidx.annotation.WorkerThread import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.unixTime -import com.lagradost.cloudstream3.APIHolder.unixTimeMS -import com.lagradost.cloudstream3.base64Encode -import com.lagradost.cloudstream3.splitUrlParameters +import com.lagradost.cloudstream3.ActorData +import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey +import com.lagradost.cloudstream3.CloudStreamApp.Companion.openBrowser +import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey +import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.NextAiring +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.Score +import com.lagradost.cloudstream3.SearchQuality +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.ShowStatus +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.mvvm.Resource +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.safe +import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonNames +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.NONE_ID +import com.lagradost.cloudstream3.syncproviders.providers.Addic7ed +import com.lagradost.cloudstream3.syncproviders.providers.AniListApi +import com.lagradost.cloudstream3.syncproviders.providers.LocalList +import com.lagradost.cloudstream3.syncproviders.providers.MALApi +import com.lagradost.cloudstream3.syncproviders.providers.KitsuApi +import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi +import com.lagradost.cloudstream3.syncproviders.providers.SimklApi +import com.lagradost.cloudstream3.syncproviders.providers.SubDlApi +import com.lagradost.cloudstream3.syncproviders.providers.SubSourceApi +import com.lagradost.cloudstream3.ui.SyncWatchType +import com.lagradost.cloudstream3.ui.library.ListSorting +import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf +import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.UiText +import com.lagradost.cloudstream3.utils.txt +import me.xdrop.fuzzywuzzy.FuzzySearch +import java.net.URL import java.security.SecureRandom +import java.util.Date +import java.util.concurrent.TimeUnit data class AuthLoginPage( /** The website to open to authenticate */ @@ -20,60 +52,55 @@ data class AuthLoginPage( /** * State/control code to verify against the redirectUrl to make sure the request is valid. * This parameter will be saved, and then used in AuthAPI::login. - */ + * */ val payload: String? = null, ) -@Serializable data class AuthToken( /** * This is the general access tokens/api token representing a logged in user. - * Access tokens are the thing that applications use to make API requests on behalf of a user. - */ - @JsonProperty("accessToken") @SerialName("accessToken") + * + * `Access tokens are the thing that applications use to make API requests on behalf of a user.` + * */ + @JsonProperty("accessToken") val accessToken: String? = null, - /** For OAuth a special refresh token is issues to refresh the access token. */ - @JsonProperty("refreshToken") @SerialName("refreshToken") + /** + * For OAuth a special refresh token is issues to refresh the access token. + * */ + @JsonProperty("refreshToken") val refreshToken: String? = null, /** In UnixTime (sec) when it expires */ - @JsonProperty("accessTokenLifetime") @SerialName("accessTokenLifetime") + @JsonProperty("accessTokenLifetime") val accessTokenLifetime: Long? = null, /** In UnixTime (sec) when it expires */ - @JsonProperty("refreshTokenLifetime") @SerialName("refreshTokenLifetime") + @JsonProperty("refreshTokenLifetime") val refreshTokenLifetime: Long? = null, - /** - * Sometimes AuthToken needs to be customized to store e.g. username/password, - * this acts as a catch all to store text or JSON data. - */ - @JsonProperty("payload") @SerialName("payload") + /** Sometimes AuthToken needs to be customized to store e.g. username/password, + * this acts as a catch all to store text or JSON data. */ + @JsonProperty("payload") val payload: String? = null, ) { fun isAccessTokenExpired(marginSec: Long = 10L) = - accessTokenLifetime != null && unixTime + marginSec >= accessTokenLifetime + accessTokenLifetime != null && (System.currentTimeMillis() / 1000) + marginSec >= accessTokenLifetime fun isRefreshTokenExpired(marginSec: Long = 10L) = - refreshTokenLifetime != null && unixTime + marginSec >= refreshTokenLifetime + refreshTokenLifetime != null && (System.currentTimeMillis() / 1000) + marginSec >= refreshTokenLifetime } -@OptIn(ExperimentalSerializationApi::class) // JsonNames is an experimental annotation for now -@Serializable data class AuthUser( /** Account display-name, can also be email if name does not exist */ - @JsonProperty("name") @SerialName("name") + @JsonProperty("name") val name: String?, - /** - * Unique account identifier. If a subsequent login is done then it - * will be refused if another account with the same id exists. - */ - @JsonProperty("id") @SerialName("id") + /** Unique account identifier, + * if a subsequent login is done then it will be refused if another account with the same id exists*/ + @JsonProperty("id") val id: Int, /** Profile picture URL */ - @JsonProperty("profilePicture") @SerialName("profilePicture") + @JsonProperty("profilePicture") val profilePicture: String? = null, /** Profile picture Headers of the URL */ - @JsonProperty("profilePictureHeaders") @JsonAlias("profilePictureHeader") - @SerialName("profilePictureHeaders") @JsonNames("profilePictureHeader") - val profilePictureHeaders: Map? = null, + @JsonProperty("profilePictureHeader") + val profilePictureHeaders: Map? = null ) /** @@ -83,11 +110,12 @@ data class AuthUser( * * Any local set/get key should use user.id.toString(), * as token.accessToken (even hashed) is unsecure, and will rotate. - */ -@Serializable + * */ data class AuthData( - @JsonProperty("user") @SerialName("user") val user: AuthUser, - @JsonProperty("token") @SerialName("token") val token: AuthToken, + @JsonProperty("user") + val user: AuthUser, + @JsonProperty("token") + val token: AuthToken, ) data class AuthPinData( @@ -110,12 +138,15 @@ data class AuthLoginRequirement( ) /** What the user responds to the AuthLoginRequirement */ -@Serializable data class AuthLoginResponse( - @JsonProperty("password") @SerialName("password") val password: String?, - @JsonProperty("username") @SerialName("username") val username: String?, - @JsonProperty("email") @SerialName("email") val email: String?, - @JsonProperty("server") @SerialName("server") val server: String?, + @JsonProperty("password") + val password: String?, + @JsonProperty("username") + val username: String?, + @JsonProperty("email") + val email: String?, + @JsonProperty("server") + val server: String?, ) /** Stateless Authentication class used for all personalized content */ @@ -148,31 +179,16 @@ abstract class AuthAPI { open val inAppLoginRequirement: AuthLoginRequirement? = null companion object { - @Deprecated( - message = "Use APIHolder.unixTime instead", - replaceWith = ReplaceWith( - expression = "APIHolder.unixTime", - imports = ["com.lagradost.cloudstream3.APIHolder"] - ), - level = DeprecationLevel.WARNING, - ) val unixTime: Long - get() = APIHolder.unixTime - - @Deprecated( - message = "Use APIHolder.unixTimeMS instead", - replaceWith = ReplaceWith( - expression = "unixTimeMS", - imports = ["com.lagradost.cloudstream3.APIHolder.unixTimeMS"] - ), - level = DeprecationLevel.WARNING, - ) + get() = System.currentTimeMillis() / 1000L val unixTimeMs: Long - get() = unixTimeMS + get() = System.currentTimeMillis() fun splitRedirectUrl(redirectUrl: String): Map { - return splitUrlParameters( - redirectUrl.replace(APP_STRING, "https").replace("/#", "?") + return splitQuery( + URL( + redirectUrl.replace(APP_STRING, "https").replace("/#", "?") + ) ) } @@ -182,8 +198,9 @@ abstract class AuthAPI { val secureRandom = SecureRandom() val codeVerifierBytes = ByteArray(96) // base64 has 6bit per char; (8/6)*96 = 128 secureRandom.nextBytes(codeVerifierBytes) - return base64Encode(codeVerifierBytes).trimEnd('=') - .replace("+", "-").replace("/", "_").replace("\n", "") + return Base64.encodeToString(codeVerifierBytes, Base64.DEFAULT).trimEnd('=') + .replace("+", "-") + .replace("/", "_").replace("\n", "") } } @@ -226,7 +243,7 @@ abstract class AuthAPI { * * Note that this will currently only be called *once* on logout, * and as such any network issues it will fail silently, and the token will not be revoked. - */ + **/ @Throws open suspend fun invalidateToken(token: AuthToken): Nothing = throw NotImplementedError() diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt index 161001611..7a93f96f6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt @@ -2,10 +2,11 @@ package com.lagradost.cloudstream3.syncproviders import androidx.annotation.WorkerThread import com.lagradost.cloudstream3.APIHolder.unixTime +import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch import com.lagradost.cloudstream3.subtitles.SubtitleResource -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf /** Stateless safe abstraction of SubtitleAPI */ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { @@ -13,8 +14,7 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { data class SavedSearchResponse( val unixTime: Long, val response: List, - val query: SubtitleSearch, - val idPrefix: String, + val query: SubtitleSearch ) data class SavedResourceResponse( @@ -24,30 +24,26 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { ) // maybe make this a generic struct? right now there is a lot of boilerplate - private val searchCache = atomicListOf() + private val searchCache = threadSafeListOf() private var searchCacheIndex: Int = 0 - private val resourceCache = atomicListOf() + private val resourceCache = threadSafeListOf() private var resourceCacheIndex: Int = 0 const val CACHE_SIZE = 20 } @WorkerThread suspend fun resource(data: SubtitleEntity): Result = runCatching { - val cached = resourceCache.withLock { - var found: SubtitleResource? = null + synchronized(resourceCache) { for (item in resourceCache) { // 20 min save if (item.query == data && (unixTime - item.unixTime) < 60 * 20) { - found = item.response - break + return@runCatching item.response } } - found } - if (cached != null) return@runCatching cached val returnValue = api.resource(freshAuth(), data) - resourceCache.withLock { + synchronized(resourceCache) { val add = SavedResourceResponse(unixTime, returnValue, data) if (resourceCache.size > CACHE_SIZE) { resourceCache[resourceCacheIndex] = add // rolling cache @@ -62,25 +58,22 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { @WorkerThread suspend fun search(query: SubtitleSearch): Result> { return runCatching { - val cached = searchCache.withLock { - var found: List? = null + synchronized(searchCache) { for (item in searchCache) { // 120 min save - if (item.idPrefix == idPrefix && item.query == query && (unixTime - item.unixTime) < 60 * 120) { - found = item.response - break + if (item.query == query && (unixTime - item.unixTime) < 60 * 120) { + return@runCatching item.response } } - found } - if (cached != null) return@runCatching cached - val returnValue = api.search(freshAuth(), query) ?: emptyList() + val returnValue = + api.search(freshAuth(), query) ?: emptyList() // only cache valid return values if (returnValue.isNotEmpty()) { - val add = SavedSearchResponse(unixTime, returnValue, query, idPrefix) - searchCache.withLock { + val add = SavedSearchResponse(unixTime, returnValue, query) + synchronized(searchCache) { if (searchCache.size > CACHE_SIZE) { searchCache[searchCacheIndex] = add // rolling cache searchCacheIndex = (searchCacheIndex + 1) % CACHE_SIZE @@ -93,3 +86,4 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { } } } + diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index f30a64748..e5f9aca84 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -10,8 +10,8 @@ import com.lagradost.cloudstream3.ShowStatus import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting -import com.lagradost.cloudstream3.utils.Levenshtein import com.lagradost.cloudstream3.utils.UiText +import me.xdrop.fuzzywuzzy.FuzzySearch import java.util.Date /** @@ -138,7 +138,7 @@ abstract class SyncAPI : AuthAPI() { ListSorting.Query -> if (query != null) { items.sortedBy { - -Levenshtein.partialRatio( + -FuzzySearch.partialRatio( query.lowercase(), it.name.lowercase() ) } @@ -191,4 +191,4 @@ abstract class SyncAPI : AuthAPI() { override var score: Score? = null, val tags: List? = null ) : SearchResponse -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index d3e7f22c7..7a46b4113 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -5,8 +5,6 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.Actor import com.lagradost.cloudstream3.ActorData import com.lagradost.cloudstream3.ActorRole -import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey import com.lagradost.cloudstream3.ErrorLoadingException @@ -27,10 +25,9 @@ import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear import com.lagradost.cloudstream3.utils.txt -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.net.URLEncoder import java.util.Locale @@ -38,7 +35,7 @@ class AniListApi : SyncAPI() { override var name = "AniList" override val idPrefix = "anilist" - private val key = BuildConfig.ANILIST_KEY + val key = "6871" override val redirectUrlIdentifier = "anilistlogin" override var requireLibraryRefresh = true override val hasOAuth2 = true @@ -53,10 +50,9 @@ class AniListApi : SyncAPI() { override suspend fun login(redirectUrl: String, payload: String?): AuthToken? { val sanitizer = splitRedirectUrl(redirectUrl) val token = AuthToken( - accessToken = sanitizer["access_token"] - ?: throw ErrorLoadingException("No access token"), - // refreshToken = sanitizer["refresh_token"], - accessTokenLifetime = APIHolder.unixTime + sanitizer["expires_in"]!!.toLong(), + accessToken = sanitizer["access_token"] ?: throw ErrorLoadingException("No access token"), + //refreshToken = sanitizer["refresh_token"], + accessTokenLifetime = unixTime + sanitizer["expires_in"]!!.toLong(), ) return token } @@ -82,12 +78,13 @@ class AniListApi : SyncAPI() { override fun urlToId(url: String): String? = url.removePrefix("$mainUrl/anime/").removeSuffix("/") + private fun getUrlFromId(id: Int): String { return "$mainUrl/anime/$id" } - override suspend fun search(auth: AuthData?, query: String): List? { - val data = searchShows(query) ?: return null + override suspend fun search(auth : AuthData?, query: String): List? { + val data = searchShows(name) ?: return null return data.data?.page?.media?.map { SyncAPI.SyncSearchResult( it.title.romaji ?: return null, @@ -99,16 +96,17 @@ class AniListApi : SyncAPI() { } } - override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? { + override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? { val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1) ?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId") val season = getSeason(internalId).data.media + return SyncAPI.SyncResult( season.id.toString(), nextAiring = season.nextAiringEpisode?.let { NextAiring( it.episode ?: return@let null, - (it.timeUntilAiring ?: return@let null) + APIHolder.unixTime + (it.timeUntilAiring ?: return@let null) + unixTime ) }, title = season.title?.userPreferred, @@ -156,13 +154,14 @@ class AniListApi : SyncAPI() { "youtube" -> listOf("https://www.youtube.com/watch?v=${season.trailer.id}") else -> null } - // TODO REST + //TODO REST ) } - override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? { + override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? { val internalId = id.toIntOrNull() ?: return null val data = getDataAboutId(auth ?: return null, internalId) ?: return null + return SyncAPI.SyncStatus( score = Score.from100(data.score), watchedEpisodes = data.progress, @@ -258,24 +257,24 @@ class AniListApi : SyncAPI() { val data = mapOf( "query" to query, - "variables" to Variables( - search = name, - page = 1, - type = "ANIME", - ).toJson() + "variables" to + mapOf( + "search" to name, + "page" to 1, + "type" to "ANIME" + ).toJson() ) val res = app.post( "https://graphql.anilist.co/", - // headers = mapOf(), - data = data, // (if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars)) + //headers = mapOf(), + data = data,//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars)) timeout = 5000 // REASONABLE TIMEOUT ).text.replace("\\", "") - return parseJson(res) + return res.toKotlinObject() } catch (e: Exception) { logError(e) } - return null } @@ -298,7 +297,7 @@ class AniListApi : SyncAPI() { .replace(")", "\\)") })""" ) - // println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}") + //println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}") val shows = searchShows(name.replace(blackListRegex, "")) shows?.data?.page?.media?.find { @@ -456,11 +455,11 @@ class AniListApi : SyncAPI() { cacheTime = 0, ).text - return tryParseJson(data) ?: throw ErrorLoadingException("Error parsing $data") + return tryParseJson(data) ?: throw ErrorLoadingException("Error parsing $data") } } - private suspend fun getDataAboutId(auth: AuthData, id: Int): AniListTitleHolder? { + private suspend fun getDataAboutId(auth : AuthData, id: Int): AniListTitleHolder? { val q = """query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id) Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query) @@ -504,9 +503,10 @@ class AniListApi : SyncAPI() { type = AniListStatusType.None, ) } + } - private suspend fun postApi(token: AuthToken, q: String, cache: Boolean = false): String? { + private suspend fun postApi(token : AuthToken, q: String, cache: Boolean = false): String? { return app.post( "https://graphql.anilist.co/", headers = mapOf( @@ -519,84 +519,71 @@ class AniListApi : SyncAPI() { q, "UTF-8" ) - ), // (if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars)) + ), //(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars)) timeout = 5 // REASONABLE TIMEOUT ).text.replace("\\/", "/") } - @Serializable - data class Variables( - @JsonProperty("search") @SerialName("search") val search: String, - @JsonProperty("page") @SerialName("page") val page: Int, - @JsonProperty("type") @SerialName("type") val type: String, - ) - @Serializable data class MediaRecommendation( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("title") @SerialName("title") val title: Title?, - @JsonProperty("idMal") @SerialName("idMal") val idMal: Int?, - @JsonProperty("coverImage") @SerialName("coverImage") val coverImage: CoverImage?, - @JsonProperty("averageScore") @SerialName("averageScore") val averageScore: Int?, + @JsonProperty("id") val id: Int, + @JsonProperty("title") val title: Title?, + @JsonProperty("idMal") val idMal: Int?, + @JsonProperty("coverImage") val coverImage: CoverImage?, + @JsonProperty("averageScore") val averageScore: Int? ) - @Serializable data class FullAnilistList( - @JsonProperty("data") @SerialName("data") val data: Data?, + @JsonProperty("data") val data: Data? ) - @Serializable data class CompletedAt( - @JsonProperty("year") @SerialName("year") val year: Int, - @JsonProperty("month") @SerialName("month") val month: Int, - @JsonProperty("day") @SerialName("day") val day: Int, + @JsonProperty("year") val year: Int, + @JsonProperty("month") val month: Int, + @JsonProperty("day") val day: Int ) - @Serializable data class StartedAt( - @JsonProperty("year") @SerialName("year") val year: String?, - @JsonProperty("month") @SerialName("month") val month: String?, - @JsonProperty("day") @SerialName("day") val day: String?, + @JsonProperty("year") val year: String?, + @JsonProperty("month") val month: String?, + @JsonProperty("day") val day: String? ) - @Serializable data class Title( - @JsonProperty("english") @SerialName("english") val english: String?, - @JsonProperty("romaji") @SerialName("romaji") val romaji: String?, + @JsonProperty("english") val english: String?, + @JsonProperty("romaji") val romaji: String? ) - @Serializable data class CoverImage( - @JsonProperty("medium") @SerialName("medium") val medium: String?, - @JsonProperty("large") @SerialName("large") val large: String?, - @JsonProperty("extraLarge") @SerialName("extraLarge") val extraLarge: String?, + @JsonProperty("medium") val medium: String?, + @JsonProperty("large") val large: String?, + @JsonProperty("extraLarge") val extraLarge: String? ) - @Serializable data class Media( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("idMal") @SerialName("idMal") val idMal: Int?, - @JsonProperty("season") @SerialName("season") val season: String?, - @JsonProperty("seasonYear") @SerialName("seasonYear") val seasonYear: Int, - @JsonProperty("format") @SerialName("format") val format: String?, - @JsonProperty("episodes") @SerialName("episodes") val episodes: Int, - @JsonProperty("title") @SerialName("title") val title: Title, - @JsonProperty("description") @SerialName("description") val description: String?, - @JsonProperty("coverImage") @SerialName("coverImage") val coverImage: CoverImage, - @JsonProperty("synonyms") @SerialName("synonyms") val synonyms: List, - @JsonProperty("nextAiringEpisode") @SerialName("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, + @JsonProperty("id") val id: Int, + @JsonProperty("idMal") val idMal: Int?, + @JsonProperty("season") val season: String?, + @JsonProperty("seasonYear") val seasonYear: Int, + @JsonProperty("format") val format: String?, + //@JsonProperty("source") val source: String, + @JsonProperty("episodes") val episodes: Int, + @JsonProperty("title") val title: Title, + @JsonProperty("description") val description: String?, + @JsonProperty("coverImage") val coverImage: CoverImage, + @JsonProperty("synonyms") val synonyms: List, + @JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, ) - @Serializable data class Entries( - @JsonProperty("status") @SerialName("status") val status: String?, - @JsonProperty("completedAt") @SerialName("completedAt") val completedAt: CompletedAt, - @JsonProperty("startedAt") @SerialName("startedAt") val startedAt: StartedAt, - @JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: Int, - @JsonProperty("progress") @SerialName("progress") val progress: Int, - @JsonProperty("score") @SerialName("score") val score: Int, - @JsonProperty("private") @SerialName("private") val private: Boolean, - @JsonProperty("media") @SerialName("media") val media: Media, + @JsonProperty("status") val status: String?, + @JsonProperty("completedAt") val completedAt: CompletedAt, + @JsonProperty("startedAt") val startedAt: StartedAt, + @JsonProperty("updatedAt") val updatedAt: Int, + @JsonProperty("progress") val progress: Int, + @JsonProperty("score") val score: Int, + @JsonProperty("private") val private: Boolean, + @JsonProperty("media") val media: Media ) { fun toLibraryItem(): SyncAPI.LibraryItem { return SyncAPI.LibraryItem( @@ -623,20 +610,17 @@ class AniListApi : SyncAPI() { } } - @Serializable data class Lists( - @JsonProperty("status") @SerialName("status") val status: String?, - @JsonProperty("entries") @SerialName("entries") val entries: List, + @JsonProperty("status") val status: String?, + @JsonProperty("entries") val entries: List ) - @Serializable data class MediaListCollection( - @JsonProperty("lists") @SerialName("lists") val lists: List, + @JsonProperty("lists") val lists: List ) - @Serializable data class Data( - @JsonProperty("MediaListCollection") @SerialName("MediaListCollection") val mediaListCollection: MediaListCollection, + @JsonProperty("MediaListCollection") val mediaListCollection: MediaListCollection ) private suspend fun getAniListAnimeListSmart(auth: AuthData): Array? { @@ -654,7 +638,7 @@ class AniListApi : SyncAPI() { } } - override suspend fun library(auth: AuthData?): SyncAPI.LibraryMetadata? { + override suspend fun library(auth : AuthData?): SyncAPI.LibraryMetadata? { val list = getAniListAnimeListSmart(auth ?: return null)?.groupBy { convertAniListStringToStatus(it.status ?: "").stringRes }?.mapValues { group -> @@ -682,9 +666,10 @@ class AniListApi : SyncAPI() { ) } - private suspend fun getFullAniListList(auth: AuthData): FullAnilistList? { + private suspend fun getFullAniListList(auth : AuthData): FullAnilistList? { val userID = auth.user.id val mediaType = "ANIME" + val query = """ query (${'$'}userID: Int = $userID, ${'$'}MEDIA: MediaType = $mediaType) { MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) { @@ -723,53 +708,43 @@ class AniListApi : SyncAPI() { } } } - } + } """ val text = postApi(auth.token, query) - return tryParseJson(text) + return text?.toKotlinObject() } - suspend fun toggleLike(auth: AuthData, id: Int): Boolean { + suspend fun toggleLike(auth : AuthData, id: Int): Boolean { val q = """mutation (${'$'}animeId: Int = $id) { - ToggleFavourite (animeId: ${'$'}animeId) { - anime { - nodes { - id - title { - romaji - } - } - } - } - }""" + ToggleFavourite (animeId: ${'$'}animeId) { + anime { + nodes { + id + title { + romaji + } + } + } + } + }""" val data = postApi(auth.token, q) return data != "" } /** Used to query a saved MediaItem on the list to get the id for removal */ - @Serializable - data class MediaListItemRoot( - @JsonProperty("data") @SerialName("data") val data: MediaListItem? = null, - ) - - @Serializable - data class MediaListItem( - @JsonProperty("MediaList") @SerialName("MediaList") val mediaList: MediaListId? = null, - ) - - @Serializable - data class MediaListId( - @JsonProperty("id") @SerialName("id") val id: Long? = null, - ) + data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null) + data class MediaListItem(@JsonProperty("MediaList") val mediaList: MediaListId? = null) + data class MediaListId(@JsonProperty("id") val id: Long? = null) private suspend fun postDataAboutId( - auth: AuthData, + auth : AuthData, id: Int, type: AniListStatusType, score: Score?, progress: Int? ): Boolean { val userID = auth.user.id + val q = // Delete item if status type is None if (type == AniListStatusType.None) { @@ -811,24 +786,24 @@ class AniListApi : SyncAPI() { return data != "" } - private suspend fun getUser(token: AuthToken): AniListUser? { + private suspend fun getUser(token : AuthToken): AniListUser? { val q = """ - { - Viewer { - id - name - avatar { - large - } - favourites { - anime { - nodes { - id + { + Viewer { + id + name + avatar { + large + } + favourites { + anime { + nodes { + id + } } } - } - } - }""" + } + }""" val data = postApi(token, q) if (data.isNullOrBlank()) return null val userData = parseJson(data) @@ -861,356 +836,305 @@ class AniListApi : SyncAPI() { return seasons.toList() } - @Serializable data class SeasonResponse( - @JsonProperty("data") @SerialName("data") val data: SeasonData, + @JsonProperty("data") val data: SeasonData, ) - @Serializable data class SeasonData( - @JsonProperty("Media") @SerialName("Media") val media: SeasonMedia, + @JsonProperty("Media") val media: SeasonMedia, ) - @Serializable - data class RecommendedMedia( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("title") @SerialName("title") val title: MediaTitle?, - @JsonProperty("coverImage") @SerialName("coverImage") val coverImage: MediaCoverImage?, - ) - - @Serializable - data class CharacterMedia( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("title") @SerialName("title") val title: MediaTitle?, - @JsonProperty("coverImage") @SerialName("coverImage") val coverImage: MediaCoverImage?, - ) - - @Serializable data class SeasonMedia( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("title") @SerialName("title") val title: MediaTitle?, - @JsonProperty("idMal") @SerialName("idMal") val idMal: Int?, - @JsonProperty("format") @SerialName("format") val format: String?, - @JsonProperty("nextAiringEpisode") @SerialName("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, - @JsonProperty("relations") @SerialName("relations") val relations: SeasonEdges?, - @JsonProperty("coverImage") @SerialName("coverImage") val coverImage: MediaCoverImage?, - @JsonProperty("duration") @SerialName("duration") val duration: Int?, - @JsonProperty("episodes") @SerialName("episodes") val episodes: Int?, - @JsonProperty("genres") @SerialName("genres") val genres: List?, - @JsonProperty("synonyms") @SerialName("synonyms") val synonyms: List?, - @JsonProperty("averageScore") @SerialName("averageScore") val averageScore: Int?, - @JsonProperty("isAdult") @SerialName("isAdult") val isAdult: Boolean?, - @JsonProperty("trailer") @SerialName("trailer") val trailer: MediaTrailer?, - @JsonProperty("description") @SerialName("description") val description: String?, - @JsonProperty("characters") @SerialName("characters") val characters: CharacterConnection?, - @JsonProperty("recommendations") @SerialName("recommendations") val recommendations: RecommendationConnection?, + @JsonProperty("id") val id: Int?, + @JsonProperty("title") val title: MediaTitle?, + @JsonProperty("idMal") val idMal: Int?, + @JsonProperty("format") val format: String?, + @JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, + @JsonProperty("relations") val relations: SeasonEdges?, + @JsonProperty("coverImage") val coverImage: MediaCoverImage?, + @JsonProperty("duration") val duration: Int?, + @JsonProperty("episodes") val episodes: Int?, + @JsonProperty("genres") val genres: List?, + @JsonProperty("synonyms") val synonyms: List?, + @JsonProperty("averageScore") val averageScore: Int?, + @JsonProperty("isAdult") val isAdult: Boolean?, + @JsonProperty("trailer") val trailer: MediaTrailer?, + @JsonProperty("description") val description: String?, + @JsonProperty("characters") val characters: CharacterConnection?, + @JsonProperty("recommendations") val recommendations: RecommendationConnection?, ) - @Serializable data class RecommendationConnection( - @JsonProperty("edges") @SerialName("edges") val edges: List = emptyList(), - @JsonProperty("nodes") @SerialName("nodes") val nodes: List = emptyList(), + @JsonProperty("edges") val edges: List = emptyList(), + @JsonProperty("nodes") val nodes: List = emptyList(), + //@JsonProperty("pageInfo") val pageInfo: PageInfo, ) - @Serializable data class RecommendationEdge( - @JsonProperty("node") @SerialName("node") val node: Recommendation, + //@JsonProperty("rating") val rating: Int, + @JsonProperty("node") val node: Recommendation, ) - @Serializable data class Recommendation( - @JsonProperty("mediaRecommendation") @SerialName("mediaRecommendation") val mediaRecommendation: RecommendedMedia?, + val id: Long, + @JsonProperty("mediaRecommendation") val mediaRecommendation: SeasonMedia?, ) - @Serializable data class CharacterName( - @JsonProperty("name") @SerialName("name") val first: String?, - @JsonProperty("middle") @SerialName("middle") val middle: String?, - @JsonProperty("last") @SerialName("last") val last: String?, - @JsonProperty("full") @SerialName("full") val full: String?, - @JsonProperty("native") @SerialName("native") val native: String?, - @JsonProperty("alternative") @SerialName("alternative") val alternative: List?, - @JsonProperty("alternativeSpoiler") @SerialName("alternativeSpoiler") val alternativeSpoiler: List?, - @JsonProperty("userPreferred") @SerialName("userPreferred") val userPreferred: String?, + @JsonProperty("name") val first: String?, + @JsonProperty("middle") val middle: String?, + @JsonProperty("last") val last: String?, + @JsonProperty("full") val full: String?, + @JsonProperty("native") val native: String?, + @JsonProperty("alternative") val alternative: List?, + @JsonProperty("alternativeSpoiler") val alternativeSpoiler: List?, + @JsonProperty("userPreferred") val userPreferred: String?, ) - @Serializable data class CharacterImage( - @JsonProperty("large") @SerialName("large") val large: String?, - @JsonProperty("medium") @SerialName("medium") val medium: String?, + @JsonProperty("large") val large: String?, + @JsonProperty("medium") val medium: String?, ) - @Serializable data class Character( - @JsonProperty("name") @SerialName("name") val name: CharacterName?, - @JsonProperty("age") @SerialName("age") val age: String?, - @JsonProperty("image") @SerialName("image") val image: CharacterImage?, + @JsonProperty("name") val name: CharacterName?, + @JsonProperty("age") val age: String?, + @JsonProperty("image") val image: CharacterImage?, ) - @Serializable data class CharacterEdge( - @JsonProperty("id") @SerialName("id") val id: Int?, + @JsonProperty("id") val id: Int?, /** - * MAIN - A primary character role in the media - * SUPPORTING - A supporting character role in the media - * BACKGROUND - A background character in the media + MAIN + A primary character role in the media + + SUPPORTING + A supporting character role in the media + + BACKGROUND + A background character in the media */ - @JsonProperty("role") @SerialName("role") val role: String?, - @JsonProperty("name") @SerialName("name") val name: String?, - @JsonProperty("voiceActors") @SerialName("voiceActors") val voiceActors: List?, - @JsonProperty("favouriteOrder") @SerialName("favouriteOrder") val favouriteOrder: Int?, - @JsonProperty("media") @SerialName("media") val media: List?, - @JsonProperty("node") @SerialName("node") val node: Character?, + @JsonProperty("role") val role: String?, + @JsonProperty("name") val name: String?, + @JsonProperty("voiceActors") val voiceActors: List?, + @JsonProperty("favouriteOrder") val favouriteOrder: Int?, + @JsonProperty("media") val media: List?, + @JsonProperty("node") val node: Character?, ) - @Serializable data class StaffImage( - @JsonProperty("large") @SerialName("large") val large: String?, - @JsonProperty("medium") @SerialName("medium") val medium: String?, + @JsonProperty("large") val large: String?, + @JsonProperty("medium") val medium: String?, ) - @Serializable data class StaffName( - @JsonProperty("name") @SerialName("name") val first: String?, - @JsonProperty("middle") @SerialName("middle") val middle: String?, - @JsonProperty("last") @SerialName("last") val last: String?, - @JsonProperty("full") @SerialName("full") val full: String?, - @JsonProperty("native") @SerialName("native") val native: String?, - @JsonProperty("alternative") @SerialName("alternative") val alternative: List?, - @JsonProperty("userPreferred") @SerialName("userPreferred") val userPreferred: String?, + @JsonProperty("name") val first: String?, + @JsonProperty("middle") val middle: String?, + @JsonProperty("last") val last: String?, + @JsonProperty("full") val full: String?, + @JsonProperty("native") val native: String?, + @JsonProperty("alternative") val alternative: List?, + @JsonProperty("userPreferred") val userPreferred: String?, ) - @Serializable data class Staff( - @JsonProperty("image") @SerialName("image") val image: StaffImage?, - @JsonProperty("name") @SerialName("name") val name: StaffName?, - @JsonProperty("age") @SerialName("age") val age: Int?, + @JsonProperty("image") val image: StaffImage?, + @JsonProperty("name") val name: StaffName?, + @JsonProperty("age") val age: Int?, ) - @Serializable data class CharacterConnection( - @JsonProperty("edges") @SerialName("edges") val edges: List?, - @JsonProperty("nodes") @SerialName("nodes") val nodes: List?, + @JsonProperty("edges") val edges: List?, + @JsonProperty("nodes") val nodes: List?, + //@JsonProperty("pageInfo") pageInfo: PageInfo ) - @Serializable data class MediaTrailer( - @JsonProperty("id") @SerialName("id") val id: String?, - @JsonProperty("site") @SerialName("site") val site: String?, - @JsonProperty("thumbnail") @SerialName("thumbnail") val thumbnail: String?, + @JsonProperty("id") val id: String?, + @JsonProperty("site") val site: String?, + @JsonProperty("thumbnail") val thumbnail: String?, ) - @Serializable data class MediaCoverImage( - @JsonProperty("extraLarge") @SerialName("extraLarge") val extraLarge: String?, - @JsonProperty("large") @SerialName("large") val large: String?, - @JsonProperty("medium") @SerialName("medium") val medium: String?, - @JsonProperty("color") @SerialName("color") val color: String?, + @JsonProperty("extraLarge") val extraLarge: String?, + @JsonProperty("large") val large: String?, + @JsonProperty("medium") val medium: String?, + @JsonProperty("color") val color: String?, ) - @Serializable data class SeasonNextAiringEpisode( - @JsonProperty("episode") @SerialName("episode") val episode: Int?, - @JsonProperty("timeUntilAiring") @SerialName("timeUntilAiring") val timeUntilAiring: Int?, + @JsonProperty("episode") val episode: Int?, + @JsonProperty("timeUntilAiring") val timeUntilAiring: Int?, ) - @Serializable data class SeasonEdges( - @JsonProperty("edges") @SerialName("edges") val edges: List?, + @JsonProperty("edges") val edges: List?, ) - @Serializable data class SeasonEdge( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("relationType") @SerialName("relationType") val relationType: String?, - @JsonProperty("node") @SerialName("node") val node: SeasonNode?, + @JsonProperty("id") val id: Int?, + @JsonProperty("relationType") val relationType: String?, + @JsonProperty("node") val node: SeasonNode?, ) - @Serializable data class AniListFavoritesMediaConnection( - @JsonProperty("nodes") @SerialName("nodes") val nodes: List, + @JsonProperty("nodes") val nodes: List, ) - @Serializable data class AniListFavourites( - @JsonProperty("anime") @SerialName("anime") val anime: AniListFavoritesMediaConnection, + @JsonProperty("anime") val anime: AniListFavoritesMediaConnection, ) - @Serializable data class MediaTitle( - @JsonProperty("romaji") @SerialName("romaji") val romaji: String?, - @JsonProperty("english") @SerialName("english") val english: String?, - @JsonProperty("native") @SerialName("native") val native: String?, - @JsonProperty("userPreferred") @SerialName("userPreferred") val userPreferred: String?, + @JsonProperty("romaji") val romaji: String?, + @JsonProperty("english") val english: String?, + @JsonProperty("native") val native: String?, + @JsonProperty("userPreferred") val userPreferred: String?, ) - @Serializable data class SeasonNode( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("format") @SerialName("format") val format: String?, - @JsonProperty("title") @SerialName("title") val title: Title?, - @JsonProperty("idMal") @SerialName("idMal") val idMal: Int?, - @JsonProperty("coverImage") @SerialName("coverImage") val coverImage: CoverImage?, - @JsonProperty("averageScore") @SerialName("averageScore") val averageScore: Int?, + @JsonProperty("id") val id: Int, + @JsonProperty("format") val format: String?, + @JsonProperty("title") val title: Title?, + @JsonProperty("idMal") val idMal: Int?, + @JsonProperty("coverImage") val coverImage: CoverImage?, + @JsonProperty("averageScore") val averageScore: Int? +// @JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, ) - @Serializable data class AniListAvatar( - @JsonProperty("large") @SerialName("large") val large: String?, + @JsonProperty("large") val large: String?, ) - @Serializable data class AniListViewer( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("avatar") @SerialName("avatar") val avatar: AniListAvatar?, - @JsonProperty("favourites") @SerialName("favourites") val favourites: AniListFavourites?, + @JsonProperty("id") val id: Int, + @JsonProperty("name") val name: String, + @JsonProperty("avatar") val avatar: AniListAvatar?, + @JsonProperty("favourites") val favourites: AniListFavourites?, ) - @Serializable data class AniListData( - @JsonProperty("Viewer") @SerialName("Viewer") val viewer: AniListViewer?, + @JsonProperty("Viewer") val viewer: AniListViewer?, ) - @Serializable data class AniListRoot( - @JsonProperty("data") @SerialName("data") val data: AniListData?, + @JsonProperty("data") val data: AniListData?, ) - @Serializable data class AniListUser( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("picture") @SerialName("picture") val picture: String?, + @JsonProperty("id") val id: Int, + @JsonProperty("name") val name: String, + @JsonProperty("picture") val picture: String?, ) - @Serializable data class LikeNode( - @JsonProperty("id") @SerialName("id") val id: Int?, + @JsonProperty("id") val id: Int?, + //@JsonProperty("idMal") public int idMal; ) - @Serializable data class LikePageInfo( - @JsonProperty("total") @SerialName("total") val total: Int?, - @JsonProperty("currentPage") @SerialName("currentPage") val currentPage: Int?, - @JsonProperty("lastPage") @SerialName("lastPage") val lastPage: Int?, - @JsonProperty("perPage") @SerialName("perPage") val perPage: Int?, - @JsonProperty("hasNextPage") @SerialName("hasNextPage") val hasNextPage: Boolean?, + @JsonProperty("total") val total: Int?, + @JsonProperty("currentPage") val currentPage: Int?, + @JsonProperty("lastPage") val lastPage: Int?, + @JsonProperty("perPage") val perPage: Int?, + @JsonProperty("hasNextPage") val hasNextPage: Boolean?, ) - @Serializable data class LikeAnime( - @JsonProperty("nodes") @SerialName("nodes") val nodes: List?, - @JsonProperty("pageInfo") @SerialName("pageInfo") val pageInfo: LikePageInfo?, + @JsonProperty("nodes") val nodes: List?, + @JsonProperty("pageInfo") val pageInfo: LikePageInfo?, ) - @Serializable data class LikeFavourites( - @JsonProperty("anime") @SerialName("anime") val anime: LikeAnime?, + @JsonProperty("anime") val anime: LikeAnime?, ) - @Serializable data class LikeViewer( - @JsonProperty("favourites") @SerialName("favourites") val favourites: LikeFavourites?, + @JsonProperty("favourites") val favourites: LikeFavourites?, ) - @Serializable data class LikeData( - @JsonProperty("Viewer") @SerialName("Viewer") val viewer: LikeViewer?, + @JsonProperty("Viewer") val viewer: LikeViewer?, ) - @Serializable data class LikeRoot( - @JsonProperty("data") @SerialName("data") val data: LikeData?, + @JsonProperty("data") val data: LikeData?, ) - @Serializable data class AniListTitleHolder( - @JsonProperty("title") @SerialName("title") val title: Title?, - @JsonProperty("isFavourite") @SerialName("isFavourite") val isFavourite: Boolean?, - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("progress") @SerialName("progress") val progress: Int?, - @JsonProperty("episodes") @SerialName("episodes") val episodes: Int?, - @JsonProperty("score") @SerialName("score") val score: Int?, - @JsonProperty("type") @SerialName("type") val type: AniListStatusType?, + @JsonProperty("title") val title: Title?, + @JsonProperty("isFavourite") val isFavourite: Boolean?, + @JsonProperty("id") val id: Int?, + @JsonProperty("progress") val progress: Int?, + @JsonProperty("episodes") val episodes: Int?, + @JsonProperty("score") val score: Int?, + @JsonProperty("type") val type: AniListStatusType?, ) - @Serializable data class GetDataMediaListEntry( - @JsonProperty("progress") @SerialName("progress") val progress: Int?, - @JsonProperty("status") @SerialName("status") val status: String?, - @JsonProperty("score") @SerialName("score") val score: Int?, + @JsonProperty("progress") val progress: Int?, + @JsonProperty("status") val status: String?, + @JsonProperty("score") val score: Int?, ) - @Serializable data class Nodes( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("mediaRecommendation") @SerialName("mediaRecommendation") val mediaRecommendation: MediaRecommendation?, + @JsonProperty("id") val id: Int?, + @JsonProperty("mediaRecommendation") val mediaRecommendation: MediaRecommendation? ) - @Serializable data class GetDataMedia( - @JsonProperty("isFavourite") @SerialName("isFavourite") val isFavourite: Boolean?, - @JsonProperty("episodes") @SerialName("episodes") val episodes: Int?, - @JsonProperty("title") @SerialName("title") val title: Title?, - @JsonProperty("mediaListEntry") @SerialName("mediaListEntry") val mediaListEntry: GetDataMediaListEntry?, + @JsonProperty("isFavourite") val isFavourite: Boolean?, + @JsonProperty("episodes") val episodes: Int?, + @JsonProperty("title") val title: Title?, + @JsonProperty("mediaListEntry") val mediaListEntry: GetDataMediaListEntry? ) - @Serializable data class Recommendations( - @JsonProperty("nodes") @SerialName("nodes") val nodes: List?, + @JsonProperty("nodes") val nodes: List? ) - @Serializable data class GetDataData( - @JsonProperty("Media") @SerialName("Media") val media: GetDataMedia?, + @JsonProperty("Media") val media: GetDataMedia?, ) - @Serializable data class GetDataRoot( - @JsonProperty("data") @SerialName("data") val data: GetDataData?, + @JsonProperty("data") val data: GetDataData?, ) - @Serializable data class GetSearchTitle( - @JsonProperty("romaji") @SerialName("romaji") val romaji: String?, + @JsonProperty("romaji") val romaji: String?, ) - @Serializable data class TrailerObject( - @JsonProperty("id") @SerialName("id") val id: String?, - @JsonProperty("thumbnail") @SerialName("thumbnail") val thumbnail: String?, - @JsonProperty("site") @SerialName("site") val site: String?, + @JsonProperty("id") val id: String?, + @JsonProperty("thumbnail") val thumbnail: String?, + @JsonProperty("site") val site: String?, ) - @Serializable data class GetSearchMedia( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("idMal") @SerialName("idMal") val idMal: Int?, - @JsonProperty("seasonYear") @SerialName("seasonYear") val seasonYear: Int, - @JsonProperty("title") @SerialName("title") val title: GetSearchTitle, - @JsonProperty("startDate") @SerialName("startDate") val startDate: StartedAt, - @JsonProperty("averageScore") @SerialName("averageScore") val averageScore: Int?, - @JsonProperty("meanScore") @SerialName("meanScore") val meanScore: Int?, - @JsonProperty("bannerImage") @SerialName("bannerImage") val bannerImage: String?, - @JsonProperty("trailer") @SerialName("trailer") val trailer: TrailerObject?, - @JsonProperty("nextAiringEpisode") @SerialName("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, - @JsonProperty("recommendations") @SerialName("recommendations") val recommendations: Recommendations?, - @JsonProperty("relations") @SerialName("relations") val relations: SeasonEdges?, + @JsonProperty("id") val id: Int, + @JsonProperty("idMal") val idMal: Int?, + @JsonProperty("seasonYear") val seasonYear: Int, + @JsonProperty("title") val title: GetSearchTitle, + @JsonProperty("startDate") val startDate: StartedAt, + @JsonProperty("averageScore") val averageScore: Int?, + @JsonProperty("meanScore") val meanScore: Int?, + @JsonProperty("bannerImage") val bannerImage: String?, + @JsonProperty("trailer") val trailer: TrailerObject?, + @JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, + @JsonProperty("recommendations") val recommendations: Recommendations?, + @JsonProperty("relations") val relations: SeasonEdges? ) - @Serializable data class GetSearchPage( - @JsonProperty("Page") @SerialName("Page") val page: GetSearchData?, + @JsonProperty("Page") val page: GetSearchData?, ) - @Serializable data class GetSearchData( - @JsonProperty("media") @SerialName("media") val media: List?, + @JsonProperty("media") val media: List?, ) - @Serializable data class GetSearchRoot( - @JsonProperty("data") @SerialName("data") val data: GetSearchPage?, + @JsonProperty("data") val data: GetSearchPage?, ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt index c613aa6ea..29c3c0c17 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt @@ -1,8 +1,8 @@ package com.lagradost.cloudstream3.syncproviders.providers + import androidx.annotation.StringRes import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey import com.lagradost.cloudstream3.R @@ -22,15 +22,14 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.txt -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import okhttp3.Interceptor import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import java.text.SimpleDateFormat +import java.time.Instant import java.time.LocalDate -import java.time.ZoneId +import java.time.format.DateTimeFormatter import java.util.Date import java.util.Locale @@ -69,9 +68,13 @@ class KitsuApi: SyncAPI() { val request: Request = chain.request() try { + val response = chain.proceed(request); + if (response.isSuccessful) return response + response.close() + } catch (_: Exception) { } @@ -80,6 +83,7 @@ class KitsuApi: SyncAPI() { .build() return chain.proceed(fallbackRequest) + } } @@ -103,7 +107,7 @@ class KitsuApi: SyncAPI() { ).parsed() return AuthToken( - accessTokenLifetime = APIHolder.unixTime + token.expiresIn.toLong(), + accessTokenLifetime = unixTime + token.expiresIn.toLong(), refreshToken = token.refreshToken, accessToken = token.accessToken, ) @@ -122,7 +126,7 @@ class KitsuApi: SyncAPI() { return AuthToken( accessToken = res.accessToken, refreshToken = res.refreshToken, - accessTokenLifetime = APIHolder.unixTime + res.expiresIn.toLong() + accessTokenLifetime = unixTime + res.expiresIn.toLong() ) } @@ -179,9 +183,9 @@ class KitsuApi: SyncAPI() { return null } - @Serializable data class KitsuResponse( - @JsonProperty("data") @SerialName("data") val data: KitsuNode, + @field:JsonProperty(value = "data") + val data: KitsuNode, ) val url = @@ -198,10 +202,10 @@ class KitsuApi: SyncAPI() { id = id, totalEpisodes = anime.episodeCount, title = anime.canonicalTitle ?: anime.titles?.enJp ?: anime.titles?.jaJp.orEmpty(), - publicScore = Score.from(anime.ratingTwenty, 20), + publicScore = Score.from(anime.ratingTwenty.toString(), 20), duration = anime.episodeLength, synopsis = anime.synopsis, - airStatus = when (anime.status) { + airStatus = when(anime.status) { "finished" -> ShowStatus.Completed "current" -> ShowStatus.Ongoing else -> null @@ -217,6 +221,7 @@ class KitsuApi: SyncAPI() { prevSeason = null, actors = null, ) + } override suspend fun status(auth : AuthData?, id: String): AbstractSyncStatus? { @@ -245,19 +250,21 @@ class KitsuApi: SyncAPI() { } return SyncStatus( - score = Score.from(anime.ratingTwenty, 20), + score = Score.from(anime.ratingTwenty.toString(), 20), status = SyncWatchType.fromInternalId(kitsuStatusAsString.indexOf(anime.status)), isFavorite = null, watchedEpisodes = anime.progress, ) } - suspend fun getAnimeIdByTitle(title: String): String? { + val animeSelectedFields = arrayOf("titles","canonicalTitle") val url = "$apiUrl/anime?filter[text]=$title&page[limit]=$KITSU_MAX_SEARCH_LIMIT&fields[anime]=${animeSelectedFields.joinToString(",")}" val res = app.get(url, interceptor = apiFallbackInterceptor).parsed() + return res.data.firstOrNull()?.id + } override fun urlToId(url: String): String? = @@ -268,6 +275,7 @@ class KitsuApi: SyncAPI() { id: String, newStatus: AbstractSyncStatus ): Boolean { + return setScoreRequest( auth ?: return false, id.toIntOrNull() ?: return false, @@ -278,18 +286,21 @@ class KitsuApi: SyncAPI() { } private suspend fun setScoreRequest( - auth: AuthData, + auth : AuthData, id: Int, status: KitsuStatusType? = null, score: Int? = null, numWatchedEpisodes: Int? = null, ): Boolean { + val libraryEntryId = getAnimeLibraryEntryId(auth, id) // Exists entry for anime in library if (libraryEntryId != null) { + // Delete anime from library if (status == null || status == KitsuStatusType.None) { + val res = app.delete( "$apiUrl/library-entries/$libraryEntryId", headers = mapOf( @@ -298,7 +309,9 @@ class KitsuApi: SyncAPI() { interceptor = apiFallbackInterceptor ) + return res.isSuccessful + } return setScoreRequest( @@ -308,6 +321,7 @@ class KitsuApi: SyncAPI() { score, numWatchedEpisodes ) + } val data = mapOf( @@ -346,6 +360,7 @@ class KitsuApi: SyncAPI() { ) return res.isSuccessful + } @Suppress("UNCHECKED_CAST") @@ -378,11 +393,15 @@ class KitsuApi: SyncAPI() { interceptor = apiFallbackInterceptor ) + return res.isSuccessful + } private suspend fun getAnimeLibraryEntryId(auth: AuthData, id: Int): Int? { + val userId = auth.user.id + val res = app.get( "$apiUrl/library-entries?filter[userId]=$userId&filter[animeId]=$id", headers = mapOf( @@ -392,6 +411,7 @@ class KitsuApi: SyncAPI() { ).parsed().data.firstOrNull() ?: return null return res.id.toInt() + } override suspend fun library(auth : AuthData?): LibraryMetadata? { @@ -433,52 +453,58 @@ class KitsuApi: SyncAPI() { } private suspend fun getKitsuAnimeList(token: AuthToken, userId: Int): Array { - val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","endDate","episodeCount") - val libraryEntriesSelectedFields = arrayOf("progress","ratingTwenty","updatedAt", "status") + + val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","episodeCount") + val libraryEntriesSelectedFields = arrayOf("progress","rating","updatedAt", "status") val limit = 500 var url = "$apiUrl/library-entries?filter[userId]=$userId&filter[kind]=anime&include=anime&page[limit]=$limit&page[offset]=0&fields[anime]=${animeSelectedFields.joinToString(",")}&fields[libraryEntries]=${libraryEntriesSelectedFields.joinToString(",")}" val fullList = mutableListOf() while (true) { + val data: KitsuResponse = getKitsuAnimeListSlice(token, url) + data.data.forEachIndexed { index, value -> value.anime = data.included?.get(index) } fullList.addAll(data.data) + url = data.links?.next ?: break } + return fullList.toTypedArray() } private suspend fun getKitsuAnimeListSlice(token: AuthToken, url: String): KitsuResponse { - return app.get( + val res = app.get( url, headers = mapOf( "Authorization" to "Bearer ${token.accessToken}", ), interceptor = apiFallbackInterceptor ).parsed() + return res } - @Serializable + data class ResponseToken( - @JsonProperty("token_type") @SerialName("token_type") val tokenType: String, - @JsonProperty("expires_in") @SerialName("expires_in") val expiresIn: Int, - @JsonProperty("access_token") @SerialName("access_token") val accessToken: String, - @JsonProperty("refresh_token") @SerialName("refresh_token") val refreshToken: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("expires_in") val expiresIn: Int, + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("refresh_token") val refreshToken: String, ) - @Serializable data class KitsuNode( - @JsonProperty("id") @SerialName("id") val id: String, - @JsonProperty("attributes") @SerialName("attributes") val attributes: KitsuNodeAttributes, + @JsonProperty("id") val id: String, + @JsonProperty("attributes") val attributes: KitsuNodeAttributes, /* User list anime node */ - @JsonProperty("relationships") @SerialName("relationships") val relationships: KitsuRelationships?, - @JsonProperty("anime") @SerialName("anime") var anime: KitsuAnimeData?, + @JsonProperty("relationships") val relationships: KitsuRelationships?, + var anime: KitsuAnimeData? ) { fun toLibraryItem(): LibraryItem { + val animeItem = this.anime val numEpisodes = animeItem?.attributes?.episodeCount @@ -500,7 +526,7 @@ class KitsuApi: SyncAPI() { this.id, this.attributes.progress, numEpisodes, - Score.from(this.attributes.ratingTwenty, 20), + Score.from(this.attributes.ratingTwenty.toString(), 20), parseDateLong(this.attributes.updatedAt), "Kitsu", TvType.Anime, @@ -509,9 +535,12 @@ class KitsuApi: SyncAPI() { null, plot = synopsis, releaseDate = if (startDate == null) null else try { - Date.from(LocalDate.parse(startDate).atStartOfDay() - .atZone(ZoneId.systemDefault()) - .toInstant()) + Date.from( + Instant.from( + DateTimeFormatter.ofPattern(if (startDate.length == 4) "yyyy" else if (startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd") + .parse(startDate) + ) + ) } catch (_: RuntimeException) { null } @@ -520,100 +549,93 @@ class KitsuApi: SyncAPI() { } - @Serializable data class KitsuAnimeAttributes( - @JsonProperty("titles") @SerialName("titles") val titles: KitsuTitles?, - @JsonProperty("canonicalTitle") @SerialName("canonicalTitle") val canonicalTitle: String?, - @JsonProperty("posterImage") @SerialName("posterImage") val posterImage: KitsuPosterImage?, - @JsonProperty("synopsis") @SerialName("synopsis") val synopsis: String?, - @JsonProperty("startDate") @SerialName("startDate") val startDate: String?, - @JsonProperty("endDate") @SerialName("endDate") val endDate: String?, - @JsonProperty("episodeCount") @SerialName("episodeCount") val episodeCount: Int?, - @JsonProperty("episodeLength") @SerialName("episodeLength") val episodeLength: Int?, + @JsonProperty("titles") val titles: KitsuTitles?, + @JsonProperty("canonicalTitle") val canonicalTitle: String?, + @JsonProperty("posterImage") val posterImage: KitsuPosterImage?, + @JsonProperty("synopsis") val synopsis: String?, + @JsonProperty("startDate") val startDate: String?, + @JsonProperty("endDate") val endDate: String?, + @JsonProperty("episodeCount") val episodeCount: Int?, + @JsonProperty("episodeLength") val episodeLength: Int?, ) - @Serializable data class KitsuAnimeData( - @JsonProperty("id") @SerialName("id") val id: String, - @JsonProperty("attributes") @SerialName("attributes") val attributes: KitsuAnimeAttributes, + @JsonProperty("id") val id: String, + @JsonProperty("attributes") val attributes: KitsuAnimeAttributes, ) - @Serializable + data class KitsuNodeAttributes( /* General attributes */ - @JsonProperty("titles") @SerialName("titles") val titles: KitsuTitles?, - @JsonProperty("canonicalTitle") @SerialName("canonicalTitle") val canonicalTitle: String?, - @JsonProperty("posterImage") @SerialName("posterImage") val posterImage: KitsuPosterImage?, - @JsonProperty("synopsis") @SerialName("synopsis") val synopsis: String?, - @JsonProperty("startDate") @SerialName("startDate") val startDate: String?, - @JsonProperty("endDate") @SerialName("endDate") val endDate: String?, - @JsonProperty("episodeCount") @SerialName("episodeCount") val episodeCount: Int?, - @JsonProperty("episodeLength") @SerialName("episodeLength") val episodeLength: Int?, + @JsonProperty("titles") val titles: KitsuTitles?, + @JsonProperty("canonicalTitle") val canonicalTitle: String?, + @JsonProperty("posterImage") val posterImage: KitsuPosterImage?, + @JsonProperty("synopsis") val synopsis: String?, + @JsonProperty("startDate") val startDate: String?, + @JsonProperty("endDate") val endDate: String?, + @JsonProperty("episodeCount") val episodeCount: Int?, + @JsonProperty("episodeLength") val episodeLength: Int?, /* User attributes */ - @JsonProperty("name") @SerialName("name") val name: String?, - @JsonProperty("location") @SerialName("location") val location: String?, - @JsonProperty("createdAt") @SerialName("createdAt") val createdAt: String?, - @JsonProperty("avatar") @SerialName("avatar") val avatar: KitsuUserAvatar?, + @JsonProperty("name") val name: String?, + @JsonProperty("location") val location: String?, + @JsonProperty("createdAt") val createdAt: String?, + @JsonProperty("avatar") val avatar: KitsuUserAvatar?, /* User list anime attributes */ - @JsonProperty("progress") @SerialName("progress") val progress: Int?, - @JsonProperty("ratingTwenty") @SerialName("ratingTwenty") val ratingTwenty: Int?, - @JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: String?, - @JsonProperty("status") @SerialName("status") val status: String?, + @JsonProperty("progress") val progress: Int?, + @JsonProperty("ratingTwenty") val ratingTwenty: Float?, + @JsonProperty("updatedAt") val updatedAt: String?, + @JsonProperty("status") val status: String?, ) - @Serializable data class KitsuRelationships( - @JsonProperty("anime") @SerialName("anime") val anime: KitsuRelationshipsAnime?, + @JsonProperty("anime") val anime: KitsuRelationshipsAnime? ) - @Serializable data class KitsuRelationshipsAnime( - @JsonProperty("links") @SerialName("links") val links: KitsuLinks?, + @JsonProperty("links") val links: KitsuLinks? ) - @Serializable data class KitsuPosterImage( - @JsonProperty("large") @SerialName("large") val large: String?, - @JsonProperty("medium") @SerialName("medium") val medium: String?, + @JsonProperty("large") val large: String?, + @JsonProperty("medium") val medium: String?, ) - @Serializable data class KitsuTitles( - @JsonProperty("en_jp") @SerialName("en_jp") val enJp: String?, - @JsonProperty("ja_jp") @SerialName("ja_jp") val jaJp: String?, + @JsonProperty("en_jp") val enJp: String?, + @JsonProperty("ja_jp") val jaJp: String? ) - @Serializable data class KitsuUserAvatar( - @JsonProperty("original") @SerialName("original") val original: String?, + @JsonProperty("original") val original: String? ) - @Serializable data class KitsuLinks( /* Pagination */ - @JsonProperty("first") @SerialName("first") val first: String?, - @JsonProperty("next") @SerialName("next") val next: String?, - @JsonProperty("last") @SerialName("last") val last: String?, + @JsonProperty("first") val first: String?, + @JsonProperty("next") val next: String?, + @JsonProperty("last") val last: String?, /* Relationships */ - @JsonProperty("related") @SerialName("related") val related: String?, + @JsonProperty("related") val related: String? ) - @Serializable data class KitsuResponse( - @JsonProperty("links") @SerialName("links") val links: KitsuLinks?, - @JsonProperty("data") @SerialName("data") val data: List, + @JsonProperty("links") val links: KitsuLinks?, + @JsonProperty("data") val data: List, /* When requesting related info (User library entry -> anime) */ - @JsonProperty("included") @SerialName("included") val included: List?, + @JsonProperty("included") val included: List?, ) + companion object { + const val KITSU_CACHED_LIST: String = "kitsu_cached_list" private fun parseDateLong(string: String?): Long? { return try { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).parse( + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse( string ?: return null )?.time?.div(1000) - } catch (_: Exception) { + } catch (e: Exception) { null } } @@ -622,13 +644,13 @@ class KitsuApi: SyncAPI() { arrayOf("current", "completed", "on_hold", "dropped", "planned") private fun fromIntToAnimeStatus(inp: SyncWatchType): KitsuStatusType { return when (inp) { - SyncWatchType.NONE -> KitsuStatusType.None - SyncWatchType.WATCHING -> KitsuStatusType.Watching - SyncWatchType.COMPLETED -> KitsuStatusType.Completed - SyncWatchType.ONHOLD -> KitsuStatusType.OnHold - SyncWatchType.DROPPED -> KitsuStatusType.Dropped - SyncWatchType.PLANTOWATCH -> KitsuStatusType.PlanToWatch - SyncWatchType.REWATCHING -> KitsuStatusType.Watching + SyncWatchType.NONE -> KitsuStatusType.None + SyncWatchType.WATCHING -> KitsuStatusType.Watching + SyncWatchType.COMPLETED -> KitsuStatusType.Completed + SyncWatchType.ONHOLD -> KitsuStatusType.OnHold + SyncWatchType.DROPPED -> KitsuStatusType.Dropped + SyncWatchType.PLANTOWATCH -> KitsuStatusType.PlanToWatch + SyncWatchType.REWATCHING -> KitsuStatusType.Watching } } @@ -643,12 +665,12 @@ class KitsuApi: SyncAPI() { private fun convertToStatus(string: String): KitsuStatusType { return when (string) { - "current" -> KitsuStatusType.Watching - "completed" -> KitsuStatusType.Completed - "on_hold" -> KitsuStatusType.OnHold - "dropped" -> KitsuStatusType.Dropped - "planned" -> KitsuStatusType.PlanToWatch - else -> KitsuStatusType.None + "current" -> KitsuStatusType.Watching + "completed" -> KitsuStatusType.Completed + "on_hold" -> KitsuStatusType.OnHold + "dropped" -> KitsuStatusType.Dropped + "planned" -> KitsuStatusType.PlanToWatch + else -> KitsuStatusType.None } } } @@ -670,7 +692,7 @@ object Kitsu { "https://kitsu.io/api/graphql", headers = headers, data = mapOf("query" to query) - ).parsed() + ).parsed() } private val cache: MutableMap, Map> = @@ -752,52 +774,44 @@ query { return map } - @Serializable data class KitsuResponse( - @JsonProperty("data") @SerialName("data") val data: Data? = null, + val data: Data? = null ) { - @Serializable data class Data( - @JsonProperty("lookupMapping") @SerialName("lookupMapping") val lookupMapping: LookupMapping? = null, + val lookupMapping: LookupMapping? = null ) - @Serializable data class LookupMapping( - @JsonProperty("id") @SerialName("id") val id: String? = null, - @JsonProperty("episodes") @SerialName("episodes") val episodes: Episodes? = null, + val id: String? = null, + val episodes: Episodes? = null ) - @Serializable data class Episodes( - @JsonProperty("nodes") @SerialName("nodes") val nodes: List? = null, + val nodes: List? = null ) - @Serializable data class Node( - @JsonProperty("number") @SerialName("number") val num: Int? = null, - @JsonProperty("titles") @SerialName("titles") val titles: Titles? = null, - @JsonProperty("description") @SerialName("description") val description: Description? = null, - @JsonProperty("thumbnail") @SerialName("thumbnail") val thumbnail: Thumbnail? = null, + @JsonProperty("number") + val num: Int? = null, + val titles: Titles? = null, + val description: Description? = null, + val thumbnail: Thumbnail? = null ) - @Serializable data class Description( - @JsonProperty("en") @SerialName("en") val en: String? = null, + val en: String? = null ) - @Serializable data class Thumbnail( - @JsonProperty("original") @SerialName("original") val original: Original? = null, + val original: Original? = null ) - @Serializable data class Original( - @JsonProperty("url") @SerialName("url") val url: String? = null, + val url: String? = null ) - @Serializable data class Titles( - @JsonProperty("canonical") @SerialName("canonical") val canonical: String? = null, + val canonical: String? = null ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index f74102b13..ba0195be6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -2,8 +2,6 @@ package com.lagradost.cloudstream3.syncproviders.providers import androidx.annotation.StringRes import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey import com.lagradost.cloudstream3.R @@ -21,9 +19,8 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import com.lagradost.cloudstream3.utils.txt -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.text.SimpleDateFormat import java.time.Instant import java.time.format.DateTimeFormatter @@ -37,7 +34,7 @@ class MALApi : SyncAPI() { override var name = "MAL" override val idPrefix = "mal" - private val key = BuildConfig.MAL_KEY + val key = "1714d6f2f4f7cc19644384f8c4629910" private val apiUrl = "https://api.myanimelist.net" override val hasOAuth2 = true override val redirectUrlIdentifier: String? = "mallogin" @@ -52,17 +49,16 @@ class MALApi : SyncAPI() { SyncWatchType.PLANTOWATCH, SyncWatchType.DROPPED, SyncWatchType.ONHOLD, - SyncWatchType.NONE, + SyncWatchType.NONE ) - @Serializable - data class Payload( - @JsonProperty("requestId") @SerialName("requestId") val requestId: Int, - @JsonProperty("codeVerifier") @SerialName("codeVerifier") val codeVerifier: String, + data class PayLoad( + val requestId: Int, + val codeVerifier: String ) override suspend fun login(redirectUrl: String, payload: String?): AuthToken? { - val payloadData = parseJson(payload!!) + val payloadData = parseJson(payload!!) val sanitizer = splitRedirectUrl(redirectUrl) val state = sanitizer["state"]!! @@ -78,13 +74,13 @@ class MALApi : SyncAPI() { "client_id" to key, "code" to currentCode, "code_verifier" to payloadData.codeVerifier, - "grant_type" to "authorization_code", + "grant_type" to "authorization_code" ) ).parsed() return AuthToken( - accessTokenLifetime = APIHolder.unixTime + token.expiresIn.toLong(), + accessTokenLifetime = unixTime + token.expiresIn.toLong(), refreshToken = token.refreshToken, - accessToken = token.accessToken, + accessToken = token.accessToken ) } @@ -98,13 +94,13 @@ class MALApi : SyncAPI() { return AuthUser( id = user.id, name = user.name, - profilePicture = user.picture, + profilePicture = user.picture ) } - override suspend fun search(auth: AuthData?, query: String): List? { + override suspend fun search(auth : AuthData?, query: String): List? { val auth = auth?.token?.accessToken ?: return null - val url = "$apiUrl/v2/anime?q=$query&limit=$MAL_MAX_SEARCH_LIMIT" + val url = "$apiUrl/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT" val res = app.get( url, headers = mapOf( "Authorization" to "Bearer $auth", @@ -117,7 +113,7 @@ class MALApi : SyncAPI() { this.name, node.id.toString(), "$mainUrl/anime/${node.id}/", - node.mainPicture?.large ?: node.mainPicture?.medium, + node.mainPicture?.large ?: node.mainPicture?.medium ) } } @@ -126,7 +122,7 @@ class MALApi : SyncAPI() { Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first() override suspend fun updateStatus( - auth: AuthData?, + auth : AuthData?, id: String, newStatus: SyncAPI.AbstractSyncStatus ): Boolean { @@ -139,89 +135,82 @@ class MALApi : SyncAPI() { ) } - @Serializable data class MalAnime( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("main_picture") @SerialName("main_picture") val mainPicture: MainPicture?, - @JsonProperty("alternative_titles") @SerialName("alternative_titles") val alternativeTitles: AlternativeTitles?, - @JsonProperty("start_date") @SerialName("start_date") val startDate: String?, - @JsonProperty("end_date") @SerialName("end_date") val endDate: String?, - @JsonProperty("synopsis") @SerialName("synopsis") val synopsis: String?, - @JsonProperty("mean") @SerialName("mean") val mean: Double?, - @JsonProperty("rank") @SerialName("rank") val rank: Int?, - @JsonProperty("popularity") @SerialName("popularity") val popularity: Int?, - @JsonProperty("num_list_users") @SerialName("num_list_users") val numListUsers: Int?, - @JsonProperty("num_scoring_users") @SerialName("num_scoring_users") val numScoringUsers: Int?, - @JsonProperty("nsfw") @SerialName("nsfw") val nsfw: String?, - @JsonProperty("created_at") @SerialName("created_at") val createdAt: String?, - @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String?, - @JsonProperty("media_type") @SerialName("media_type") val mediaType: String?, - @JsonProperty("status") @SerialName("status") val status: String?, - @JsonProperty("genres") @SerialName("genres") val genres: ArrayList?, - @JsonProperty("my_list_status") @SerialName("my_list_status") val myListStatus: MyListStatus?, - @JsonProperty("num_episodes") @SerialName("num_episodes") val numEpisodes: Int?, - @JsonProperty("start_season") @SerialName("start_season") val startSeason: StartSeason?, - @JsonProperty("broadcast") @SerialName("broadcast") val broadcast: Broadcast?, - @JsonProperty("source") @SerialName("source") val source: String?, - @JsonProperty("average_episode_duration") @SerialName("average_episode_duration") val averageEpisodeDuration: Int?, - @JsonProperty("rating") @SerialName("rating") val rating: String?, - @JsonProperty("pictures") @SerialName("pictures") val pictures: ArrayList?, - @JsonProperty("background") @SerialName("background") val background: String?, - @JsonProperty("related_anime") @SerialName("related_anime") val relatedAnime: ArrayList?, - @JsonProperty("related_manga") @SerialName("related_manga") val relatedManga: ArrayList?, - @JsonProperty("recommendations") @SerialName("recommendations") val recommendations: ArrayList?, - @JsonProperty("studios") @SerialName("studios") val studios: ArrayList?, - @JsonProperty("statistics") @SerialName("statistics") val statistics: Statistics?, + @JsonProperty("id") val id: Int?, + @JsonProperty("title") val title: String?, + @JsonProperty("main_picture") val mainPicture: MainPicture?, + @JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?, + @JsonProperty("start_date") val startDate: String?, + @JsonProperty("end_date") val endDate: String?, + @JsonProperty("synopsis") val synopsis: String?, + @JsonProperty("mean") val mean: Double?, + @JsonProperty("rank") val rank: Int?, + @JsonProperty("popularity") val popularity: Int?, + @JsonProperty("num_list_users") val numListUsers: Int?, + @JsonProperty("num_scoring_users") val numScoringUsers: Int?, + @JsonProperty("nsfw") val nsfw: String?, + @JsonProperty("created_at") val createdAt: String?, + @JsonProperty("updated_at") val updatedAt: String?, + @JsonProperty("media_type") val mediaType: String?, + @JsonProperty("status") val status: String?, + @JsonProperty("genres") val genres: ArrayList?, + @JsonProperty("my_list_status") val myListStatus: MyListStatus?, + @JsonProperty("num_episodes") val numEpisodes: Int?, + @JsonProperty("start_season") val startSeason: StartSeason?, + @JsonProperty("broadcast") val broadcast: Broadcast?, + @JsonProperty("source") val source: String?, + @JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?, + @JsonProperty("rating") val rating: String?, + @JsonProperty("pictures") val pictures: ArrayList?, + @JsonProperty("background") val background: String?, + @JsonProperty("related_anime") val relatedAnime: ArrayList?, + @JsonProperty("related_manga") val relatedManga: ArrayList?, + @JsonProperty("recommendations") val recommendations: ArrayList?, + @JsonProperty("studios") val studios: ArrayList?, + @JsonProperty("statistics") val statistics: Statistics?, ) - @Serializable data class Recommendations( - @JsonProperty("node") @SerialName("node") val node: Node? = null, - @JsonProperty("num_recommendations") @SerialName("num_recommendations") val numRecommendations: Int? = null, + @JsonProperty("node") val node: Node? = null, + @JsonProperty("num_recommendations") val numRecommendations: Int? = null ) - @Serializable data class Studios( - @JsonProperty("id") @SerialName("id") val id: Int? = null, - @JsonProperty("name") @SerialName("name") val name: String? = null, + @JsonProperty("id") val id: Int? = null, + @JsonProperty("name") val name: String? = null ) - @Serializable data class MyListStatus( - @JsonProperty("status") @SerialName("status") val status: String? = null, - @JsonProperty("score") @SerialName("score") val score: Int? = null, - @JsonProperty("num_episodes_watched") @SerialName("num_episodes_watched") val numEpisodesWatched: Int? = null, - @JsonProperty("is_rewatching") @SerialName("is_rewatching") val isRewatching: Boolean? = null, - @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String? = null, + @JsonProperty("status") val status: String? = null, + @JsonProperty("score") val score: Int? = null, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int? = null, + @JsonProperty("is_rewatching") val isRewatching: Boolean? = null, + @JsonProperty("updated_at") val updatedAt: String? = null ) - @Serializable data class RelatedAnime( - @JsonProperty("node") @SerialName("node") val node: Node? = null, - @JsonProperty("relation_type") @SerialName("relation_type") val relationType: String? = null, - @JsonProperty("relation_type_formatted") @SerialName("relation_type_formatted") val relationTypeFormatted: String? = null, + @JsonProperty("node") val node: Node? = null, + @JsonProperty("relation_type") val relationType: String? = null, + @JsonProperty("relation_type_formatted") val relationTypeFormatted: String? = null ) - @Serializable data class Status( - @JsonProperty("watching") @SerialName("watching") val watching: String? = null, - @JsonProperty("completed") @SerialName("completed") val completed: String? = null, - @JsonProperty("on_hold") @SerialName("on_hold") val onHold: String? = null, - @JsonProperty("dropped") @SerialName("dropped") val dropped: String? = null, - @JsonProperty("plan_to_watch") @SerialName("plan_to_watch") val planToWatch: String? = null, + @JsonProperty("watching") val watching: String? = null, + @JsonProperty("completed") val completed: String? = null, + @JsonProperty("on_hold") val onHold: String? = null, + @JsonProperty("dropped") val dropped: String? = null, + @JsonProperty("plan_to_watch") val planToWatch: String? = null ) - @Serializable data class Statistics( - @JsonProperty("status") @SerialName("status") val status: Status? = null, - @JsonProperty("num_list_users") @SerialName("num_list_users") val numListUsers: Int? = null, + @JsonProperty("status") val status: Status? = null, + @JsonProperty("num_list_users") val numListUsers: Int? = null ) private fun parseDate(string: String?): Long? { return try { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(string ?: return null)?.time - } catch (_: Exception) { + } catch (e: Exception) { null } } @@ -232,11 +221,11 @@ class MALApi : SyncAPI() { apiName = this.name, syncId = node.id.toString(), url = "$mainUrl/anime/${node.id}", - posterUrl = node.mainPicture?.large, + posterUrl = node.mainPicture?.large ) } - override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? { + override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? { val auth = auth?.token?.accessToken ?: return null val internalId = id.toIntOrNull() ?: return null val url = @@ -258,7 +247,7 @@ class MALApi : SyncAPI() { airStatus = when (malAnime.status) { "finished_airing" -> ShowStatus.Completed "currently_airing" -> ShowStatus.Ongoing - // "not_yet_aired" + //"not_yet_aired" else -> null }, nextAiring = null, @@ -282,7 +271,7 @@ class MALApi : SyncAPI() { } } - override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? { + override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? { val auth = auth?.token?.accessToken ?: return null // https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get @@ -345,7 +334,7 @@ class MALApi : SyncAPI() { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse( string ?: return null )?.time?.div(1000) - } catch (_: Exception) { + } catch (e: Exception) { null } } @@ -355,10 +344,12 @@ class MALApi : SyncAPI() { val codeVerifier = generateCodeVerifier() val requestId = ++requestIdCounter val codeChallenge = codeVerifier - val request = "$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId" + val request = + "$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId" + return AuthLoginPage( url = request, - payload = Payload(requestId, codeVerifier).toJson(), + payload = PayLoad(requestId, codeVerifier).toJson() ) } @@ -368,71 +359,69 @@ class MALApi : SyncAPI() { data = mapOf( "client_id" to key, "grant_type" to "refresh_token", - "refresh_token" to token.refreshToken!!, + "refresh_token" to token.refreshToken!! ) ).parsed() + return AuthToken( accessToken = res.accessToken, refreshToken = res.refreshToken, - accessTokenLifetime = APIHolder.unixTime + res.expiresIn.toLong(), + accessTokenLifetime = unixTime + res.expiresIn.toLong() ) } private var requestIdCounter = 0 + + private val allTitles = hashMapOf() - @Serializable data class MalList( - @JsonProperty("data") @SerialName("data") val data: List, - @JsonProperty("paging") @SerialName("paging") val paging: Paging, + @JsonProperty("data") val data: List, + @JsonProperty("paging") val paging: Paging ) - @Serializable data class MainPicture( - @JsonProperty("medium") @SerialName("medium") val medium: String, - @JsonProperty("large") @SerialName("large") val large: String, + @JsonProperty("medium") val medium: String, + @JsonProperty("large") val large: String ) - @Serializable data class Node( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("title") @SerialName("title") val title: String, - @JsonProperty("main_picture") @SerialName("main_picture") val mainPicture: MainPicture?, - @JsonProperty("alternative_titles") @SerialName("alternative_titles") val alternativeTitles: AlternativeTitles?, - @JsonProperty("media_type") @SerialName("media_type") val mediaType: String?, - @JsonProperty("num_episodes") @SerialName("num_episodes") val numEpisodes: Int?, - @JsonProperty("status") @SerialName("status") val status: String?, - @JsonProperty("start_date") @SerialName("start_date") val startDate: String?, - @JsonProperty("end_date") @SerialName("end_date") val endDate: String?, - @JsonProperty("average_episode_duration") @SerialName("average_episode_duration") val averageEpisodeDuration: Int?, - @JsonProperty("synopsis") @SerialName("synopsis") val synopsis: String?, - @JsonProperty("mean") @SerialName("mean") val mean: Double?, - @JsonProperty("genres") @SerialName("genres") val genres: List?, - @JsonProperty("rank") @SerialName("rank") val rank: Int?, - @JsonProperty("popularity") @SerialName("popularity") val popularity: Int?, - @JsonProperty("num_list_users") @SerialName("num_list_users") val numListUsers: Int?, - @JsonProperty("num_favorites") @SerialName("num_favorites") val numFavorites: Int?, - @JsonProperty("num_scoring_users") @SerialName("num_scoring_users") val numScoringUsers: Int?, - @JsonProperty("start_season") @SerialName("start_season") val startSeason: StartSeason?, - @JsonProperty("broadcast") @SerialName("broadcast") val broadcast: Broadcast?, - @JsonProperty("nsfw") @SerialName("nsfw") val nsfw: String?, - @JsonProperty("created_at") @SerialName("created_at") val createdAt: String?, - @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String?, + @JsonProperty("id") val id: Int, + @JsonProperty("title") val title: String, + @JsonProperty("main_picture") val mainPicture: MainPicture?, + @JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?, + @JsonProperty("media_type") val mediaType: String?, + @JsonProperty("num_episodes") val numEpisodes: Int?, + @JsonProperty("status") val status: String?, + @JsonProperty("start_date") val startDate: String?, + @JsonProperty("end_date") val endDate: String?, + @JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?, + @JsonProperty("synopsis") val synopsis: String?, + @JsonProperty("mean") val mean: Double?, + @JsonProperty("genres") val genres: List?, + @JsonProperty("rank") val rank: Int?, + @JsonProperty("popularity") val popularity: Int?, + @JsonProperty("num_list_users") val numListUsers: Int?, + @JsonProperty("num_favorites") val numFavorites: Int?, + @JsonProperty("num_scoring_users") val numScoringUsers: Int?, + @JsonProperty("start_season") val startSeason: StartSeason?, + @JsonProperty("broadcast") val broadcast: Broadcast?, + @JsonProperty("nsfw") val nsfw: String?, + @JsonProperty("created_at") val createdAt: String?, + @JsonProperty("updated_at") val updatedAt: String? ) - @Serializable data class ListStatus( - @JsonProperty("status") @SerialName("status") val status: String?, - @JsonProperty("score") @SerialName("score") val score: Int, - @JsonProperty("num_episodes_watched") @SerialName("num_episodes_watched") val numEpisodesWatched: Int, - @JsonProperty("is_rewatching") @SerialName("is_rewatching") val isRewatching: Boolean, - @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String, + @JsonProperty("status") val status: String?, + @JsonProperty("score") val score: Int, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int, + @JsonProperty("is_rewatching") val isRewatching: Boolean, + @JsonProperty("updated_at") val updatedAt: String, ) - @Serializable data class Data( - @JsonProperty("node") @SerialName("node") val node: Node, - @JsonProperty("list_status") @SerialName("list_status") val listStatus: ListStatus?, + @JsonProperty("node") val node: Node, + @JsonProperty("list_status") val listStatus: ListStatus?, ) { fun toLibraryItem(): SyncAPI.LibraryItem { return SyncAPI.LibraryItem( @@ -463,37 +452,32 @@ class MALApi : SyncAPI() { } } - @Serializable data class Paging( - @JsonProperty("next") @SerialName("next") val next: String?, + @JsonProperty("next") val next: String? ) - @Serializable data class AlternativeTitles( - @JsonProperty("synonyms") @SerialName("synonyms") val synonyms: List, - @JsonProperty("en") @SerialName("en") val en: String, - @JsonProperty("ja") @SerialName("ja") val ja: String, + @JsonProperty("synonyms") val synonyms: List, + @JsonProperty("en") val en: String, + @JsonProperty("ja") val ja: String ) - @Serializable data class Genres( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("name") @SerialName("name") val name: String, + @JsonProperty("id") val id: Int, + @JsonProperty("name") val name: String ) - @Serializable data class StartSeason( - @JsonProperty("year") @SerialName("year") val year: Int, - @JsonProperty("season") @SerialName("season") val season: String, + @JsonProperty("year") val year: Int, + @JsonProperty("season") val season: String ) - @Serializable data class Broadcast( - @JsonProperty("day_of_the_week") @SerialName("day_of_the_week") val dayOfTheWeek: String?, - @JsonProperty("start_time") @SerialName("start_time") val startTime: String?, + @JsonProperty("day_of_the_week") val dayOfTheWeek: String?, + @JsonProperty("start_time") val startTime: String? ) - override suspend fun library(auth: AuthData?): LibraryMetadata? { + override suspend fun library(auth : AuthData?): LibraryMetadata? { val list = getMalAnimeListSmart(auth ?: return null)?.groupBy { convertToStatus(it.listStatus?.status ?: "").stringRes }?.mapValues { group -> @@ -521,7 +505,7 @@ class MALApi : SyncAPI() { ) } - private suspend fun getMalAnimeListSmart(auth: AuthData): Array? { + private suspend fun getMalAnimeListSmart(auth : AuthData): Array? { return if (requireLibraryRefresh) { val list = getMalAnimeList(auth.token) setKey(MAL_CACHED_LIST, auth.user.id.toString(), list) @@ -536,7 +520,7 @@ class MALApi : SyncAPI() { val fullList = mutableListOf() val offsetRegex = Regex("""offset=(\d+)""") while (true) { - val data: MalList = getMalAnimeListSlice(token, offset) + val data: MalList = getMalAnimeListSlice(token, offset) ?: break fullList.addAll(data.data) offset = data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() } @@ -545,17 +529,18 @@ class MALApi : SyncAPI() { return fullList.toTypedArray() } - private suspend fun getMalAnimeListSlice(token: AuthToken, offset: Int = 0): MalList { + private suspend fun getMalAnimeListSlice(token: AuthToken, offset: Int = 0): MalList? { val user = "@me" // Very lackluster docs // https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get - val url = "$apiUrl/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset" + val url = + "$apiUrl/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset" val res = app.get( url, headers = mapOf( "Authorization" to "Bearer ${token.accessToken}", ), cacheTime = 0 ).text - return parseJson(res) + return res.toKotlinObject() } private suspend fun setScoreRequest( @@ -598,7 +583,7 @@ class MALApi : SyncAPI() { val data = mapOf( "status" to status, "score" to score?.toString(), - "num_watched_episodes" to numWatchedEpisodes?.toString(), + "num_watched_episodes" to numWatchedEpisodes?.toString() ).filterValues { it != null } as Map return app.put( @@ -610,74 +595,71 @@ class MALApi : SyncAPI() { ).text } - @Serializable + data class ResponseToken( - @JsonProperty("token_type") @SerialName("token_type") val tokenType: String, - @JsonProperty("expires_in") @SerialName("expires_in") val expiresIn: Int, - @JsonProperty("access_token") @SerialName("access_token") val accessToken: String, - @JsonProperty("refresh_token") @SerialName("refresh_token") val refreshToken: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("expires_in") val expiresIn: Int, + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("refresh_token") val refreshToken: String, ) - @Serializable data class MalRoot( - @JsonProperty("data") @SerialName("data") val data: List, + @JsonProperty("data") val data: List, ) - @Serializable data class MalDatum( - @JsonProperty("node") @SerialName("node") val node: MalNode, - @JsonProperty("list_status") @SerialName("list_status") val listStatus: MalStatus, + @JsonProperty("node") val node: MalNode, + @JsonProperty("list_status") val listStatus: MalStatus, ) - @Serializable data class MalNode( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("title") @SerialName("title") val title: String, + @JsonProperty("id") val id: Int, + @JsonProperty("title") val title: String, + /* + also, but not used + main_picture -> + public string medium; + public string large; + */ ) - @Serializable data class MalStatus( - @JsonProperty("status") @SerialName("status") val status: String, - @JsonProperty("score") @SerialName("score") val score: Int, - @JsonProperty("num_episodes_watched") @SerialName("num_episodes_watched") val numEpisodesWatched: Int, - @JsonProperty("is_rewatching") @SerialName("is_rewatching") val isRewatching: Boolean, - @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String, + @JsonProperty("status") val status: String, + @JsonProperty("score") val score: Int, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int, + @JsonProperty("is_rewatching") val isRewatching: Boolean, + @JsonProperty("updated_at") val updatedAt: String, ) - @Serializable data class MalUser( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("location") @SerialName("location") val location: String, - @JsonProperty("joined_at") @SerialName("joined_at") val joinedAt: String, - @JsonProperty("picture") @SerialName("picture") val picture: String?, + @JsonProperty("id") val id: Int, + @JsonProperty("name") val name: String, + @JsonProperty("location") val location: String, + @JsonProperty("joined_at") val joinedAt: String, + @JsonProperty("picture") val picture: String?, ) - @Serializable data class MalMainPicture( - @JsonProperty("large") @SerialName("large") val large: String?, - @JsonProperty("medium") @SerialName("medium") val medium: String?, + @JsonProperty("large") val large: String?, + @JsonProperty("medium") val medium: String?, ) // Used for getDataAboutId() - @Serializable data class SmallMalAnime( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("num_episodes") @SerialName("num_episodes") val numEpisodes: Int, - @JsonProperty("my_list_status") @SerialName("my_list_status") val myListStatus: MalStatus?, - @JsonProperty("main_picture") @SerialName("main_picture") val mainPicture: MalMainPicture?, + @JsonProperty("id") val id: Int, + @JsonProperty("title") val title: String?, + @JsonProperty("num_episodes") val numEpisodes: Int, + @JsonProperty("my_list_status") val myListStatus: MalStatus?, + @JsonProperty("main_picture") val mainPicture: MalMainPicture?, ) - @Serializable data class MalSearchNode( - @JsonProperty("node") @SerialName("node") val node: Node, + @JsonProperty("node") val node: Node, ) - @Serializable data class MalSearch( - @JsonProperty("data") @SerialName("data") val data: List, - // paging + @JsonProperty("data") val data: List, + //paging ) data class MalTitleHolder( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt index fca3e5df8..4b17fdb29 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -2,11 +2,9 @@ package com.lagradost.cloudstream3.syncproviders.providers import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.syncproviders.AuthData import com.lagradost.cloudstream3.syncproviders.AuthLoginRequirement @@ -20,8 +18,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToOpenSubtitlesTag -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable class OpenSubtitlesApi : SubtitleAPI() { override val name = "OpenSubtitles" @@ -47,17 +43,17 @@ class OpenSubtitlesApi : SubtitleAPI() { } private fun canDoRequest(): Boolean { - return unixTimeMS > currentCoolDown + return unixTimeMs > currentCoolDown } private fun throwIfCantDoRequest() { if (!canDoRequest()) { - throw ErrorLoadingException("Too many requests wait for ${(currentCoolDown - unixTimeMS) / 1000L}s") + throw ErrorLoadingException("Too many requests wait for ${(currentCoolDown - unixTimeMs) / 1000L}s") } } private fun throwGotTooManyRequests() { - currentCoolDown = unixTimeMS + COOLDOWN_DURATION + currentCoolDown = unixTimeMs + COOLDOWN_DURATION throw ErrorLoadingException("Too many requests") } @@ -93,7 +89,7 @@ class OpenSubtitlesApi : SubtitleAPI() { accessToken = response.token ?: throw ErrorLoadingException("Invalid password or username"), /// JWT token is valid 24 hours after successfully authentication of user - accessTokenLifetime = APIHolder.unixTime + 60 * 60 * 24, + accessTokenLifetime = unixTime + 60 * 60 * 24, payload = form.toJson() ) } @@ -101,7 +97,7 @@ class OpenSubtitlesApi : SubtitleAPI() { /** * Fetch subtitles using token authenticated on previous method (see authorize). * Returns list of Subtitles which user can select to download (see load). - */ + * */ override suspend fun search( auth : AuthData?, query: AbstractSubtitleEntities.SubtitleSearch @@ -181,15 +177,16 @@ class OpenSubtitlesApi : SubtitleAPI() { return results } - /** - * Process data returned from search. - * Returns string url for the subtitle file. - */ + /* + Process data returned from search. + Returns string url for the subtitle file. + */ + override suspend fun load( auth : AuthData?, subtitle: AbstractSubtitleEntities.SubtitleEntity ): String? { - if (auth == null) return null + if(auth == null) return null throwIfCantDoRequest() val req = app.post( @@ -221,64 +218,57 @@ class OpenSubtitlesApi : SubtitleAPI() { return null } - @Serializable data class OAuthToken( - @JsonProperty("token") @SerialName("token") var token: String? = null, - @JsonProperty("status") @SerialName("status") var status: Int? = null, + @JsonProperty("token") var token: String? = null, + @JsonProperty("status") var status: Int? = null ) - @Serializable data class Results( - @JsonProperty("data") @SerialName("data") var data: List? = listOf(), + @JsonProperty("data") var data: List? = listOf() ) - @Serializable data class ResultData( - @JsonProperty("id") @SerialName("id") var id: String? = null, - @JsonProperty("type") @SerialName("type") var type: String? = null, - @JsonProperty("attributes") @SerialName("attributes") var attributes: ResultAttributes? = ResultAttributes(), + @JsonProperty("id") var id: String? = null, + @JsonProperty("type") var type: String? = null, + @JsonProperty("attributes") var attributes: ResultAttributes? = ResultAttributes() ) - @Serializable data class ResultAttributes( - @JsonProperty("subtitle_id") @SerialName("subtitle_id") var subtitleId: String? = null, - @JsonProperty("language") @SerialName("language") var language: String? = null, - @JsonProperty("release") @SerialName("release") var release: String? = null, - @JsonProperty("url") @SerialName("url") var url: String? = null, - @JsonProperty("files") @SerialName("files") var files: List? = listOf(), - @JsonProperty("feature_details") @SerialName("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(), - @JsonProperty("hearing_impaired") @SerialName("hearing_impaired") var hearingImpaired: Boolean? = null, + @JsonProperty("subtitle_id") var subtitleId: String? = null, + @JsonProperty("language") var language: String? = null, + @JsonProperty("release") var release: String? = null, + @JsonProperty("url") var url: String? = null, + @JsonProperty("files") var files: List? = listOf(), + @JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(), + @JsonProperty("hearing_impaired") var hearingImpaired: Boolean? = null, ) - @Serializable data class ResultFiles( - @JsonProperty("file_id") @SerialName("file_id") var fileId: Int? = null, - @JsonProperty("file_name") @SerialName("file_name") var fileName: String? = null, + @JsonProperty("file_id") var fileId: Int? = null, + @JsonProperty("file_name") var fileName: String? = null ) - @Serializable data class ResultDownloadLink( - @JsonProperty("link") @SerialName("link") var link: String? = null, - @JsonProperty("file_name") @SerialName("file_name") var fileName: String? = null, - @JsonProperty("requests") @SerialName("requests") var requests: Int? = null, - @JsonProperty("remaining") @SerialName("remaining") var remaining: Int? = null, - @JsonProperty("message") @SerialName("message") var message: String? = null, - @JsonProperty("reset_time") @SerialName("reset_time") var resetTime: String? = null, - @JsonProperty("reset_time_utc") @SerialName("reset_time_utc") var resetTimeUtc: String? = null, + @JsonProperty("link") var link: String? = null, + @JsonProperty("file_name") var fileName: String? = null, + @JsonProperty("requests") var requests: Int? = null, + @JsonProperty("remaining") var remaining: Int? = null, + @JsonProperty("message") var message: String? = null, + @JsonProperty("reset_time") var resetTime: String? = null, + @JsonProperty("reset_time_utc") var resetTimeUtc: String? = null ) - @Serializable data class ResultFeatureDetails( - @JsonProperty("year") @SerialName("year") var year: Int? = null, - @JsonProperty("title") @SerialName("title") var title: String? = null, - @JsonProperty("movie_name") @SerialName("movie_name") var movieName: String? = null, - @JsonProperty("imdb_id") @SerialName("imdb_id") var imdbId: Int? = null, - @JsonProperty("tmdb_id") @SerialName("tmdb_id") var tmdbId: Int? = null, - @JsonProperty("season_number") @SerialName("season_number") var seasonNumber: Int? = null, - @JsonProperty("episode_number") @SerialName("episode_number") var episodeNumber: Int? = null, - @JsonProperty("parent_imdb_id") @SerialName("parent_imdb_id") var parentImdbId: Int? = null, - @JsonProperty("parent_title") @SerialName("parent_title") var parentTitle: String? = null, - @JsonProperty("parent_tmdb_id") @SerialName("parent_tmdb_id") var parentTmdbId: Int? = null, - @JsonProperty("parent_feature_id") @SerialName("parent_feature_id") var parentFeatureId: Int? = null, + @JsonProperty("year") var year: Int? = null, + @JsonProperty("title") var title: String? = null, + @JsonProperty("movie_name") var movieName: String? = null, + @JsonProperty("imdb_id") var imdbId: Int? = null, + @JsonProperty("tmdb_id") var tmdbId: Int? = null, + @JsonProperty("season_number") var seasonNumber: Int? = null, + @JsonProperty("episode_number") var episodeNumber: Int? = null, + @JsonProperty("parent_imdb_id") var parentImdbId: Int? = null, + @JsonProperty("parent_title") var parentTitle: String? = null, + @JsonProperty("parent_tmdb_id") var parentTmdbId: Int? = null, + @JsonProperty("parent_feature_id") var parentFeatureId: Int? = null ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index a57ec6716..c4095e2d8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -2,11 +2,10 @@ package com.lagradost.cloudstream3.syncproviders.providers import androidx.annotation.StringRes import androidx.core.net.toUri -import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.CloudStreamApp import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKeys import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey @@ -17,6 +16,7 @@ import com.lagradost.cloudstream3.Score import com.lagradost.cloudstream3.SimklSyncServices import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mapper import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING @@ -30,14 +30,8 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear -import com.lagradost.cloudstream3.utils.serializers.NonEmptySerializer import com.lagradost.cloudstream3.utils.txt -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KeepGeneratedSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.math.BigInteger import java.security.SecureRandom import java.text.SimpleDateFormat @@ -50,18 +44,22 @@ import kotlin.time.DurationUnit import kotlin.time.toDuration class SimklApi : SyncAPI() { - override val name = "Simkl" + override var name = "Simkl" override val idPrefix = "simkl" + val key = "simkl-key" override val redirectUrlIdentifier = "simkl" override val hasOAuth2 = true override val hasPin = true override var requireLibraryRefresh = true - override val mainUrl = "https://api.simkl.com" + override var mainUrl = "https://api.simkl.com" override val icon = R.drawable.simkl_logo override val createAccountUrl = "$mainUrl/signup" override val syncIdName = SyncIdName.Simkl + /** Automatically adds simkl auth headers */ + // private val interceptor = HeaderInterceptor() + /** * This is required to override the reported last activity as simkl activites * may not always update based on testing. @@ -70,99 +68,68 @@ class SimklApi : SyncAPI() { private object SimklCache { private const val SIMKL_CACHE_KEY = "SIMKL_API_CACHE" + enum class CacheTimes(val value: String) { OneMonth("30d"), ThirtyMinutes("30m") } - @Serializable - private data class MediaObjectCacheEntry( - @JsonProperty("obj") @SerialName("obj") val obj: MediaObject?, - @JsonProperty("validUntil") @SerialName("validUntil") val validUntil: Long, - @JsonProperty("cacheTime") @SerialName("cacheTime") val cacheTime: Long = APIHolder.unixTime, - ) + private class SimklCacheWrapper( + @JsonProperty("obj") val obj: T?, + @JsonProperty("validUntil") val validUntil: Long, + @JsonProperty("cacheTime") val cacheTime: Long = unixTime, + ) { + /** Returns true if cache is newer than cacheDays */ + fun isFresh(): Boolean { + return validUntil > unixTime + } - @Serializable - private data class EpisodesCacheEntry( - @JsonProperty("obj") @SerialName("obj") val obj: Array?, - @JsonProperty("validUntil") @SerialName("validUntil") val validUntil: Long, - @JsonProperty("cacheTime") @SerialName("cacheTime") val cacheTime: Long = APIHolder.unixTime, - ) - - /** - * Minimal class used only to peek at an entry's expiry, without caring which of the - * concrete entry types above actually produced it. - */ - @Serializable - private data class CacheFreshness( - @JsonProperty("validUntil") @SerialName("validUntil") val validUntil: Long, - ) - - private fun Long.isFresh(): Boolean = this > APIHolder.unixTime - - private fun Long.remaining(): Duration { - val unixTime = APIHolder.unixTime - return if (this > unixTime) { - (this - unixTime).toDuration(DurationUnit.SECONDS) - } else { - Duration.ZERO + fun remainingTime(): Duration { + val unixTime = unixTime + return if (validUntil > unixTime) { + (validUntil - unixTime).toDuration(DurationUnit.SECONDS) + } else { + Duration.ZERO + } } } fun cleanOldCache() { getKeys(SIMKL_CACHE_KEY)?.forEach { - val isOld = getKey(it)?.validUntil?.isFresh() == false - if (isOld) removeKey(it) - } - } - - fun setMediaObject(path: String, value: MediaObject, cacheTime: Duration) { - debugPrint { "Set cache: $SIMKL_CACHE_KEY/$path for ${cacheTime.inWholeDays} days or ${cacheTime.inWholeSeconds} seconds." } - setKey( - SIMKL_CACHE_KEY, - path, - MediaObjectCacheEntry(value, APIHolder.unixTime + cacheTime.inWholeSeconds).toJson(), - ) - } - - /** Gets the cached [MediaObject], if it's not fresh returns null and removes it from cache */ - fun getMediaObject(path: String): MediaObject? { - val cache = getKey(SIMKL_CACHE_KEY, path)?.let { - tryParseJson(it) - } - - return if (cache?.validUntil?.isFresh() == true) { - debugPrint { - "Cache hit at: $SIMKL_CACHE_KEY/$path. " + - "Remains fresh for ${cache.validUntil.remaining().inWholeDays} days or ${cache.validUntil.remaining().inWholeSeconds} seconds." + val isOld = CloudStreamApp.getKey>(it)?.isFresh() == false + if (isOld) { + removeKey(it) } - cache.obj - } else { - debugPrint { "Cache miss at: $SIMKL_CACHE_KEY/$path" } - removeKey(SIMKL_CACHE_KEY, path) - null } } - fun setEpisodes(path: String, value: Array, cacheTime: Duration) { + fun setKey(path: String, value: T, cacheTime: Duration) { debugPrint { "Set cache: $SIMKL_CACHE_KEY/$path for ${cacheTime.inWholeDays} days or ${cacheTime.inWholeSeconds} seconds." } setKey( SIMKL_CACHE_KEY, path, - EpisodesCacheEntry(value, APIHolder.unixTime + cacheTime.inWholeSeconds).toJson(), + // Storing as plain sting is required to make generics work. + SimklCacheWrapper(value, unixTime + cacheTime.inWholeSeconds).toJson() ) } - /** Gets the cached episode list, if it's not fresh returns null and removes it from cache */ - fun getEpisodes(path: String): Array? { + /** + * Gets cached object, if object is not fresh returns null and removes it from cache + */ + inline fun getKey(path: String): T? { + // Required for generic otherwise "LinkedHashMap cannot be cast to MediaObject" + val type = mapper.typeFactory.constructParametricType( + SimklCacheWrapper::class.java, + T::class.java + ) val cache = getKey(SIMKL_CACHE_KEY, path)?.let { - tryParseJson(it) + mapper.readValue>(it, type) } - return if (cache?.validUntil?.isFresh() == true) { + return if (cache?.isFresh() == true) { debugPrint { "Cache hit at: $SIMKL_CACHE_KEY/$path. " + - "Remains fresh for ${cache.validUntil.remaining().inWholeDays} days or ${cache.validUntil.remaining().inWholeSeconds} seconds." + "Remains fresh for ${cache.remainingTime().inWholeDays} days or ${cache.remainingTime().inWholeSeconds} seconds." } cache.obj } else { @@ -206,7 +173,7 @@ class SimklApi : SyncAPI() { ) ) ) - } catch (_: Exception) { + } catch (e: Exception) { null } } @@ -222,7 +189,7 @@ class SimklApi : SyncAPI() { enum class SimklListStatusType( var value: Int, @StringRes val stringRes: Int, - val originalName: String?, + val originalName: String? ) { Watching(0, R.string.type_watching, "watching"), Completed(1, R.string.type_completed, "completed"), @@ -241,92 +208,83 @@ class SimklApi : SyncAPI() { } } + // ------------------- @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = TokenRequest.Serializer::class) data class TokenRequest( - @JsonProperty("code") @SerialName("code") val code: String, - @JsonProperty("client_id") @SerialName("client_id") val clientId: String = CLIENT_ID, - @JsonProperty("client_secret") @SerialName("client_secret") val clientSecret: String = CLIENT_SECRET, - @JsonProperty("redirect_uri") @SerialName("redirect_uri") val redirectUri: String = "$APP_STRING://simkl", - @JsonProperty("grant_type") @SerialName("grant_type") val grantType: String = "authorization_code", - ) { - object Serializer : NonEmptySerializer(TokenRequest.generatedSerializer()) - } + @JsonProperty("code") val code: String, + @JsonProperty("client_id") val clientId: String = CLIENT_ID, + @JsonProperty("client_secret") val clientSecret: String = CLIENT_SECRET, + @JsonProperty("redirect_uri") val redirectUri: String = "$APP_STRING://simkl", + @JsonProperty("grant_type") val grantType: String = "authorization_code" + ) - @Serializable data class TokenResponse( /** No expiration date */ - @JsonProperty("access_token") @SerialName("access_token") val accessToken: String, - @JsonProperty("token_type") @SerialName("token_type") val tokenType: String, - @JsonProperty("scope") @SerialName("scope") val scope: String, + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("scope") val scope: String ) + // ------------------- /** https://simkl.docs.apiary.io/#reference/users/settings/receive-settings */ - @Serializable data class SettingsResponse( - @JsonProperty("user") @SerialName("user") val user: User, - @JsonProperty("account") @SerialName("account") val account: Account, + @JsonProperty("user") + val user: User, + @JsonProperty("account") + val account: Account, ) { - @Serializable data class User( - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("avatar") @SerialName("avatar") val avatar: String, // Url + @JsonProperty("name") + val name: String, + /** Url */ + @JsonProperty("avatar") + val avatar: String ) - @Serializable data class Account( - @JsonProperty("id") @SerialName("id") val id: Int, + @JsonProperty("id") + val id: Int, ) } - @Serializable data class PinAuthResponse( - @JsonProperty("result") @SerialName("result") val result: String, - @JsonProperty("device_code") @SerialName("device_code") val deviceCode: String, - @JsonProperty("user_code") @SerialName("user_code") val userCode: String, - @JsonProperty("verification_url") @SerialName("verification_url") val verificationUrl: String, - @JsonProperty("expires_in") @SerialName("expires_in") val expiresIn: Int, - @JsonProperty("interval") @SerialName("interval") val interval: Int, + @JsonProperty("result") val result: String, + @JsonProperty("device_code") val deviceCode: String, + @JsonProperty("user_code") val userCode: String, + @JsonProperty("verification_url") val verificationUrl: String, + @JsonProperty("expires_in") val expiresIn: Int, + @JsonProperty("interval") val interval: Int, ) - @Serializable data class PinExchangeResponse( - @JsonProperty("result") @SerialName("result") val result: String, - @JsonProperty("message") @SerialName("message") val message: String? = null, - @JsonProperty("access_token") @SerialName("access_token") val accessToken: String? = null, + @JsonProperty("result") val result: String, + @JsonProperty("message") val message: String? = null, + @JsonProperty("access_token") val accessToken: String? = null, ) - @Serializable + // ------------------- data class ActivitiesResponse( - @JsonProperty("all") @SerialName("all") val all: String?, - @JsonProperty("tv_shows") @SerialName("tv_shows") val tvShows: UpdatedAt, - @JsonProperty("anime") @SerialName("anime") val anime: UpdatedAt, - @JsonProperty("movies") @SerialName("movies") val movies: UpdatedAt, + @JsonProperty("all") val all: String?, + @JsonProperty("tv_shows") val tvShows: UpdatedAt, + @JsonProperty("anime") val anime: UpdatedAt, + @JsonProperty("movies") val movies: UpdatedAt, ) { - @Serializable data class UpdatedAt( - @JsonProperty("all") @SerialName("all") val all: String?, - @JsonProperty("removed_from_list") @SerialName("removed_from_list") val removedFromList: String?, - @JsonProperty("rated_at") @SerialName("rated_at") val ratedAt: String?, + @JsonProperty("all") val all: String?, + @JsonProperty("removed_from_list") val removedFromList: String?, + @JsonProperty("rated_at") val ratedAt: String?, ) } /** https://simkl.docs.apiary.io/#reference/tv/episodes/get-tv-show-episodes */ @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = EpisodeMetadata.Serializer::class) data class EpisodeMetadata( - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("description") @SerialName("description") val description: String?, - @JsonProperty("season") @SerialName("season") val season: Int?, - @JsonProperty("episode") @SerialName("episode") val episode: Int, - @JsonProperty("img") @SerialName("img") val img: String?, + @JsonProperty("title") val title: String?, + @JsonProperty("description") val description: String?, + @JsonProperty("season") val season: Int?, + @JsonProperty("episode") val episode: Int, + @JsonProperty("img") val img: String? ) { - object Serializer : NonEmptySerializer(EpisodeMetadata.generatedSerializer()) - companion object { fun convertToEpisodes(list: List?): List? { return list?.map { @@ -346,58 +304,40 @@ class SimklApi : SyncAPI() { /** * https://simkl.docs.apiary.io/#introduction/about-simkl-api/standard-media-objects - * Useful for finding shows from metadata. + * Useful for finding shows from metadata */ @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = MediaObject.Serializer::class) - data class MediaObject( - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("year") @SerialName("year") val year: Int?, - @JsonProperty("ids") @SerialName("ids") val ids: Ids?, - @JsonProperty("total_episodes") @SerialName("total_episodes") val totalEpisodes: Int? = null, - @JsonProperty("status") @SerialName("status") val status: String? = null, - @JsonProperty("poster") @SerialName("poster") val poster: String? = null, - @JsonProperty("type") @SerialName("type") val type: String? = null, - @JsonProperty("seasons") @SerialName("seasons") val seasons: List? = null, - @JsonProperty("episodes") @SerialName("episodes") val episodes: List? = null, + open class MediaObject( + @JsonProperty("title") val title: String?, + @JsonProperty("year") val year: Int?, + @JsonProperty("ids") val ids: Ids?, + @JsonProperty("total_episodes") val totalEpisodes: Int? = null, + @JsonProperty("status") val status: String? = null, + @JsonProperty("poster") val poster: String? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("seasons") val seasons: List? = null, + @JsonProperty("episodes") val episodes: List? = null ) { - object Serializer : NonEmptySerializer(MediaObject.generatedSerializer()) - fun hasEnded(): Boolean { return status == "released" || status == "ended" } @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = Season.Serializer::class) data class Season( - @JsonProperty("number") @SerialName("number") val number: Int, - @JsonProperty("episodes") @SerialName("episodes") val episodes: List, + @JsonProperty("number") val number: Int, + @JsonProperty("episodes") val episodes: List ) { - object Serializer : NonEmptySerializer(Season.generatedSerializer()) - - @Serializable - data class Episode( - @JsonProperty("number") @SerialName("number") val number: Int, - ) + data class Episode(@JsonProperty("number") val number: Int) } @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = Ids.Serializer::class) data class Ids( - @JsonProperty("simkl") @SerialName("simkl") val simkl: Int?, - @JsonProperty("imdb") @SerialName("imdb") val imdb: String? = null, - @JsonProperty("tmdb") @SerialName("tmdb") val tmdb: String? = null, - @JsonProperty("mal") @SerialName("mal") val mal: String? = null, - @JsonProperty("anilist") @SerialName("anilist") val anilist: String? = null, + @JsonProperty("simkl") val simkl: Int?, + @JsonProperty("imdb") val imdb: String? = null, + @JsonProperty("tmdb") val tmdb: String? = null, + @JsonProperty("mal") val mal: String? = null, + @JsonProperty("anilist") val anilist: String? = null, ) { - object Serializer : NonEmptySerializer(Ids.generatedSerializer()) - companion object { fun fromMap(map: Map): Ids { return Ids( @@ -405,21 +345,20 @@ class SimklApi : SyncAPI() { imdb = map[SimklSyncServices.Imdb], tmdb = map[SimklSyncServices.Tmdb], mal = map[SimklSyncServices.Mal], - anilist = map[SimklSyncServices.AniList], + anilist = map[SimklSyncServices.AniList] ) } } } fun toSyncSearchResult(): SyncAPI.SyncSearchResult? { - val currentIds = this.ids return SyncAPI.SyncSearchResult( this.title ?: return null, "Simkl", - currentIds?.simkl?.toString() ?: return null, - getUrlFromId(currentIds.simkl), + this.ids?.simkl?.toString() ?: return null, + getUrlFromId(this.ids.simkl), this.poster?.let { getPosterUrl(it) }, - if (this.type == "movie") TvType.Movie else TvType.TvSeries, + if (this.type == "movie") TvType.Movie else TvType.TvSeries ) } } @@ -434,7 +373,7 @@ class SimklApi : SyncAPI() { private var addEpisodes: Pair?, List?>? = null, private var removeEpisodes: Pair?, List?>? = null, // Required for knowing if the status should be overwritten - private var onList: Boolean = false, + private var onList: Boolean = false ) { fun token(token: AuthToken) = apply { this.headers = getHeaders(token) } fun apiUrl(url: String) = apply { this.url = url } @@ -448,7 +387,11 @@ class SimklApi : SyncAPI() { fun status(newStatus: Int?, oldStatus: Int?) = apply { onList = oldStatus != null // Only set status if its new - this.status = if (newStatus != oldStatus) newStatus else null + if (newStatus != oldStatus) { + this.status = newStatus + } else { + this.status = null + } } fun episodes( @@ -457,6 +400,7 @@ class SimklApi : SyncAPI() { oldEpisodes: Int?, ) = apply { if (allEpisodes == null || newEpisodes == null) return@apply + fun getEpisodes(rawEpisodes: List) = if (rawEpisodes.any { it.season != null }) { EpisodeMetadata.convertToSeasons(rawEpisodes) to null @@ -467,34 +411,36 @@ class SimklApi : SyncAPI() { // Do not add episodes if there is no change if (newEpisodes > (oldEpisodes ?: 0)) { this.addEpisodes = getEpisodes(allEpisodes.take(newEpisodes)) + // Set to watching if episodes are added and there is no current status if (!onList) { status = SimklListStatusType.Watching.value } } - if ((oldEpisodes ?: 0) > newEpisodes) { this.removeEpisodes = getEpisodes(allEpisodes.drop(newEpisodes)) } } suspend fun execute(): Boolean { - val time = getDateTime(APIHolder.unixTime) + val time = getDateTime(unixTime) val headers = this.headers ?: emptyMap() return if (this.status == SimklListStatusType.None.value) { app.post( "$url/sync/history/remove", - json = HistoryRequest( + json = StatusRequest( shows = listOf(HistoryMediaObject(ids = ids)), - movies = emptyList(), + movies = emptyList() ), - headers = headers, + headers = headers ).isSuccessful } else { val statusResponse = this.status?.let { setStatus -> - val newStatus = SimklListStatusType.entries.firstOrNull { - it.value == setStatus - }?.originalName ?: SimklListStatusType.Watching.originalName!! + val newStatus = + SimklListStatusType.entries + .firstOrNull { it.value == setStatus }?.originalName + ?: SimklListStatusType.Watching.originalName!! + app.post( "${this.url}/sync/add-to-list", json = StatusRequest( @@ -504,40 +450,41 @@ class SimklApi : SyncAPI() { null, ids, newStatus, - ), - ), - movies = emptyList(), + ) + ), movies = emptyList() ), - headers = headers, + headers = headers ).isSuccessful } ?: true val episodeRemovalResponse = removeEpisodes?.let { (seasons, episodes) -> app.post( "${this.url}/sync/history/remove", - json = HistoryRequest( + json = StatusRequest( shows = listOf( HistoryMediaObject( ids = ids, seasons = seasons, - episodes = episodes, - ), + episodes = episodes + ) ), - movies = emptyList(), + movies = emptyList() ), - headers = headers, + headers = headers ).isSuccessful } ?: true // You cannot rate if you are planning to watch it. - val shouldRate = score != null && status != SimklListStatusType.Planning.value + val shouldRate = + score != null && status != SimklListStatusType.Planning.value val realScore = if (shouldRate) score else null + val historyResponse = // Only post if there are episodes or score to upload if (addEpisodes != null || shouldRate) { app.post( "${this.url}/sync/history", - json = HistoryRequest( + json = StatusRequest( shows = listOf( HistoryMediaObject( null, @@ -547,33 +494,34 @@ class SimklApi : SyncAPI() { addEpisodes?.second, realScore, realScore?.let { time }, - ), - ), - movies = emptyList(), + ) + ), movies = emptyList() ), - headers = headers, + headers = headers ).isSuccessful - } else true + } else { + true + } + statusResponse && episodeRemovalResponse && historyResponse } } } } - fun getHeaders(token: AuthToken): Map = mapOf( - "Authorization" to "Bearer ${token.accessToken}", - "simkl-api-key" to CLIENT_ID, - ) + fun getHeaders(token: AuthToken): Map = + mapOf("Authorization" to "Bearer ${token.accessToken}", "simkl-api-key" to CLIENT_ID) suspend fun getEpisodes( simklId: Int?, type: String?, episodes: Int?, - hasEnded: Boolean?, + hasEnded: Boolean? ): Array? { if (simklId == null) return null + val cacheKey = "Episodes/$simklId" - val cache = SimklCache.getEpisodes(cacheKey) + val cache = SimklCache.getKey>(cacheKey) // Return cached result if its higher or equal the amount of episodes. if (cache != null && cache.size >= (episodes ?: 0)) { @@ -590,7 +538,6 @@ class SimklApi : SyncAPI() { }.toTypedArray() } } - val url = when (type) { "anime" -> "https://api.simkl.com/anime/episodes/$simklId" "tv" -> "https://api.simkl.com/tv/episodes/$simklId" @@ -605,86 +552,57 @@ class SimklApi : SyncAPI() { if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value // 1 Month cache - SimklCache.setEpisodes(cacheKey, it, Duration.parse(cacheTime)) + SimklCache.setKey(cacheKey, it, Duration.parse(cacheTime)) } } @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = HistoryMediaObject.Serializer::class) - data class HistoryMediaObject( - @JsonProperty("title") @SerialName("title") val title: String? = null, - @JsonProperty("year") @SerialName("year") val year: Int? = null, - @JsonProperty("ids") @SerialName("ids") val ids: MediaObject.Ids? = null, - @JsonProperty("seasons") @SerialName("seasons") val seasons: List? = null, - @JsonProperty("episodes") @SerialName("episodes") val episodes: List? = null, - @JsonProperty("rating") @SerialName("rating") val rating: Int? = null, - @JsonProperty("rated_at") @SerialName("rated_at") val ratedAt: String? = null, - ) { - object Serializer : NonEmptySerializer(HistoryMediaObject.generatedSerializer()) - } + class HistoryMediaObject( + @JsonProperty("title") title: String? = null, + @JsonProperty("year") year: Int? = null, + @JsonProperty("ids") ids: Ids? = null, + @JsonProperty("seasons") seasons: List? = null, + @JsonProperty("episodes") episodes: List? = null, + @JsonProperty("rating") val rating: Int? = null, + @JsonProperty("rated_at") val ratedAt: String? = null, + ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = RatingMediaObject.Serializer::class) - data class RatingMediaObject( - @JsonProperty("title") @SerialName("title") val title: String? = null, - @JsonProperty("year") @SerialName("year") val year: Int? = null, - @JsonProperty("ids") @SerialName("ids") val ids: MediaObject.Ids? = null, - @JsonProperty("rating") @SerialName("rating") val rating: Int, - @JsonProperty("rated_at") @SerialName("rated_at") val ratedAt: String? = getDateTime(APIHolder.unixTime), - ) { - object Serializer : NonEmptySerializer(RatingMediaObject.generatedSerializer()) - } + class RatingMediaObject( + @JsonProperty("title") title: String?, + @JsonProperty("year") year: Int?, + @JsonProperty("ids") ids: Ids?, + @JsonProperty("rating") val rating: Int, + @JsonProperty("rated_at") val ratedAt: String? = getDateTime(unixTime) + ) : MediaObject(title, year, ids) @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = StatusMediaObject.Serializer::class) - data class StatusMediaObject( - @JsonProperty("title") @SerialName("title") val title: String? = null, - @JsonProperty("year") @SerialName("year") val year: Int? = null, - @JsonProperty("ids") @SerialName("ids") val ids: MediaObject.Ids? = null, - @JsonProperty("to") @SerialName("to") val to: String, - @JsonProperty("watched_at") @SerialName("watched_at") val watchedAt: String? = getDateTime(APIHolder.unixTime), - ) { - object Serializer : NonEmptySerializer(StatusMediaObject.generatedSerializer()) - } + class StatusMediaObject( + @JsonProperty("title") title: String?, + @JsonProperty("year") year: Int?, + @JsonProperty("ids") ids: Ids?, + @JsonProperty("to") val to: String, + @JsonProperty("watched_at") val watchedAt: String? = getDateTime(unixTime) + ) : MediaObject(title, year, ids) @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = StatusRequest.Serializer::class) data class StatusRequest( - @JsonProperty("movies") @SerialName("movies") val movies: List, - @JsonProperty("shows") @SerialName("shows") val shows: List, - ) { - object Serializer : NonEmptySerializer(StatusRequest.generatedSerializer()) - } - - /** Same shape as [StatusRequest], for the endpoints that post [HistoryMediaObject]s instead. */ - @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = HistoryRequest.Serializer::class) - data class HistoryRequest( - @JsonProperty("movies") @SerialName("movies") val movies: List, - @JsonProperty("shows") @SerialName("shows") val shows: List, - ) { - object Serializer : NonEmptySerializer(HistoryRequest.generatedSerializer()) - } + @JsonProperty("movies") val movies: List, + @JsonProperty("shows") val shows: List + ) /** https://simkl.docs.apiary.io/#reference/sync/get-all-items/get-all-items-in-the-user's-watchlist */ - @Serializable data class AllItemsResponse( - @JsonProperty("shows") @SerialName("shows") val shows: List = emptyList(), - @JsonProperty("anime") @SerialName("anime") val anime: List = emptyList(), - @JsonProperty("movies") @SerialName("movies") val movies: List = emptyList(), + @JsonProperty("shows") + val shows: List = emptyList(), + @JsonProperty("anime") + val anime: List = emptyList(), + @JsonProperty("movies") + val movies: List = emptyList(), ) { companion object { fun merge(first: AllItemsResponse?, second: AllItemsResponse?): AllItemsResponse { + // Replace the first item with the same id, or add the new item fun MutableList.replaceOrAddItem(newItem: T, predicate: (T) -> Boolean) { for (i in this.indices) { @@ -693,10 +611,10 @@ class SimklApi : SyncAPI() { return } } - this.add(newItem) } + // fun merge( first: List?, second: List? @@ -730,17 +648,15 @@ class SimklApi : SyncAPI() { fun toLibraryItem(): SyncAPI.LibraryItem } - @Serializable data class MovieMetadata( - @JsonProperty("last_watched_at") @SerialName("last_watched_at") override val lastWatchedAt: String?, - @JsonProperty("status") @SerialName("status") override val status: String, - @JsonProperty("user_rating") @SerialName("user_rating") override val userRating: Int?, - @JsonProperty("last_watched") @SerialName("last_watched") override val lastWatched: String?, - @JsonProperty("watched_episodes_count") @SerialName("watched_episodes_count") override val watchedEpisodesCount: Int?, - @JsonProperty("total_episodes_count") @SerialName("total_episodes_count") override val totalEpisodesCount: Int?, - @JsonProperty("movie") @SerialName("movie") val movie: ShowMetadata.Show, + @JsonProperty("last_watched_at") override val lastWatchedAt: String?, + @JsonProperty("status") override val status: String, + @JsonProperty("user_rating") override val userRating: Int?, + @JsonProperty("last_watched") override val lastWatched: String?, + @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, + @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, + val movie: ShowMetadata.Show ) : Metadata { - @JsonIgnore override fun getIds(): ShowMetadata.Show.Ids { return this.movie.ids } @@ -760,22 +676,20 @@ class SimklApi : SyncAPI() { null, null, this.movie.year?.toYear(), - movie.ids.simkl, + movie.ids.simkl ) } } - @Serializable data class ShowMetadata( - @JsonProperty("last_watched_at") @SerialName("last_watched_at") override val lastWatchedAt: String?, - @JsonProperty("status") @SerialName("status") override val status: String, - @JsonProperty("user_rating") @SerialName("user_rating") override val userRating: Int?, - @JsonProperty("last_watched") @SerialName("last_watched") override val lastWatched: String?, - @JsonProperty("watched_episodes_count") @SerialName("watched_episodes_count") override val watchedEpisodesCount: Int?, - @JsonProperty("total_episodes_count") @SerialName("total_episodes_count") override val totalEpisodesCount: Int?, - @JsonProperty("show") @SerialName("show") val show: Show, + @JsonProperty("last_watched_at") override val lastWatchedAt: String?, + @JsonProperty("status") override val status: String, + @JsonProperty("user_rating") override val userRating: Int?, + @JsonProperty("last_watched") override val lastWatched: String?, + @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, + @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, + @JsonProperty("show") val show: Show ) : Metadata { - @JsonIgnore override fun getIds(): Show.Ids { return this.show.ids } @@ -795,30 +709,28 @@ class SimklApi : SyncAPI() { null, null, this.show.year?.toYear(), - show.ids.simkl, + show.ids.simkl ) } - @Serializable data class Show( - @JsonProperty("title") @SerialName("title") val title: String, - @JsonProperty("poster") @SerialName("poster") val poster: String?, - @JsonProperty("year") @SerialName("year") val year: Int?, - @JsonProperty("ids") @SerialName("ids") val ids: Ids, + @JsonProperty("title") val title: String, + @JsonProperty("poster") val poster: String?, + @JsonProperty("year") val year: Int?, + @JsonProperty("ids") val ids: Ids, ) { - @Serializable data class Ids( - @JsonProperty("simkl") @SerialName("simkl") val simkl: Int, - @JsonProperty("slug") @SerialName("slug") val slug: String?, - @JsonProperty("imdb") @SerialName("imdb") val imdb: String?, - @JsonProperty("zap2it") @SerialName("zap2it") val zap2it: String?, - @JsonProperty("tmdb") @SerialName("tmdb") val tmdb: String?, - @JsonProperty("offen") @SerialName("offen") val offen: String?, - @JsonProperty("tvdb") @SerialName("tvdb") val tvdb: String?, - @JsonProperty("mal") @SerialName("mal") val mal: String?, - @JsonProperty("anidb") @SerialName("anidb") val anidb: String?, - @JsonProperty("anilist") @SerialName("anilist") val anilist: String?, - @JsonProperty("traktslug") @SerialName("traktslug") val traktslug: String?, + @JsonProperty("simkl") val simkl: Int, + @JsonProperty("slug") val slug: String?, + @JsonProperty("imdb") val imdb: String?, + @JsonProperty("zap2it") val zap2it: String?, + @JsonProperty("tmdb") val tmdb: String?, + @JsonProperty("offen") val offen: String?, + @JsonProperty("tvdb") val tvdb: String?, + @JsonProperty("mal") val mal: String?, + @JsonProperty("anidb") val anidb: String?, + @JsonProperty("anilist") val anilist: String?, + @JsonProperty("traktslug") val traktslug: String? ) { fun matchesId(database: SimklSyncServices, id: String): Boolean { return when (database) { @@ -835,10 +747,27 @@ class SimklApi : SyncAPI() { } } + /** + * Appends api keys to the requests + **/ + /*private inner class HeaderInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + debugPrint { "${this@SimklApi.name} made request to ${chain.request().url}" } + return chain.proceed( + chain.request() + .newBuilder() + .addHeader("Authorization", "Bearer $token") + .addHeader("simkl-api-key", CLIENT_ID) + .build() + ) + } + }*/ + private suspend fun getUser(token: AuthToken): SettingsResponse = app.post("$mainUrl/users/settings", headers = getHeaders(token)) .parsed() + /** * Useful to get episodes on demand to prevent unnecessary requests. */ @@ -846,7 +775,7 @@ class SimklApi : SyncAPI() { private val simklId: Int?, private val type: String?, private val totalEpisodeCount: Int?, - private val hasEnded: Boolean?, + private val hasEnded: Boolean? ) { suspend fun getEpisodes(): Array? { return getEpisodes(simklId, type, totalEpisodeCount, hasEnded) @@ -861,12 +790,10 @@ class SimklApi : SyncAPI() { val episodeConstructor: SimklEpisodeConstructor, override var isFavorite: Boolean? = null, override var maxEpisodes: Int? = null, - /** - * Save seen episodes separately to know the change from old to new. - * Required to remove seen episodes if count decreases. - */ + /** Save seen episodes separately to know the change from old to new. + * Required to remove seen episodes if count decreases */ val oldEpisodes: Int, - val oldStatus: String?, + val oldStatus: String? ) : SyncAPI.AbstractSyncStatus() override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? { @@ -876,23 +803,22 @@ class SimklApi : SyncAPI() { // Key which assumes all ids are the same each time :/ // This could be some sort of reference system to make multiple IDs // point to the same key. - val idKey = realIds.toList().map { - "${it.first.originalName}=${it.second}" - }.sorted().joinToString() + val idKey = + realIds.toList().map { "${it.first.originalName}=${it.second}" }.sorted().joinToString() - val cachedObject = SimklCache.getMediaObject(idKey) + val cachedObject = SimklCache.getKey(idKey) val searchResult: MediaObject = cachedObject ?: (searchByIds(realIds)?.firstOrNull()?.also { result -> val cacheTime = if (result.hasEnded()) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value - SimklCache.setMediaObject(idKey, result, Duration.parse(cacheTime)) + SimklCache.setKey(idKey, result, Duration.parse(cacheTime)) }) ?: return null val episodeConstructor = SimklEpisodeConstructor( searchResult.ids?.simkl, searchResult.type, searchResult.totalEpisodes, - searchResult.hasEnded(), + searchResult.hasEnded() ) val foundItem = getSyncListSmart(auth)?.let { list -> @@ -907,16 +833,19 @@ class SimklApi : SyncAPI() { return SimklSyncStatus( status = foundItem.status?.let { SyncWatchType.fromInternalId( - SimklListStatusType.fromString(it)?.value + SimklListStatusType.fromString( + it + )?.value ) - } ?: return null, + } + ?: return null, score = Score.from10(foundItem.userRating), watchedEpisodes = foundItem.watchedEpisodesCount, maxEpisodes = searchResult.totalEpisodes, episodeConstructor = episodeConstructor, oldEpisodes = foundItem.watchedEpisodesCount ?: 0, oldScore = foundItem.userRating, - oldStatus = foundItem.status, + oldStatus = foundItem.status ) } else { return SimklSyncStatus( @@ -927,7 +856,7 @@ class SimklApi : SyncAPI() { episodeConstructor = episodeConstructor, oldEpisodes = 0, oldStatus = null, - oldScore = null, + oldScore = null ) } } @@ -935,11 +864,12 @@ class SimklApi : SyncAPI() { override suspend fun updateStatus( auth: AuthData?, id: String, - newStatus: AbstractSyncStatus, + newStatus: AbstractSyncStatus ): Boolean { - lastScoreTime = APIHolder.unixTime val parsedId = readIdFromString(id) + lastScoreTime = unixTime val simklStatus = newStatus as? SimklSyncStatus + val builder = SimklScoreBuilder.Builder() .apiUrl(this.mainUrl) .score(newStatus.score?.toInt(10), simklStatus?.oldScore) @@ -953,6 +883,7 @@ class SimklApi : SyncAPI() { .token(auth?.token ?: return false) .ids(MediaObject.Ids.fromMap(parsedId)) + // Get episodes only when required val episodes = simklStatus?.episodeConstructor?.getEpisodes() @@ -965,33 +896,38 @@ class SimklApi : SyncAPI() { } builder.episodes(episodes?.toList(), watchedEpisodes, simklStatus?.oldEpisodes) + requireLibraryRefresh = true return builder.execute() } + /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ private suspend fun searchByIds(serviceMap: Map): Array? { if (serviceMap.isEmpty()) return emptyArray() + return app.get( "$mainUrl/search/id", params = mapOf("client_id" to CLIENT_ID) + serviceMap.map { (service, id) -> service.originalName to id } - ).parsedSafe>() + ).parsedSafe() } override suspend fun search(auth: AuthData?, query: String): List? { return app.get( - "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to query) + "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name) ).parsedSafe>()?.mapNotNull { it.toSyncSearchResult() } } override fun loginRequest(): AuthLoginPage? { val lastLoginState = BigInteger(130, SecureRandom()).toString(32) - val url = "https://simkl.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$APP_STRING://$redirectUrlIdentifier&state=$lastLoginState" + val url = + "https://simkl.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrlIdentifier}&state=$lastLoginState" + return AuthLoginPage( url = url, - payload = lastLoginState, + payload = lastLoginState ) } @@ -1006,12 +942,12 @@ class SimklApi : SyncAPI() { return app.get( "$mainUrl/sync/all-items/", params = params, - headers = getHeaders(auth.token), - ).parsedSafe() + headers = getHeaders(auth.token) + ).parsedSafe() } private suspend fun getActivities(token: AuthToken): ActivitiesResponse? { - return app.post("$mainUrl/sync/activities", headers = getHeaders(token)).parsedSafe() + return app.post("$mainUrl/sync/activities", headers = getHeaders(token)).parsedSafe() } private fun getSyncListCached(auth: AuthData): AllItemsResponse? { @@ -1025,13 +961,18 @@ class SimklApi : SyncAPI() { val lastRemoval = listOf( activities?.tvShows?.removedFromList, activities?.anime?.removedFromList, - activities?.movies?.removedFromList, - ).maxOf { getUnixTime(it) ?: -1 } - val lastRealUpdate = listOf( - activities?.tvShows?.all, - activities?.anime?.all, - activities?.movies?.all, - ).maxOf { getUnixTime(it) ?: -1 } + activities?.movies?.removedFromList + ).maxOf { + getUnixTime(it) ?: -1 + } + val lastRealUpdate = + listOf( + activities?.tvShows?.all, + activities?.anime?.all, + activities?.movies?.all, + ).maxOf { + getUnixTime(it) ?: -1 + } debugPrint { "Cache times: lastCacheUpdate=$lastCacheUpdate, lastRemoval=$lastRemoval, lastRealUpdate=$lastRealUpdate" } val list = if (lastCacheUpdate == null || lastCacheUpdate < lastRemoval) { @@ -1043,33 +984,38 @@ class SimklApi : SyncAPI() { setKey(SIMKL_CACHED_LIST_TIME, userId, lastCacheUpdate) AllItemsResponse.merge( getSyncListCached(auth), - getSyncListSince(auth, lastCacheUpdate), + getSyncListSince(auth, lastCacheUpdate) ) } else { debugPrint { "Cached list update in ${this.name}." } getSyncListCached(auth) } - debugPrint { "List sizes: movies=${list?.movies?.size}, shows=${list?.shows?.size}, anime=${list?.anime?.size}" } + setKey(SIMKL_CACHED_LIST, userId, list) + return list } override suspend fun library(auth: AuthData?): SyncAPI.LibraryMetadata? { val list = getSyncListSmart(auth ?: return null) ?: return null - val baseMap = SimklListStatusType.entries - .filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value } - .associate { - it.stringRes to emptyList() - } + + val baseMap = + SimklListStatusType.entries + .filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value } + .associate { + it.stringRes to emptyList() + } val syncMap = listOf(list.anime, list.movies, list.shows) .flatten() - .groupBy { it.status } + .groupBy { + it.status + } .mapNotNull { (status, list) -> - val stringRes = status?.let { - SimklListStatusType.fromString(it)?.stringRes - } ?: return@mapNotNull null + val stringRes = + status?.let { SimklListStatusType.fromString(it)?.stringRes } + ?: return@mapNotNull null val libraryList = list.map { it.toLibraryItem() } stringRes to libraryList }.toMap() @@ -1095,14 +1041,15 @@ class SimklApi : SyncAPI() { override suspend fun pinRequest(): AuthPinData? { val pinAuthResp = app.get( - "$mainUrl/oauth/pin?client_id=$CLIENT_ID&redirect_uri=$APP_STRING://$redirectUrlIdentifier" + "$mainUrl/oauth/pin?client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrlIdentifier}" ).parsedSafe() ?: return null + return AuthPinData( deviceCode = pinAuthResp.deviceCode, userCode = pinAuthResp.userCode, verificationUrl = pinAuthResp.verificationUrl, expiresIn = pinAuthResp.expiresIn, - interval = pinAuthResp.interval, + interval = pinAuthResp.interval ) } @@ -1110,6 +1057,7 @@ class SimklApi : SyncAPI() { val pinAuthResp = app.get( "$mainUrl/oauth/pin/${payload.userCode}?client_id=$CLIENT_ID" ).parsedSafe() ?: return null + return AuthToken( accessToken = pinAuthResp.accessToken ?: return null, ) @@ -1125,6 +1073,7 @@ class SimklApi : SyncAPI() { val tokenResponse = app.post( "$mainUrl/oauth/token", json = TokenRequest(code) ).parsedSafe() ?: return null + return AuthToken( accessToken = tokenResponse.accessToken, ) @@ -1135,7 +1084,7 @@ class SimklApi : SyncAPI() { return AuthUser( id = user.account.id, name = user.user.name, - profilePicture = user.user.avatar, + profilePicture = user.user.avatar ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt index 317c3dc90..19122768e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt @@ -7,10 +7,9 @@ import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.SubtitleResource import com.lagradost.cloudstream3.syncproviders.AuthData import com.lagradost.cloudstream3.syncproviders.SubtitleAPI +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.SubtitleHelper -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import java.util.concurrent.TimeUnit class SubSourceApi : SubtitleAPI() { override val name = "SubSource" @@ -19,70 +18,77 @@ class SubSourceApi : SubtitleAPI() { override val requiresLogin = false companion object { - const val APIURL = "https://api.subsource.net/v1" + const val APIURL = "https://api.subsource.net/api" + const val DOWNLOADENDPOINT = "https://api.subsource.net/api/downloadSub" } override suspend fun search( auth: AuthData?, query: AbstractSubtitleEntities.SubtitleSearch ): List? { + //Only supports Imdb Id search for now if (query.imdbId == null) return null val queryLang = SubtitleHelper.fromTagToEnglishLanguageName(query.lang) val type = if ((query.seasonNumber ?: 0) > 0) TvType.TvSeries else TvType.Movie - val searchResponse = app.post( - url = "$APIURL/movie/search", - json = mapOf( - "includeSeasons" to false, - "limit" to 15, - "query" to query.imdbId!!, - "signal" to "{}" - ), - cacheTime = 120, - cacheUnit = TimeUnit.MINUTES, - ).parsedSafe() ?: return null - - val firstResult = searchResponse.results.firstOrNull() ?: return null - - val apiResponse = app.get( - url = "$APIURL${firstResult.link.replace("series", "subtitles")}", - cacheTime = 120, - cacheUnit = TimeUnit.MINUTES, - ).parsedSafe() ?: return null - - val filteredSubtitles = apiResponse.subtitles.filter { sub -> - sub.releaseType != "trailer" && - sub.language.equals(queryLang, true) - } - - // api doesn't has episode number or lang filtering - val subtitles = if (type == TvType.Movie) { - filteredSubtitles - } else { - val shouldContain = String.format( - null, - "E%02d", - query.epNumber + val searchRes = app.post( + url = "$APIURL/searchMovie", + data = mapOf( + "query" to query.imdbId!! + ) + ).parsedSafe() ?: return null + + val postData = if (type == TvType.TvSeries) { + mapOf( + "langs" to "[]", + "movieName" to searchRes.found.first().linkName, + "season" to "season-${query.seasonNumber}" + ) + } else { + mapOf( + "langs" to "[]", + "movieName" to searchRes.found.first().linkName, ) - filteredSubtitles.filter { sub -> - sub.releaseInfo.contains( - shouldContain - ) - } } - return subtitles.map { subtitle -> + val getMovieRes = app.post( + url = "$APIURL/getMovie", + data = postData + ).parsedSafe().let { + // api doesn't has episode number or lang filtering + if (type == TvType.Movie) { + it?.subs?.filter { sub -> + sub.lang == queryLang + } + } else { + it?.subs?.filter { sub -> + sub.releaseName!!.contains( + String.format( + null, + "E%02d", + query.epNumber + ) + ) && sub.lang == queryLang + } + } + } ?: return null + + return getMovieRes.map { subtitle -> AbstractSubtitleEntities.SubtitleEntity( idPrefix = this.idPrefix, - name = subtitle.releaseInfo, - lang = subtitle.language, - data = subtitle.link, + name = subtitle.releaseName!!, + lang = subtitle.lang!!, + data = SubData( + movie = subtitle.linkName!!, + lang = subtitle.lang, + id = subtitle.subId.toString(), + ).toJson(), type = type, source = this.name, epNumber = query.epNumber, seasonNumber = query.seasonNumber, - isHearingImpaired = subtitle.hearingImpaired == 1, + isHearingImpaired = subtitle.hi == 1, ) } } @@ -91,114 +97,71 @@ class SubSourceApi : SubtitleAPI() { auth: AuthData?, subtitle: AbstractSubtitleEntities.SubtitleEntity ) { - val data = app.get("$APIURL/subtitle/${subtitle.data}") - .parsedSafe() - ?: return + val parsedSub = parseJson(subtitle.data) + + val subRes = app.post( + url = "$APIURL/getSub", + data = mapOf( + "movie" to parsedSub.movie, + "lang" to subtitle.lang, + "id" to parsedSub.id + ) + ).parsedSafe() ?: return this.addZipUrl( - "$APIURL/subtitle/download/${data.subtitle.downloadToken}" + "$DOWNLOADENDPOINT/${subRes.sub.downloadToken}" ) { name, _ -> name } } - - @Serializable - data class SearchRoot( - @JsonProperty("success") @SerialName("success") var success: Boolean? = null, - @JsonProperty("results") @SerialName("results") var results: ArrayList = arrayListOf(), - @JsonProperty("users") @SerialName("users") var users: ArrayList = arrayListOf() + data class ApiSearch( + @JsonProperty("success") val success: Boolean, + @JsonProperty("found") val found: List, ) - @Serializable - data class Users( - - @JsonProperty("id") @SerialName("id") var id: Int? = null, - @JsonProperty("displayname") @SerialName("displayname") var displayname: String? = null, - @JsonProperty("avatar") @SerialName("avatar") var avatar: String? = null, - @JsonProperty("badges") @SerialName("badges") var badges: ArrayList = arrayListOf() - + data class Found( + @JsonProperty("id") val id: Long, + @JsonProperty("title") val title: String, + @JsonProperty("seasons") val seasons: Long, + @JsonProperty("type") val type: String, + @JsonProperty("releaseYear") val releaseYear: Long, + @JsonProperty("linkName") val linkName: String, ) - @Serializable - data class Results( - @JsonProperty("id") @SerialName("id") var id: Int? = null, - @JsonProperty("title") @SerialName("title") var title: String? = null, - @JsonProperty("type") @SerialName("type") var type: String? = null, - @JsonProperty("link") @SerialName("link") var link: String, - @JsonProperty("releaseYear") @SerialName("releaseYear") var releaseYear: Int? = null, - @JsonProperty("poster") @SerialName("poster") var poster: String? = null, - @JsonProperty("subtitleCount") @SerialName("subtitleCount") var subtitleCount: String? = null, - @JsonProperty("rating") @SerialName("rating") var rating: Double? = null, - @JsonProperty("cast") @SerialName("cast") var cast: ArrayList = arrayListOf(), - @JsonProperty("genres") @SerialName("genres") var genres: ArrayList = arrayListOf(), - @JsonProperty("score") @SerialName("score") var score: Double? = null + data class ApiResponse( + @JsonProperty("success") val success: Boolean, + @JsonProperty("movie") val movie: Movie, + @JsonProperty("subs") val subs: List, ) - @Serializable - - data class ItemRoot( - - // @SerialName("media_type" ) var mediaType : String? = null, - @JsonProperty("subtitles") @SerialName("subtitles") var subtitles: ArrayList, - //@SerialName("movie" ) var movie : Movie? = Movie() - + data class Movie( + @JsonProperty("id") val id: Long? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("year") val year: Long? = null, + @JsonProperty("fullName") val fullName: String? = null, ) - @Serializable - data class Subtitles( - - @JsonProperty("id") @SerialName("id") var id: Int? = null, - @JsonProperty("language") @SerialName("language") var language: String, - @JsonProperty("release_type") @SerialName("release_type") var releaseType: String? = null, - @JsonProperty("release_info") @SerialName("release_info") var releaseInfo: String, - @JsonProperty("upload_date") @SerialName("upload_date") var uploadDate: String? = null, - @JsonProperty("hearing_impaired") @SerialName("hearing_impaired") var hearingImpaired: Int? = null, - @JsonProperty("caption") @SerialName("caption") var caption: String? = null, - @JsonProperty("rating") @SerialName("rating") var rating: String? = null, - @JsonProperty("uploader_id") @SerialName("uploader_id") var uploaderId: Int? = null, - @JsonProperty("uploader_displayname") @SerialName("uploader_displayname") var uploaderDisplayname: String? = null, - @JsonProperty("uploader_badges") @SerialName("uploader_badges") var uploaderBadges: ArrayList = arrayListOf(), - @JsonProperty("link") @SerialName("link") var link: String, - @JsonProperty("production_type") @SerialName("production_type") var productionType: String? = null, - @JsonProperty("last_subtitle") @SerialName("last_subtitle") var lastSubtitle: Boolean? = null - + data class Sub( + @JsonProperty("hi") val hi: Int? = null, + @JsonProperty("fullLink") val fullLink: String? = null, + @JsonProperty("linkName") val linkName: String? = null, + @JsonProperty("lang") val lang: String? = null, + @JsonProperty("releaseName") val releaseName: String? = null, + @JsonProperty("subId") val subId: Long? = null, ) - @Serializable - data class DownloadRoot( - @JsonProperty("subtitle") @SerialName("subtitle") var subtitle: Subtitle, - //@SerializedName("movie" ) var movie : Movie? = Movie(), - //@SerializedName("donationLinks" ) var donationLinks : DonationLinks? = DonationLinks(), - //@SerializedName("isDownloaded" ) var isDownloaded : Boolean? = null, - //@SerializedName("user_rated" ) var userRated : String? = null + data class SubData( + @JsonProperty("movie") val movie: String, + @JsonProperty("lang") val lang: String, + @JsonProperty("id") val id: String, ) - @Serializable - data class Subtitle( - - @JsonProperty("id") @SerialName("id") var id: Int? = null, - @JsonProperty("uploaded_at") @SerialName("uploaded_at") var uploadedAt: String? = null, - @JsonProperty("language") @SerialName("language") var language: String? = null, - @JsonProperty("rating") @SerialName("rating") var rating: String? = null, - //SerialName("rates" ) var rates : Rates? = Rates(), - @JsonProperty("uploaded_by") @SerialName("uploaded_by") var uploadedBy: Int? = null, - //@SerialName("contribs" ) var contribs : ArrayList = arrayListOf(), - @JsonProperty("release_info") @SerialName("release_info") var releaseInfo: ArrayList = arrayListOf(), - @JsonProperty("commentary") @SerialName("commentary") var commentary: String? = null, - @JsonProperty("files") @SerialName("files") var files: String? = null, - @JsonProperty("size") @SerialName("size") var size: String? = null, - @JsonProperty("downloads") @SerialName("downloads") var downloads: Int? = null, - @JsonProperty("comments") @SerialName("comments") var comments: Int? = null, - @JsonProperty("production_type") @SerialName("production_type") var productionType: String? = null, - @JsonProperty("release_type") @SerialName("release_type") var releaseType: String? = null, - @JsonProperty("episode") @SerialName("episode") var episode: String? = null, - @JsonProperty("hearing_impaired") @SerialName("hearing_impaired") var hearingImpaired: Int? = null, - @JsonProperty("foreign_parts") @SerialName("foreign_parts") var foreignParts: String? = null, - @JsonProperty("framerate") @SerialName("framerate") var framerate: String? = null, - @JsonProperty("preview") @SerialName("preview") var preview: String? = null, - @JsonProperty("user_uploaded") @SerialName("user_uploaded") var userUploaded: Boolean? = null, - @JsonProperty("download_token") @SerialName("download_token") var downloadToken: String - + data class SubTitleLink( + @JsonProperty("sub") val sub: SubToken, ) -} + + data class SubToken( + @JsonProperty("downloadToken") val downloadToken: String, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt index 19bd3b1a7..1f1e6de44 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt @@ -12,8 +12,6 @@ import com.lagradost.cloudstream3.syncproviders.AuthToken import com.lagradost.cloudstream3.syncproviders.AuthUser import com.lagradost.cloudstream3.syncproviders.SubtitleAPI import com.lagradost.cloudstream3.TvType -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable class SubDlApi : SubtitleAPI() { override val name = "SubDL" @@ -26,7 +24,7 @@ class SubDlApi : SubtitleAPI() { override val createAccountUrl = "https://subdl.com/panel/register" companion object { - const val APIURL = "https://api.subdl.com" + const val APIURL = "https://apiold.subdl.com" const val APIENDPOINT = "$APIURL/api/v1/subtitles" const val DOWNLOADENDPOINT = "https://dl.subdl.com" } @@ -124,80 +122,72 @@ class SubDlApi : SubtitleAPI() { } } - @Serializable data class SubtitleOAuthEntity( - @JsonProperty("userEmail") @SerialName("userEmail") var userEmail: String, - @JsonProperty("pass") @SerialName("pass") var pass: String, - @JsonProperty("name") @SerialName("name") var name: String? = null, - @JsonProperty("accessToken") @SerialName("accessToken") var accessToken: String? = null, - @JsonProperty("apiKey") @SerialName("apiKey") var apiKey: String? = null, + @JsonProperty("userEmail") var userEmail: String, + @JsonProperty("pass") var pass: String, + @JsonProperty("name") var name: String? = null, + @JsonProperty("accessToken") var accessToken: String? = null, + @JsonProperty("apiKey") var apiKey: String? = null, ) - @Serializable data class OAuthTokenResponse( - @JsonProperty("token") @SerialName("token") val token: String, - @JsonProperty("userData") @SerialName("userData") val userData: UserData? = null, - @JsonProperty("status") @SerialName("status") val status: Boolean? = null, - @JsonProperty("message") @SerialName("message") val message: String? = null, + @JsonProperty("token") val token: String, + @JsonProperty("userData") val userData: UserData? = null, + @JsonProperty("status") val status: Boolean? = null, + @JsonProperty("message") val message: String? = null, ) - @Serializable data class UserData( - @JsonProperty("email") @SerialName("email") val email: String, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("country") @SerialName("country") val country: String, - @JsonProperty("scStepCode") @SerialName("scStepCode") val scStepCode: String, - @JsonProperty("scVerified") @SerialName("scVerified") val scVerified: Boolean, - @JsonProperty("username") @SerialName("username") val username: String? = null, - @JsonProperty("scUsername") @SerialName("scUsername") val scUsername: String, + @JsonProperty("email") val email: String, + @JsonProperty("name") val name: String, + @JsonProperty("country") val country: String, + @JsonProperty("scStepCode") val scStepCode: String, + @JsonProperty("scVerified") val scVerified: Boolean, + @JsonProperty("username") val username: String? = null, + @JsonProperty("scUsername") val scUsername: String, ) - @Serializable data class ApiKeyResponse( - @JsonProperty("ok") @SerialName("ok") val ok: Boolean? = false, - @JsonProperty("api_key") @SerialName("api_key") val apiKey: String, - @JsonProperty("usage") @SerialName("usage") val usage: Usage? = null, + @JsonProperty("ok") val ok: Boolean? = false, + @JsonProperty("api_key") val apiKey: String, + @JsonProperty("usage") val usage: Usage? = null, ) - @Serializable data class Usage( - @JsonProperty("total") @SerialName("total") val total: Long? = 0, - @JsonProperty("today") @SerialName("today") val today: Long? = 0, + @JsonProperty("total") val total: Long? = 0, + @JsonProperty("today") val today: Long? = 0, ) - @Serializable data class ApiResponse( - @JsonProperty("status") @SerialName("status") val status: Boolean? = null, - @JsonProperty("results") @SerialName("results") val results: List? = null, - @JsonProperty("subtitles") @SerialName("subtitles") val subtitles: List? = null, + @JsonProperty("status") val status: Boolean? = null, + @JsonProperty("results") val results: List? = null, + @JsonProperty("subtitles") val subtitles: List? = null, ) - @Serializable data class Result( - @JsonProperty("sd_id") @SerialName("sd_id") val sdId: Int? = null, - @JsonProperty("type") @SerialName("type") val type: String? = null, - @JsonProperty("name") @SerialName("name") val name: String? = null, - @JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String? = null, - @JsonProperty("tmdb_id") @SerialName("tmdb_id") val tmdbId: Long? = null, - @JsonProperty("first_air_date") @SerialName("first_air_date") val firstAirDate: String? = null, - @JsonProperty("year") @SerialName("year") val year: Int? = null, + @JsonProperty("sd_id") val sdId: Int? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("name") val name: String? = null, + @JsonProperty("imdb_id") val imdbId: String? = null, + @JsonProperty("tmdb_id") val tmdbId: Long? = null, + @JsonProperty("first_air_date") val firstAirDate: String? = null, + @JsonProperty("year") val year: Int? = null, ) - @Serializable data class Subtitle( - @JsonProperty("release_name") @SerialName("release_name") val releaseName: String, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("lang") @SerialName("lang") val lang: String, // subdl language code - @JsonProperty("author") @SerialName("author") val author: String? = null, - @JsonProperty("url") @SerialName("url") val url: String? = null, - @JsonProperty("subtitlePage") @SerialName("subtitlePage") val subtitlePage: String? = null, - @JsonProperty("season") @SerialName("season") val season: Int? = null, - @JsonProperty("episode") @SerialName("episode") val episode: Int? = null, - @JsonProperty("language") @SerialName("language") val language: String? = null, // full language name - @JsonProperty("hi") @SerialName("hi") val hearingImpaired: Boolean? = null, + @JsonProperty("release_name") val releaseName: String, + @JsonProperty("name") val name: String, + @JsonProperty("lang") val lang: String, // subdl language code + @JsonProperty("author") val author: String? = null, + @JsonProperty("url") val url: String? = null, + @JsonProperty("subtitlePage") val subtitlePage: String? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("episode") val episode: Int? = null, + @JsonProperty("language") val language: String? = null, // full language name + @JsonProperty("hi") val hearingImpaired: Boolean? = null, ) - // https://subdl.com/api-files/language_list.json + // https://subdl.com/api-files/language_list.json // most of it is IETF BPC 47 conformant tag // but there are some exceptions private val langTagIETF2subdl = mapOf( @@ -207,63 +197,63 @@ class SubDlApi : SubtitleAPI() { "en-nl" to "NL_EN", // "Dutch_English" "pt-br" to "BR_PT", // "Brazillian Portuguese" "zh-hant" to "ZH_BG", // "Big 5 code" -> traditional Chinese (?_?) - // "ar" to "AR", // "Arabic" - // "az" to "AZ", // "Azerbaijani" - // "be" to "BE", // "Belarusian" - // "bg" to "BG", // "Bulgarian" - // "bn" to "BN", // "Bengali" - // "bs" to "BS", // "Bosnian" - // "ca" to "CA", // "Catalan" - // "cs" to "CS", // "Czech" - // "da" to "DA", // "Danish" - // "de" to "DE", // "German" - // "el" to "EL", // "Greek" - // "en" to "EN", // "English" - // "eo" to "EO", // "Esperanto" - // "es" to "ES", // "Spanish" - // "et" to "ET", // "Estonian" - // "fa" to "FA", // "Farsi_Persian" - // "fi" to "FI", // "Finnish" - // "fr" to "FR", // "French" - // "he" to "HE", // "Hebrew" - // "hi" to "HI", // "Hindi" - // "hr" to "HR", // "Croatian" - // "hu" to "HU", // "Hungarian" - // "id" to "ID", // "Indonesian" - // "is" to "IS", // "Icelandic" - // "it" to "IT", // "Italian" - // "ja" to "JA", // "Japanese" - // "ka" to "KA", // "Georgian" - // "kl" to "KL", // "Greenlandic" - // "ko" to "KO", // "Korean" - // "ku" to "KU", // "Kurdish" - // "lt" to "LT", // "Lithuanian" - // "lv" to "LV", // "Latvian" - // "mk" to "MK", // "Macedonian" - // "ml" to "ML", // "Malayalam" - // "mni" to "MNI", // "Manipuri" - // "ms" to "MS", // "Malay" - // "my" to "MY", // "Burmese" - // "nl" to "NL", // "Dutch" - // "no" to "NO", // "Norwegian" - // "pl" to "PL", // "Polish" - // "pt" to "PT", // "Portuguese" - // "ro" to "RO", // "Romanian" - // "ru" to "RU", // "Russian" - // "si" to "SI", // "Sinhala" - // "sk" to "SK", // "Slovak" - // "sl" to "SL", // "Slovenian" - // "sq" to "SQ", // "Albanian" - // "sr" to "SR", // "Serbian" - // "sv" to "SV", // "Swedish" - // "ta" to "TA", // "Tamil" - // "te" to "TE", // "Telugu" - // "th" to "TH", // "Thai" - // "tl" to "TL", // "Tagalog" - // "tr" to "TR", // "Turkish" - // "uk" to "UK", // "Ukranian" - // "ur" to "UR", // "Urdu" - // "vi" to "VI", // "Vietnamese" - // "zh" to "ZH", // "Chinese BG code" + // "ar" to "AR", // "Arabic" + // "az" to "AZ", // "Azerbaijani" + // "be" to "BE", // "Belarusian" + // "bg" to "BG", // "Bulgarian" + // "bn" to "BN", // "Bengali" + // "bs" to "BS", // "Bosnian" + // "ca" to "CA", // "Catalan" + // "cs" to "CS", // "Czech" + // "da" to "DA", // "Danish" + // "de" to "DE", // "German" + // "el" to "EL", // "Greek" + // "en" to "EN", // "English" + // "eo" to "EO", // "Esperanto" + // "es" to "ES", // "Spanish" + // "et" to "ET", // "Estonian" + // "fa" to "FA", // "Farsi_Persian" + // "fi" to "FI", // "Finnish" + // "fr" to "FR", // "French" + // "he" to "HE", // "Hebrew" + // "hi" to "HI", // "Hindi" + // "hr" to "HR", // "Croatian" + // "hu" to "HU", // "Hungarian" + // "id" to "ID", // "Indonesian" + // "is" to "IS", // "Icelandic" + // "it" to "IT", // "Italian" + // "ja" to "JA", // "Japanese" + // "ka" to "KA", // "Georgian" + // "kl" to "KL", // "Greenlandic" + // "ko" to "KO", // "Korean" + // "ku" to "KU", // "Kurdish" + // "lt" to "LT", // "Lithuanian" + // "lv" to "LV", // "Latvian" + // "mk" to "MK", // "Macedonian" + // "ml" to "ML", // "Malayalam" + // "mni" to "MNI", // "Manipuri" + // "ms" to "MS", // "Malay" + // "my" to "MY", // "Burmese" + // "nl" to "NL", // "Dutch" + // "no" to "NO", // "Norwegian" + // "pl" to "PL", // "Polish" + // "pt" to "PT", // "Portuguese" + // "ro" to "RO", // "Romanian" + // "ru" to "RU", // "Russian" + // "si" to "SI", // "Sinhala" + // "sk" to "SK", // "Slovak" + // "sl" to "SL", // "Slovenian" + // "sq" to "SQ", // "Albanian" + // "sr" to "SR", // "Serbian" + // "sv" to "SV", // "Swedish" + // "ta" to "TA", // "Tamil" + // "te" to "TE", // "Telugu" + // "th" to "TH", // "Thai" + // "tl" to "TL", // "Tagalog" + // "tr" to "TR", // "Turkish" + // "uk" to "UK", // "Ukranian" + // "ur" to "UR", // "Urdu" + // "vi" to "VI", // "Vietnamese" + // "zh" to "ZH", // "Chinese BG code" ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 8ec082520..93a79689e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -17,7 +17,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.newSearchResponseList -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.ExtractorLink import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -55,7 +55,7 @@ class APIRepository(val api: MainAPI) { val hash: Pair ) - private val cache = atomicListOf() + private val cache = threadSafeListOf() private var cacheIndex: Int = 0 const val CACHE_SIZE = 20 @@ -66,7 +66,9 @@ class APIRepository(val api: MainAPI) { private fun afterPluginsLoaded(forceReload: Boolean) { if (forceReload) { - cache.clear() + synchronized(cache) { + cache.clear() + } } } @@ -89,25 +91,21 @@ class APIRepository(val api: MainAPI) { val fixedUrl = api.fixUrl(url) val lookingForHash = Pair(api.name, fixedUrl) - val cached = cache.withLock { - var found: LoadResponse? = null + synchronized(cache) { for (item in cache) { // 10 min save if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) { - found = item.response - break + return@withTimeout item.response } } - found } - if (cached != null) return@withTimeout cached api.load(fixedUrl)?.also { response -> // Remove all blank tags as early as possible response.tags = response.tags?.filter { it.isNotBlank() } val add = SavedLoadResponse(unixTime, response, lookingForHash) - cache.withLock { + synchronized(cache) { if (cache.size > CACHE_SIZE) { cache[cacheIndex] = add // rolling cache cacheIndex = (cacheIndex + 1) % CACHE_SIZE @@ -217,4 +215,4 @@ class APIRepository(val api: MainAPI) { return false } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index f00fd8031..f91d40f28 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -12,7 +12,9 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.ListView import androidx.appcompat.app.AlertDialog -import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.module.kotlin.kotlinModule import com.google.android.gms.cast.MediaLoadOptions import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.MediaSeekOptions @@ -35,24 +37,35 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs -import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import org.json.JSONObject +/*class SkipOpController(val view: ImageView) : UIController() { + init { + view.setImageResource(R.drawable.exo_controls_fastforward) + view.setOnClickListener { + remoteMediaClient?.let { + val options = MediaSeekOptions.Builder() + .setPosition(it.approximateStreamPosition + 85000) + it.seek(options.build()) + } + } + } +}*/ + private fun RemoteMediaClient.getItemIndex(): Int? { return try { val index = this.mediaQueue.itemIds.indexOf(this.currentItem?.itemId ?: 0) if (index < 0) null else index - } catch (_: Exception) { + } catch (e: Exception) { null } } @@ -79,41 +92,51 @@ class SkipNextEpisodeController(val view: ImageView) : UIController() { } } -@Serializable data class MetadataHolder( - @JsonProperty("apiName") @SerialName("apiName") val apiName: String, - @JsonProperty("isMovie") @SerialName("isMovie") val isMovie: Boolean, - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("poster") @SerialName("poster") val poster: String?, - @JsonProperty("currentEpisodeIndex") @SerialName("currentEpisodeIndex") val currentEpisodeIndex: Int, - @JsonProperty("episodes") @SerialName("episodes") val episodes: List, - @JsonProperty("currentLinks") @SerialName("currentLinks") val currentLinks: List, - @JsonProperty("currentSubtitles") @SerialName("currentSubtitles") val currentSubtitles: List, + val apiName: String, + val isMovie: Boolean, + val title: String?, + val poster: String?, + val currentEpisodeIndex: Int, + val episodes: List, + val currentLinks: List, + val currentSubtitles: List ) -class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : UIController() { +class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : + UIController() { + private val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() + init { view.setImageResource(R.drawable.ic_baseline_playlist_play_24) view.setOnClickListener { + // lateinit var dialog: AlertDialog val holder = getCurrentMetaData() + if (holder != null) { val items = holder.currentLinks if (items.isNotEmpty() && remoteMediaClient?.currentItem != null) { - val subTracks = remoteMediaClient?.mediaInfo?.mediaTracks?.filter { it.type == MediaTrack.TYPE_TEXT } - ?: ArrayList() + val subTracks = + remoteMediaClient?.mediaInfo?.mediaTracks?.filter { it.type == MediaTrack.TYPE_TEXT } + ?: ArrayList() - val bottomSheetDialogBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack) + val bottomSheetDialogBuilder = + AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack) bottomSheetDialogBuilder.setView(R.layout.sort_bottom_sheet) - val bottomSheetDialog = bottomSheetDialogBuilder.create() bottomSheetDialog.show() - - val providerList = bottomSheetDialog.findViewById(R.id.sort_providers)!! - val subtitleList = bottomSheetDialog.findViewById(R.id.sort_subtitles)!! + // bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet) + val providerList = + bottomSheetDialog.findViewById(R.id.sort_providers)!! + val subtitleList = + bottomSheetDialog.findViewById(R.id.sort_subtitles)!! if (subTracks.isEmpty()) { - bottomSheetDialog.findViewById(R.id.sort_subtitles_holder)?.visibility = GONE + bottomSheetDialog.findViewById(R.id.sort_subtitles_holder)?.visibility = + GONE } else { - val arrayAdapter = ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) + val arrayAdapter = + ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) arrayAdapter.add(view.context.getString(R.string.no_subtitles)) arrayAdapter.addAll(subTracks.mapNotNull { it.name }) @@ -121,8 +144,10 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi subtitleList.adapter = arrayAdapter val currentTracks = remoteMediaClient?.mediaStatus?.activeTrackIds - val subtitleIndex = if (currentTracks == null) 0 else subTracks.map { it.id } - .indexOfFirst { currentTracks.contains(it) } + 1 + + val subtitleIndex = + if (currentTracks == null) 0 else subTracks.map { it.id } + .indexOfFirst { currentTracks.contains(it) } + 1 subtitleList.setSelection(subtitleIndex) subtitleList.setItemChecked(subtitleIndex, true) @@ -134,7 +159,9 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi ChromecastSubtitlesFragment.getCurrentSavedStyle().apply { val font = TextTrackStyle() font.setFontFamily(fontFamily ?: "Google Sans") - fontGenericFamily?.let { font.fontGenericFamily = it } + fontGenericFamily?.let { + font.fontGenericFamily = it + } font.windowColor = windowColor font.backgroundColor = backgroundColor @@ -151,7 +178,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi if (!it.status.isSuccess) { Log.e( "CHROMECAST", "Failed with status code:" + - it.status.statusCode + " > " + it.status.statusMessage + it.status.statusCode + " > " + it.status.statusMessage ) } } @@ -160,15 +187,17 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi } } - // https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages.MediaInformation + //https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages.MediaInformation val contentUrl = (remoteMediaClient?.currentItem?.media?.contentUrl ?: remoteMediaClient?.currentItem?.media?.contentId) - val sortingMethods = items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" } - .toTypedArray() + val sortingMethods = + items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" } + .toTypedArray() val sotringIndex = items.indexOfFirst { it.url == contentUrl } - val arrayAdapter = ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) + val arrayAdapter = + ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) arrayAdapter.addAll(sortingMethods.toMutableList()) providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE @@ -178,8 +207,10 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi providerList.setOnItemClickListener { _, _, which, _ -> val epData = holder.episodes[holder.currentEpisodeIndex] + fun loadMirror(index: Int) { if (holder.currentLinks.size <= index) return + val mediaItem = getMediaInfo( epData, holder, @@ -189,21 +220,25 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi ) val startAt = remoteMediaClient?.approximateStreamPosition ?: 0 + + //remoteMediaClient.load(mediaItem, true, startAt) try { // THIS IS VERY IMPORTANT BECAUSE WE NEVER WANT TO AUTOLOAD THE NEXT EPISODE val currentIdIndex = remoteMediaClient?.getItemIndex() + val nextId = remoteMediaClient?.mediaQueue?.itemIds?.get( currentIdIndex?.plus(1) ?: 0 ) - if (currentIdIndex == null && nextId != null) { awaitLinks( remoteMediaClient?.queueInsertAndPlayItem( MediaQueueItem.Builder(mediaItem).build(), nextId, startAt, - JSONObject(), + JSONObject() ) - ) { loadMirror(index + 1) } + ) { + loadMirror(index + 1) + } } else { val mediaLoadOptions = MediaLoadOptions.Builder() @@ -215,9 +250,11 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi mediaItem, mediaLoadOptions ) - ) { loadMirror(index + 1) } + ) { + loadMirror(index + 1) + } } - } catch (_: Exception) { + } catch (e: Exception) { val mediaLoadOptions = MediaLoadOptions.Builder() .setPlayPosition(startAt) @@ -228,8 +265,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi } } } - loadMirror(which) + bottomSheetDialog.dismissSafe(activity) } } @@ -239,19 +276,23 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi private fun getCurrentMetaData(): MetadataHolder? { return try { - val data = remoteMediaClient?.mediaInfo?.customData?.toString() ?: return null - parseJson(data) - } catch (_: Exception) { + val data = remoteMediaClient?.mediaInfo?.customData?.toString() + data?.toKotlinObject() + } catch (e: Exception) { null } } var isLoadingMore = false + + override fun onMediaStatusUpdated() { super.onMediaStatusUpdated() val meta = getCurrentMetaData() - view.visibility = if ((meta?.currentLinks?.size ?: 0) > 1) VISIBLE else INVISIBLE + view.visibility = if ((meta?.currentLinks?.size + ?: 0) > 1 + ) VISIBLE else INVISIBLE try { if (meta != null && meta.episodes.size > meta.currentEpisodeIndex + 1) { val currentIdIndex = remoteMediaClient?.getItemIndex() ?: return @@ -268,7 +309,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi currentPosition, currentDuration, epData, - meta.episodes.getOrNull(index + 1), + meta.episodes.getOrNull(index + 1) ) } catch (t: Throwable) { logError(t) @@ -279,7 +320,9 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi ioSafe { val currentLinks = mutableSetOf() val currentSubs = mutableSetOf() + val generator = RepoLinkGenerator(listOf(epData)) + val isSuccessful = safeApiCall { generator.generateLinks( clearCache = false, @@ -292,7 +335,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi currentSubs.add(it) }, offset = 0, - isCasting = true, + isCasting = true ) } @@ -303,18 +346,32 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi val jsonCopy = meta.copy( currentLinks = sortedLinks, currentSubtitles = sortedSubs, - currentEpisodeIndex = index, + currentEpisodeIndex = index ) - val done = JSONObject(jsonCopy.toJson()) + val done = + JSONObject(jsonCopy.toJson()) + val mediaInfo = getMediaInfo( epData, jsonCopy, 0, done, - sortedSubs, + sortedSubs ) + /*fun loadIndex(index: Int) { + println("LOAD INDEX::::: $index") + if (meta.currentLinks.size <= index) return + val info = getMediaInfo( + epData, + meta, + index, + done) + awaitLinks(remoteMediaClient?.load(info, true, 0)) { + loadIndex(index + 1) + } + }*/ activity.runOnUiThread { awaitLinks( remoteMediaClient?.queueAppendItem( @@ -323,6 +380,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi ) ) { println("FAILED TO LOAD NEXT ITEM") + // loadIndex(1) } isLoadingMore = false } @@ -345,7 +403,10 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi class SkipTimeController(val view: ImageView, forwards: Boolean) : UIController() { init { + //val settingsManager = PreferenceManager.getDefaultSharedPreferences() + //val time = settingsManager?.getInt("chromecast_tap_time", 30) ?: 30 val time = 30 + //view.setImageResource(if (forwards) R.drawable.netflix_skip_forward else R.drawable.netflix_skip_back) view.setImageResource(if (forwards) R.drawable.go_forward_30 else R.drawable.go_back_30) view.setOnClickListener { remoteMediaClient?.let { @@ -388,4 +449,4 @@ class ControllerActivity : ExpandedControllerActivity() { SkipNextEpisodeController(skipOpButton) ) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt index f70458285..92d33d0f3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt @@ -1,8 +1,11 @@ package com.lagradost.cloudstream3.ui.account +import android.os.Build import android.view.LayoutInflater import android.view.ViewGroup +import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import coil3.transform.RoundedCornersTransformation import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.AccountListItemAddBinding import com.lagradost.cloudstream3.databinding.AccountListItemBinding @@ -15,8 +18,6 @@ import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.ImageLoader.loadImage -import android.util.TypedValue -import android.view.View class AccountAdapter( private val accountSelectCallback: (DataStoreHelper.Account) -> Unit, @@ -28,12 +29,6 @@ class AccountAdapter( companion object { const val VIEW_TYPE_SELECT_ACCOUNT = 0 const val VIEW_TYPE_EDIT_ACCOUNT = 2 - - fun View.setRippleForeground() { - val outValue = TypedValue() - context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, outValue, true) - foreground = context.getDrawable(outValue.resourceId) - } } @@ -52,25 +47,45 @@ class AccountAdapter( when (val binding = holder.view) { is AccountListItemBinding -> binding.apply { val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode + val isLastUsedAccount = item.keyIndex == DataStoreHelper.selectedKeyIndex accountName.text = item.name accountImage.loadImage(item.image) lockIcon.isVisible = item.lockPin != null - root.isSelected = isLastUsedAccount - cardView.isSelected = isLastUsedAccount + outline.isVisible = !isTv && isLastUsedAccount - if (!isTv) cardView.setRippleForeground() + if (isTv) { + // For emulator but this is fine on TV also + root.isFocusableInTouchMode = true + if (isLastUsedAccount) { + root.requestFocus() + } - if (!isTv) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + root.foreground = ContextCompat.getDrawable( + root.context, + R.drawable.outline_drawable + ) + } + } else { root.setOnLongClickListener { showAccountEditDialog( context = root.context, account = item, isNewAccount = false, - accountEditCallback = { account -> accountEditCallback.invoke(account) }, - accountDeleteCallback = { account -> accountDeleteCallback.invoke(account) } + accountEditCallback = { account -> + accountEditCallback.invoke( + account + ) + }, + accountDeleteCallback = { account -> + accountDeleteCallback.invoke( + account + ) + } ) + true } } @@ -82,15 +97,30 @@ class AccountAdapter( is AccountListItemEditBinding -> binding.apply { val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode + val isLastUsedAccount = item.keyIndex == DataStoreHelper.selectedKeyIndex accountName.text = item.name - accountImage.loadImage(item.image) + accountImage.loadImage(item.image) { + RoundedCornersTransformation(10f) + } lockIcon.isVisible = item.lockPin != null - root.isSelected = isLastUsedAccount - cardView.isSelected = isLastUsedAccount + outline.isVisible = !isTv && isLastUsedAccount - if (!isTv) cardView.setRippleForeground() + if (isTv) { + // For emulator but this is fine on TV also + root.isFocusableInTouchMode = true + if (isLastUsedAccount) { + root.requestFocus() + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + root.foreground = ContextCompat.getDrawable( + root.context, + R.drawable.outline_drawable + ) + } + } root.setOnClickListener { showAccountEditDialog( @@ -98,7 +128,11 @@ class AccountAdapter( account = item, isNewAccount = false, accountEditCallback = { account -> accountEditCallback.invoke(account) }, - accountDeleteCallback = { account -> accountDeleteCallback.invoke(account) } + accountDeleteCallback = { account -> + accountDeleteCallback.invoke( + account + ) + } ) } } @@ -108,9 +142,6 @@ class AccountAdapter( override fun onBindFooter(holder: ViewHolderState) { val binding = holder.view as? AccountListItemAddBinding ?: return binding.apply { - val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode - if (!isTv) cardView.setRippleForeground() - root.setOnClickListener { val accounts = this@AccountAdapter.immutableCurrentList diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt index 69881702d..1d6b41e5b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt @@ -37,14 +37,8 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount import com.lagradost.cloudstream3.utils.ImageLoader.loadImage import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import com.lagradost.cloudstream3.utils.UIHelper.hideProgress import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod -import com.lagradost.cloudstream3.utils.UIHelper.showProgress -import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.setRippleForeground -import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR -import com.lagradost.cloudstream3.ui.settings.Globals.TV -import com.lagradost.cloudstream3.ui.settings.Globals.isLayout object AccountHelper { fun showAccountEditDialog( @@ -60,12 +54,6 @@ object AccountHelper { var currentEditAccount = account val dialog = builder.show() - binding.accountImageHolder.requestFocus() - - if (!isLayout(TV or EMULATOR)) { - binding.accountImageHolder.setRippleForeground() - binding.editProfilePhotoButtonHolder.setRippleForeground() - } if (!isNewAccount) binding.title.setText(R.string.edit_account) @@ -110,7 +98,7 @@ object AccountHelper { // Handle the profile picture and its interactions binding.accountImage.loadImage(account.image) - binding.accountImageHolder.setOnClickListener { + binding.accountImage.setOnClickListener { // Roll the image forwards once currentEditAccount = currentEditAccount.copy(customImage = null) currentEditAccount = @@ -176,57 +164,52 @@ object AccountHelper { canSetPin = true - binding.editProfilePhotoButtonHolder.setOnClickListener { - val builder = AlertDialog.Builder(context, R.style.AlertDialogCustom) + binding.editProfilePhotoButton.setOnClickListener({ + val bottomSheetDialog = BottomSheetDialog(context) val sheetBinding = BottomInputDialogBinding.inflate(LayoutInflater.from(context)) - builder.setView(sheetBinding.root) - val dialog = builder.create() - dialog.show() + bottomSheetDialog.setContentView(sheetBinding.root) + bottomSheetDialog.show() sheetBinding.apply { text1.text = context.getString(R.string.edit_profile_image_title) nginxTextInput.hint = context.getString(R.string.edit_profile_image_hint) - applyBtt.setOnClickListener { + applyBtt.setOnClickListener({ val url = sheetBinding.nginxTextInput.text.toString() - if (url.isEmpty()) { + if (url.isNotEmpty()) { + val imageLoader = ImageLoader(context) + val request = ImageRequest.Builder(context) + .data(url) + .allowHardware(false) + .listener( + onSuccess = { _, _ -> + currentEditAccount = currentEditAccount.copy(customImage = url) + binding.accountImage.loadImage(url) + showToast( + R.string.edit_profile_image_success, + Toast.LENGTH_SHORT + ) + bottomSheetDialog.dismiss() + }, + onError = { _, _ -> + showToast( + R.string.edit_profile_image_error_invalid, + Toast.LENGTH_SHORT + ) + } + ) + .build() + imageLoader.enqueue(request) + } else { showToast(R.string.edit_profile_image_error_empty, Toast.LENGTH_SHORT) - return@setOnClickListener } - applyBtt.showProgress() - val imageLoader = ImageLoader(context) - val request = ImageRequest.Builder(context) - .data(url) - .allowHardware(false) - .listener( - onSuccess = { _, _ -> - currentEditAccount = currentEditAccount.copy(customImage = url) - binding.accountImage.loadImage(url) - showToast( - R.string.edit_profile_image_success, - Toast.LENGTH_SHORT - ) - dialog.dismissSafe() - }, - onError = { _, _ -> - showToast( - R.string.edit_profile_image_error_invalid, - Toast.LENGTH_SHORT - ) - applyBtt.hideProgress() - }, - onCancel = { - applyBtt.hideProgress() - } - ) - .build() - imageLoader.enqueue(request) - } - sheetBinding.cancelBtt.setOnClickListener { - dialog.dismissSafe() - } + bottomSheetDialog.dismissSafe() + }) + sheetBinding.cancelBtt.setOnClickListener({ + bottomSheetDialog.dismissSafe() + }) } - } + }) } fun showPinInputDialog( @@ -368,11 +351,6 @@ object AccountHelper { fun Activity?.showAccountSelectLinear() { val activity = this as? MainActivity ?: return - activity.navigate( - R.id.accountSelectActivity, - Bundle().apply { putBoolean(AccountSelectActivity2.IS_FROM_MAIN_ACTIVITY, true) } - ) - /* val viewModel = ViewModelProvider(activity)[AccountViewModel::class.java] val binding: AccountSelectLinearBinding = AccountSelectLinearBinding.inflate( @@ -430,6 +408,6 @@ object AccountHelper { val layoutManager = recyclerView.layoutManager as LinearLayoutManager layoutManager.scrollToPositionWithOffset(selectedKeyIndex, 0) } - }*/ + } } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountScreen.kt deleted file mode 100644 index 056d3224e..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountScreen.kt +++ /dev/null @@ -1,872 +0,0 @@ -package com.lagradost.cloudstream3.ui.account - -import android.content.Intent -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.focusGroup -import androidx.compose.foundation.indication -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.FlowRow -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 -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.ripple -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2 -import androidx.compose.ui.focus.focusProperties -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.PreviewLightDark -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties -import coil3.compose.AsyncImage -import com.lagradost.cloudstream3.CloudStreamApp -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.DataStoreHelper -import com.lagradost.cloudstream3.utils.DataStoreHelper.Account -import com.lagradost.cloudstream4.compose.ActionDialog -import com.lagradost.cloudstream4.compose.BlackButton -import com.lagradost.cloudstream4.compose.BlackTextField -import com.lagradost.cloudstream4.compose.EMULATOR -import com.lagradost.cloudstream4.compose.PHONE -import com.lagradost.cloudstream4.compose.RoundedShape -import com.lagradost.cloudstream4.compose.TV -import com.lagradost.cloudstream4.compose.WhiteButton -import com.lagradost.cloudstream4.compose.circle -import com.lagradost.cloudstream4.compose.circleBorder -import com.lagradost.cloudstream4.compose.focusOutline -import com.lagradost.cloudstream4.compose.isLayout -import com.lagradost.cloudstream4.compose.rounded -import com.lagradost.cloudstream4.compose.whiteOutline -import com.lagradost.cloudstream4.state.SearchableData -import com.lagradost.cloudstream4.state.sortByInt -import com.lagradost.cloudstream4.theme.CloudStreamPreviewTheme -import com.mihon.material.padding -import com.mihon.presentation.settings.widget.SwitchPreferenceWidget -import com.mihon.presentation.settings.widget.TextPreferenceWidget -import kotlinx.collections.immutable.toPersistentMap - -@PreviewLightDark -@Composable -fun Preview() { - CloudStreamPreviewTheme { - AccountScreen.Content( - state = AccountState( - canEditAccount = false, - editAccount = Account( - keyIndex = 1, - name = "Hello world", - defaultImageIndex = 2, - lockPin = "1234" - ), - accounts = SearchableData.from( - data = - (0..7).associateWith { index -> - Account( - keyIndex = index, - name = "hello $index", - defaultImageIndex = index, - lockPin = if (index % 2 == 1) { - "1234" - } else { - null - } - ) - }.toPersistentMap(), - sortedBy = sortByInt { it.keyIndex }, - ), - ), onAction = {}) - } -} - -object AccountScreen { - @Composable - fun Content( - state: AccountState, - onAction: (AccountAction) -> Unit - ) { - if (state.editAccount != null) { - EditAccountDialog( - exists = state.accounts.data.containsKey(state.editAccount.keyIndex), - account = state.editAccount, - onAction = onAction - ) - } - - Scaffold { contentPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(contentPadding), - horizontalAlignment = Alignment.End - ) { - val (edit, accounts) = remember { FocusRequester.createRefs() } - - IconButton( - onClick = { - onAction(AccountAction.SetEditMode(!state.canEditAccount)) - }, modifier = Modifier - .focusRequester(edit) - .focusProperties { - down = accounts - } - .focusOutline(shape = CircleShape) - ) { - Icon( - painter = if (state.canEditAccount) { - painterResource(R.drawable.close_24px) - } else { - painterResource(R.drawable.edit_24px) - }, - contentDescription = stringResource(R.string.manage_accounts) - ) - } - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = if (state.canEditAccount) stringResource(R.string.manage_accounts) else stringResource( - R.string.select_an_account - ), - style = MaterialTheme.typography.headlineMedium, - textAlign = TextAlign.Center, - modifier = Modifier.padding(MaterialTheme.padding.large) - ) - - AccountsRow( - state = state, onAction = onAction, - modifier = Modifier - .focusRequester(accounts) - .focusProperties { - up = edit - }) - - Spacer(modifier = Modifier.height(100.dp)) - } - } - } - } - - enum class WaitForDialogType { - RemoveLock, - NewPassword, - DeleteAccount, - NewProfileUrl, - NewProfilePicture, - Login, - } - - @Composable - fun WaitForDialog( - type: WaitForDialogType, - onDismissRequest: () -> Unit, - account: Account, - onAction: (AccountAction) -> Unit - ) { - when (type) { - WaitForDialogType.RemoveLock -> { - if (account.lockPin != null) { - EnterPinDialog( - expected = account.lockPin, - onDismissRequest = onDismissRequest, - onCorrect = { - onAction(AccountAction.EditAccount(account.copy(lockPin = null))) - } - ) - } - } - - WaitForDialogType.Login -> { - if (account.lockPin != null) { - EnterPinDialog( - expected = account.lockPin, - onDismissRequest = onDismissRequest, - onCorrect = { - onAction(AccountAction.SaveAccount(account)) - onAction(AccountAction.EditAccount(null)) - } - ) - } - } - - WaitForDialogType.NewPassword -> { - SetPinDialog( - onDismissRequest = onDismissRequest, - onSet = { newLockPin -> - onAction(AccountAction.EditAccount(account.copy(lockPin = newLockPin))) - }) - } - - WaitForDialogType.DeleteAccount -> { - ActionDialog( - title = stringResource(R.string.delete), - text = stringResource(R.string.delete_message, account.name), - confirm = { - onAction(AccountAction.DeleteAccount(account.keyIndex)) - onAction(AccountAction.EditAccount(null)) - onDismissRequest() - }, - dismiss = onDismissRequest, - confirmText = stringResource(R.string.delete), - dismissText = stringResource(R.string.cancel) - ) - } - - WaitForDialogType.NewProfilePicture -> { - SetPictureDialog( - account = account, - onDismissRequest = onDismissRequest, - onSet = { newImage -> - onAction( - AccountAction.EditAccount( - account.copy( - defaultImageIndex = newImage, - customImage = null - ) - ) - ) - onDismissRequest() - }) - } - - WaitForDialogType.NewProfileUrl -> { - SetUrlDialog(onDismissRequest = onDismissRequest, onSet = { newImage -> - onAction(AccountAction.EditAccount(account.copy(customImage = newImage))) - onDismissRequest() - }) - } - } - } - - @Composable - fun EditAccountDialog( - exists: Boolean, - account: Account, - onAction: (AccountAction) -> Unit - ) { - val selectFileSelector = - rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> - // It lies, it can be null if file manager quits. - if (uri == null) return@rememberLauncherForActivityResult - val context = CloudStreamApp.context ?: return@rememberLauncherForActivityResult - - try { - context.contentResolver.takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION - ) - onAction(AccountAction.EditAccount(account = account.copy(customImage = uri.toString()))) - } catch (t: Throwable) { - logError(t) - } - } - - var pendingDialog by remember { mutableStateOf(null) } - pendingDialog?.let { waitType -> - WaitForDialog( - type = waitType, - onDismissRequest = { pendingDialog = null }, - account = account, - onAction = onAction - ) - } - - val focusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } - - val isTv = isLayout(TV) - AlertDialog( - containerColor = MaterialTheme.colorScheme.background, - confirmButton = { - if (exists) { - WhiteButton(text = stringResource(R.string.delete)) { - pendingDialog = WaitForDialogType.DeleteAccount - } - } - WhiteButton(text = stringResource(R.string.sort_apply)) { - if (account.lockPin == null) { - onAction(AccountAction.SaveAccount(account)) - onAction(AccountAction.EditAccount(null)) - } else { - pendingDialog = WaitForDialogType.Login - } - } - }, - dismissButton = { - BlackButton( - text = stringResource(R.string.cancel), - modifier = Modifier.focusRequester(focusRequester) - ) { - onAction(AccountAction.EditAccount(null)) - } - }, - onDismissRequest = { - /** - * For UX reasons we can only dismiss it with the cancel button, - * but does not have those issues - * */ - if (isTv) { - onAction(AccountAction.EditAccount(null)) - } - }, - text = { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.verticalScroll(rememberScrollState()) - ) { - AccountImage( - selected = false, - edit = false, - account = account, - onClick = { - onAction( - AccountAction.EditAccount( - account.copy( - customImage = null, - defaultImageIndex = (account.defaultImageIndex + 1) % DataStoreHelper.profileImages.size - ) - ) - ) - }, - onLongClick = {}) - Spacer(modifier = Modifier.height(MaterialTheme.padding.medium)) - - BlackTextField( - modifier = Modifier.fillMaxWidth(), - value = account.name, - onValueChange = { newText -> - val newName = if (newText.length <= 100) { - newText - } else { - newText.substring(0, 100) - } - onAction( - AccountAction.EditAccount( - account.copy( - name = newName - ) - ) - ) - }, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.PersonName, - ), - placeHolder = stringResource(R.string.name) - ) - - var selectProfileImage by remember { mutableStateOf(false) } - - - TextPreferenceWidget( - modifier = Modifier - .rounded() - .focusOutline(), - title = stringResource(R.string.edit_profile_image_title), - widget = { - Icon( - painter = painterResource(R.drawable.add_photo_alternate_24px), - contentDescription = null - ) - DropdownMenu(expanded = selectProfileImage, onDismissRequest = { - selectProfileImage = false - }, shape = RoundedShape()) { - DropdownMenuItem( - text = { Text(text = stringResource(R.string.edit_profile_image_url)) }, - onClick = { - pendingDialog = WaitForDialogType.NewProfileUrl - selectProfileImage = false - } - ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.edit_profile_image_picker)) }, - onClick = { - selectFileSelector.launch(arrayOf("image/*")) - selectProfileImage = false - } - ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.edit_profile_image_default)) }, - onClick = { - pendingDialog = WaitForDialogType.NewProfilePicture - selectProfileImage = false - } - ) - } - }, - onPreferenceClick = { - selectProfileImage = true - } - ) - - if (account.keyIndex != 0) { - SwitchPreferenceWidget( - modifier = Modifier - .rounded() - .focusOutline(), - title = stringResource(R.string.lock_profile), - checked = account.lockPin != null, - onCheckedChanged = { - pendingDialog = if (account.lockPin == null) { - WaitForDialogType.NewPassword - } else { - WaitForDialogType.RemoveLock - } - }) - } - - } - } - ) - } - - - @Composable - fun AccountsRow( - state: AccountState, - onAction: (AccountAction) -> Unit, - modifier: Modifier = Modifier, - ) { - FlowRow( - modifier = modifier.focusGroup(), - horizontalArrangement = Arrangement.spacedBy( - MaterialTheme.padding.medium, - alignment = Alignment.Start - ), - verticalArrangement = Arrangement.spacedBy( - MaterialTheme.padding.medium, - alignment = Alignment.CenterVertically - ), - ) { - state.accounts.sorted.forEach { key -> - val account = state.accounts.data[key]!! - Account( - edit = state.canEditAccount, - account = account, - onAction = onAction, - selected = state.lastLoginKeyIndex == account.keyIndex - ) - } - - var hasFocus by remember { mutableStateOf(false) } - val size = 100.dp - val innerPadding by animateDpAsState( - targetValue = if (hasFocus || isLayout(PHONE or EMULATOR)) 0.dp else MaterialTheme.padding.extraSmall, - ) - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .width(size) - .onFocusChanged { newFocus -> - hasFocus = newFocus.hasFocus - } - ) { - val interactionSource = remember { MutableInteractionSource() } - Box( - modifier = Modifier - .clickable(interactionSource = interactionSource, indication = null) { - onAction(AccountAction.NewAccount) - } - .size(size) - .padding(innerPadding) - .border( - 2.dp, - MaterialTheme.colorScheme.onBackground.copy(alpha = 0.2f), - CircleShape - ) - .circle() - .background(MaterialTheme.colorScheme.surfaceVariant) - .whiteOutline(hasFocus = hasFocus, shape = CircleShape) - .indication( - interactionSource = interactionSource, - indication = ripple() - ), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(R.drawable.add_24px), - contentDescription = stringResource(R.string.add_account), - tint = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.size(36.dp) - ) - } - Spacer(Modifier.height(MaterialTheme.padding.small)) - Text( - text = stringResource(R.string.add_account), - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - ) - } - } - } - - @Composable - fun PinTextField(value: String, onValueChange: (String) -> Unit) { - BlackTextField( - value = value, keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Done, - autoCorrectEnabled = false, - keyboardType = KeyboardType.NumberPassword, - ), placeHolder = stringResource(R.string.pin), onValueChange = onValueChange - ) - } - - @Composable - fun EnterPinDialog(expected: String, onDismissRequest: (() -> Unit), onCorrect: (() -> Unit)) { - AlertDialog( - containerColor = MaterialTheme.colorScheme.background, - onDismissRequest = onDismissRequest, - confirmButton = {}, - dismissButton = { - BlackButton(text = stringResource(R.string.cancel), onClick = onDismissRequest) - }, - text = { - var inputText by remember { mutableStateOf("") } - var hasError by remember { mutableStateOf(false) } - Column { - PinTextField( - value = inputText, - onValueChange = { newText -> - if (newText.length == expected.length) { - if (newText == expected) { - onCorrect() - onDismissRequest() - } else { - inputText = "" - hasError = true - } - } else { - inputText = newText - } - }, - ) - Spacer(modifier = Modifier.height(MaterialTheme.padding.medium)) - if (hasError) { - Text(text = stringResource(R.string.pin_error_incorrect)) - } - } - }, - title = { - Text(text = stringResource(R.string.enter_pin)) - }) - } - - @Composable - fun SetPinDialog(onDismissRequest: (() -> Unit), onSet: ((String) -> Unit)) { - var inputText by remember { mutableStateOf("") } - AlertDialog( - containerColor = MaterialTheme.colorScheme.background, - onDismissRequest = onDismissRequest, - confirmButton = { - if (inputText.length == 4) { - WhiteButton( - text = stringResource(R.string.sort_apply), - onClick = { - onSet(inputText) - onDismissRequest() - }) - } - }, - dismissButton = { - BlackButton(text = stringResource(R.string.cancel), onClick = onDismissRequest) - }, - text = { - PinTextField( - value = inputText, - onValueChange = { newText -> - /** - * We require isDigit for SetPin, - * but do not for the EnterPin to avoid inaccessable accounts - * */ - val newTextFilter = if (newText.all(Char::isDigit)) { - newText - } else { - newText.filter(Char::isDigit) - } - - inputText = if (newTextFilter.length <= 4) { - newTextFilter - } else { - newTextFilter.substring(0, 4) - } - }, - ) - }, - title = { - Text(text = stringResource(R.string.enter_pin)) - }) - } - - @Composable - fun SetUrlDialog(onDismissRequest: (() -> Unit), onSet: ((String) -> Unit)) { - var inputText by remember { mutableStateOf("") } - AlertDialog( - containerColor = MaterialTheme.colorScheme.background, - onDismissRequest = onDismissRequest, - confirmButton = { - WhiteButton( - text = stringResource(R.string.sort_apply), - onClick = { - onSet(inputText) - onDismissRequest() - }) - }, - dismissButton = { - BlackButton(text = stringResource(R.string.cancel), onClick = onDismissRequest) - }, - text = { - BlackTextField( - value = inputText, - onValueChange = { newText -> - inputText = newText - }, - placeHolder = stringResource(R.string.edit_profile_image_hint) - ) - }, - title = { - Text(text = stringResource(R.string.edit_profile_image_title)) - }) - } - - @Composable - fun SetPictureDialog( - account: Account, - onDismissRequest: (() -> Unit), - onSet: ((Int) -> Unit) - ) { - AlertDialog( - properties = DialogProperties(usePlatformDefaultWidth = false), - containerColor = MaterialTheme.colorScheme.background, - onDismissRequest = onDismissRequest, - confirmButton = { }, - dismissButton = { - BlackButton(text = stringResource(R.string.cancel), onClick = onDismissRequest) - }, - text = { - FlowRow( - modifier = Modifier.focusGroup(), - horizontalArrangement = Arrangement.spacedBy( - MaterialTheme.padding.medium, - alignment = Alignment.Start - ), - verticalArrangement = Arrangement.spacedBy( - MaterialTheme.padding.medium, - alignment = Alignment.CenterVertically - ), - ) { - for (i in DataStoreHelper.profileImages.indices) { - AccountImage( - edit = false, - showName = false, - selected = account.defaultImageIndex == i, - account = account.copy( - customImage = null, - defaultImageIndex = i, - lockPin = null - ), - onClick = { - onSet(i) - }, - onLongClick = {}) - } - } - }, - title = { - Text(text = stringResource(R.string.edit_profile_image_title)) - }) - } - - @Composable - fun AccountImage( - /** If we should show the edit pen */ - edit: Boolean, - /** If this element should have focus */ - selected: Boolean, - /** If we should show the name under the picture */ - showName: Boolean = true, - account: Account, - onClick: () -> Unit, - onLongClick: () -> Unit - ) { - val image = account.customImage - ?: DataStoreHelper.profileImages[account.defaultImageIndex % DataStoreHelper.profileImages.size] - - val interactionSource = remember { MutableInteractionSource() } - var hasFocus by remember { mutableStateOf(false) } - val size = 100.dp - val innerPadding by animateDpAsState( - targetValue = if (hasFocus || isLayout(PHONE or EMULATOR)) 0.dp else MaterialTheme.padding.extraSmall, - ) - val focusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { - if (selected) { - focusRequester.requestFocus() - } - } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .width(size) - .focusRequester(focusRequester) - .onFocusChanged { newFocus -> - hasFocus = newFocus.hasFocus - } - ) { - Box( - modifier = Modifier - .size(size) - .combinedClickable( - interactionSource = interactionSource, - indication = null, - onClick = onClick, - onLongClick = onLongClick - ) - .padding(innerPadding), - contentAlignment = Alignment.Center - ) { - AsyncImage( - model = image, - modifier = Modifier - .circleBorder(size) - .whiteOutline(hasFocus = hasFocus, shape = CircleShape) - .indication(interactionSource = interactionSource, indication = ripple()), - contentDescription = account.name, - contentScale = ContentScale.Crop, - alpha = if (edit) { - 0.6f - } else { - 1.0f - } - ) - - if (account.lockPin != null) { - Box( - contentAlignment = Alignment.TopEnd, modifier = Modifier - .fillMaxSize() - .padding( - MaterialTheme.padding.extraSmall - ) - ) { - Icon( - painter = painterResource(R.drawable.video_locked), - contentDescription = null, - tint = MaterialTheme.colorScheme.onBackground, - modifier = Modifier - .size(22.dp + MaterialTheme.padding.extraSmall * 2) - .background( - MaterialTheme.colorScheme.surfaceVariant, - shape = CircleShape - ) - .padding(MaterialTheme.padding.extraSmall) - ) - } - } - - this@Column.AnimatedVisibility( - visible = edit, - enter = fadeIn(), - exit = fadeOut() - ) { - Icon( - painter = painterResource(R.drawable.edit_24px), - contentDescription = null, - tint = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.size(36.dp), - ) - } - } - if (showName) { - Spacer(Modifier.height(MaterialTheme.padding.small)) - Text( - text = account.name, - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - } - - } - } - - @Composable - fun Account( - edit: Boolean, - selected: Boolean, - account: Account, - onAction: (AccountAction) -> Unit - ) { - var waitForLock by remember { mutableStateOf(false) } - if (waitForLock && account.lockPin != null) { - EnterPinDialog( - expected = account.lockPin, - onDismissRequest = { - waitForLock = false - }, onCorrect = { - onAction(AccountAction.LoginWithAccount(account.keyIndex)) - }) - } - - AccountImage(selected = selected, edit = edit, account = account, onClick = { - if (edit) { - onAction(AccountAction.EditAccount(account)) - } else if (account.lockPin != null) { - waitForLock = true - } else { - onAction(AccountAction.LoginWithAccount(account.keyIndex)) - } - }, onLongClick = { - /** Long click -> edit without having to press the edit button for better UX */ - onAction(AccountAction.EditAccount(account)) - }) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt index 28f5c15d8..ad323c7d1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -54,15 +54,10 @@ class AccountSelectActivity : FragmentActivity(), BiometricCallback { false ) - val isFromMainActivity = intent.getBooleanExtra( - "isFromMainActivity", - false - ) - // Sometimes we start this activity when we have already logged in // For example when using cloudstreamsearch:// // In those cases we want to just go to the main activity instantly - if (hasLoggedIn && !isEditingFromMainActivity && !isFromMainActivity) { + if (hasLoggedIn && !isEditingFromMainActivity) { navigateToMainActivity() return } @@ -103,7 +98,7 @@ class AccountSelectActivity : FragmentActivity(), BiometricCallback { // Don't show account selection if there is only // one account that exists - if (!isFromMainActivity && !isEditingFromMainActivity && skipStartup) { + if (!isEditingFromMainActivity && skipStartup) { val currentAccount = accounts.firstOrNull { it.keyIndex == selectedKeyIndex } if (currentAccount?.lockPin != null) { CommonActivity.init(this) @@ -165,11 +160,6 @@ class AccountSelectActivity : FragmentActivity(), BiometricCallback { // Scroll to current account (which is focused by default) val layoutManager = recyclerView.layoutManager as GridLayoutManager layoutManager.scrollToPositionWithOffset(selectedKeyIndex, 0) - if (isLayout(TV or EMULATOR)) { - recyclerView.post { - layoutManager.findViewByPosition(selectedKeyIndex)?.requestFocus() - } - } } observe(accountViewModel.isEditing) { isEditing -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity2.kt deleted file mode 100644 index 61052f4e7..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity2.kt +++ /dev/null @@ -1,180 +0,0 @@ -package com.lagradost.cloudstream3.ui.account - -import android.annotation.SuppressLint -import android.os.Bundle -import android.util.Log -import androidx.activity.compose.setContent -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.viewmodel.compose.viewModel -import com.lagradost.cloudstream3.CommonActivity -import com.lagradost.cloudstream3.CommonActivity.loadThemes -import com.lagradost.cloudstream3.MainActivity -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.ui.settings.Globals.PHONE -import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.BiometricAuthenticator -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication -import com.lagradost.cloudstream3.utils.DataStoreHelper -import com.lagradost.cloudstream3.utils.DataStoreHelper.Account -import com.lagradost.cloudstream3.utils.UIHelper.enableEdgeToEdgeCompat -import com.lagradost.cloudstream3.utils.UIHelper.openActivity -import com.lagradost.cloudstream3.utils.UIHelper.setNavigationBarColorCompat -import com.lagradost.cloudstream4.AppSettings -import com.lagradost.cloudstream4.compose.CloudStreamThemeAppSettings -import com.lagradost.cloudstream4.state.SearchableData -import com.lagradost.cloudstream4.state.sortByInt -import kotlinx.collections.immutable.persistentMapOf - -class AccountSelectActivity2 : FragmentActivity(), BiometricCallback { - companion object { - var hasLoggedIn: Boolean = false - const val IS_FROM_MAIN_ACTIVITY = "isFromMainActivity" - const val IS_EDITING_FROM_MAIN_ACTIVITY = "isEditingFromMainActivity" - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // Are we editing and coming from MainActivity? - val isEditingFromMainActivity = intent.getBooleanExtra(IS_EDITING_FROM_MAIN_ACTIVITY, false) - - // Are we invoked from the main activity to edit it? - val isFromMainActivity = intent.getBooleanExtra(IS_FROM_MAIN_ACTIVITY, false) - - /** - * Sometimes we start this activity when we have already logged in - * For example when using cloudstreamsearch:// - * - * In those cases we want to just go to the main activity instantly - **/ - if (hasLoggedIn && !isEditingFromMainActivity && !isFromMainActivity) { - navigateToMainActivity() - return - } - - /** - * Here we set the themes, this *can* be deferred for faster instant login, - * but has some consequences that makes it undesirable even if it is slower - * - * CommonActivity.init : loads the correct locale for default_account - * loadThemes : loads the correct color on the toast when logging in - * */ - CommonActivity.init(this) - loadThemes(this) - enableEdgeToEdgeCompat() - setNavigationBarColorCompat(R.attr.primaryBlackBackground) - - /** - * To avoid touching the UI layer, we use the raw state before the viewmodel is created - * to enable us to skip account selection but still use the ergonomics of AccountState - * */ - val defaultName = getString(R.string.default_account) - val defaultAccount = Account( - keyIndex = 0, - name = defaultName, - defaultImageIndex = 0, - ) - val initialState = AccountState( - canEditAccount = isEditingFromMainActivity, - lastLoginKeyIndex = DataStoreHelper.selectedKeyIndex, - accounts = SearchableData.from( - /** We must always have a default account - * even if DataStoreHelper.accounts.size = 0 */ - data = persistentMapOf(defaultAccount.keyIndex to defaultAccount) - .puttingAll(DataStoreHelper.accounts.associateBy { it.keyIndex }), - sortedBy = sortByInt { it.keyIndex } - ) - ) - - val settings = AppSettings(this) - val skipAccountSelection = settings.security.skipAccountSelection.get() - - /** Don't show account selection if there is only one account that exists */ - if ( - !isFromMainActivity && - !isEditingFromMainActivity && - (skipAccountSelection || initialState.accounts.data.size == 1) - ) { - /** We must have an account without a pin */ - val currentAccount = initialState.accounts.data[initialState.lastLoginKeyIndex] - if ( - currentAccount != null && - currentAccount.lockPin == null - ) { - /** - * Show toast if we have many accounts, - * as it is unnecessary for 1 account - * */ - DataStoreHelper.setAccount( - currentAccount, - showToast = initialState.accounts.data.size > 1 - ) - /** - * We avoid using the viewmodel Login as we - * can return *before* we load Compose code! - * */ - navigateToMainActivity() - return - } - } - - /** - * If this app is locked with biometrics, - * then require that on a startup that is not skipped! - * */ - if ( - !isFromMainActivity && - !isEditingFromMainActivity && - isLayout(PHONE) && - settings.security.biometrics.get() && - deviceHasPasswordPinLock(this) - ) { - startBiometricAuthentication( - this, - R.string.biometric_authentication_title, - false - ) - promptInfo?.let { prompt -> - biometricPrompt?.authenticate(prompt) - } - } - - setContent { - val viewModel = viewModel { - AccountViewModel2( - defaultAccount = defaultAccount, - initialState = initialState - ) - } - val state by viewModel.state.collectAsState() - if (state.loggedInWith != null) { - navigateToMainActivity() - } - CloudStreamThemeAppSettings { - AccountScreen.Content(state, viewModel::onAction) - } - } - } - - @SuppressLint("UnsafeIntentLaunch") - private fun navigateToMainActivity() { - hasLoggedIn = true - // We want to propagate any intent we get here to MainActivity since this is just an intermediary - openActivity(MainActivity::class.java, baseIntent = intent) - finish() // Finish the account selection activity - } - - override fun onAuthenticationSuccess() { - Log.i(BiometricAuthenticator.TAG, "Authentication successful in AccountSelectActivity") - } - - override fun onAuthenticationError() { - finish() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectLinearItemDecoration.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectLinearItemDecoration.kt index 91e0caa2a..eb907b344 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectLinearItemDecoration.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectLinearItemDecoration.kt @@ -8,7 +8,7 @@ class AccountSelectLinearItemDecoration(private val size: Int) : RecyclerView.It override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { val layoutParams = view.layoutParams as RecyclerView.LayoutParams layoutParams.width = size - layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT + layoutParams.height = size view.layoutParams = layoutParams } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountViewModel2.kt deleted file mode 100644 index 62a92e42f..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountViewModel2.kt +++ /dev/null @@ -1,141 +0,0 @@ -package com.lagradost.cloudstream3.ui.account - -import androidx.compose.runtime.Immutable -import androidx.lifecycle.ViewModel -import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKeys -import com.lagradost.cloudstream3.MainActivity -import com.lagradost.cloudstream3.utils.DataStoreHelper -import com.lagradost.cloudstream4.state.ActionHandler -import com.lagradost.cloudstream4.state.DefaultStateContainer -import com.lagradost.cloudstream4.state.SearchableData -import com.lagradost.cloudstream4.state.StateContainer -import com.lagradost.cloudstream4.state.sortByInt -import kotlinx.collections.immutable.persistentMapOf -import kotlin.random.Random -import kotlin.random.nextInt - -@Immutable -data class AccountState( - /** Mostly used for focus stuff on TV */ - val lastLoginKeyIndex : Int = 0, - - /** Is currently open to editing */ - val canEditAccount: Boolean = false, - - /** keyIndex -> DataStoreHelper.Account sorted by keyIndex as keyIndex is unique and in add order */ - val accounts: SearchableData = SearchableData.from( - data = persistentMapOf(), - /** This will cause bugs if changed! */ - sortedBy = sortByInt { it.keyIndex } - ), - - /** - * We are currectly editing this account with this value, it may be an existing/new account - * non-null = editing, null = not editing - * */ - val editAccount: DataStoreHelper.Account? = null, - - /** We are currently logged in with this account, we use state instead of effect to prevent bugs */ - val loggedInWith: Int? = null -) - -@Immutable -sealed class AccountAction { - data class SetEditMode(val canEditAccount: Boolean) : AccountAction() - object NewAccount : AccountAction() - data class EditAccount(val account: DataStoreHelper.Account?) : AccountAction() - data class SaveAccount(val account: DataStoreHelper.Account) : AccountAction() - data class LoginWithAccount(val accountKeyIndex: Int) : AccountAction() - data class DeleteAccount(val accountKeyIndex: Int) : AccountAction() -} - -class AccountViewModel2( - val defaultAccount: DataStoreHelper.Account, - initialState : AccountState, -) : ViewModel(), StateContainer by DefaultStateContainer(initialState), ActionHandler { - fun updateAccount( - updater: (SearchableData) -> SearchableData - ) { - updateState { - /** `adding` has will "Replace" on same key, and "Add" on a new key */ - val newAccounts = updater(accounts) - DataStoreHelper.accounts = - Array(newAccounts.sorted.size) { i -> - /** i -> keyIndex -> Account; As keyIndex != index */ - newAccounts.data[newAccounts.sorted[i]]!! - } - copy(accounts = newAccounts) - } - - MainActivity.reloadAccountEvent(true) - } - - override fun onAction(action: AccountAction) { - when (action) { - is AccountAction.SetEditMode -> { - updateState { copy(canEditAccount = action.canEditAccount) } - } - - is AccountAction.NewAccount -> { - updateState { - copy( - editAccount = DataStoreHelper.Account( - /** Last => maxOf, this WILL break if we allow other sorting orders */ - keyIndex = (accounts.sorted.lastOrNull() ?: 0) + 1, - /** This is not "translated", but that should not really be a problem */ - name = "Account ${accounts.data.size + 1}", - customImage = null, - defaultImageIndex = Random.nextInt(DataStoreHelper.profileImages.indices), - lockPin = null, - ) - ) - } - } - - is AccountAction.DeleteAccount -> { - removeKeys(action.accountKeyIndex.toString()) - - /** If we remove the logged in account, - * then we switch to the default account */ - if (action.accountKeyIndex == DataStoreHelper.selectedKeyIndex) { - val nextAccount = state.value.accounts.data[defaultAccount.keyIndex] - /** This should never be non-null, but just in case! */ - ?: defaultAccount - DataStoreHelper.setAccount(nextAccount) - updateState { copy(lastLoginKeyIndex = nextAccount.keyIndex) } - } - - updateAccount { accounts -> - val pendingAccounts = accounts.removing(action.accountKeyIndex) - - /** We must always have a default account */ - if (pendingAccounts.data.containsKey(defaultAccount.keyIndex)) { - pendingAccounts - } else { - pendingAccounts.adding(defaultAccount.keyIndex, defaultAccount) - } - } - } - - is AccountAction.SaveAccount -> { - updateAccount { accounts -> - /** `adding` has will "Replace" on same key, and "Add" on a new key */ - accounts.adding(action.account.keyIndex, action.account) - } - } - - is AccountAction.LoginWithAccount -> { - val accounts = state.value.accounts - val loginAccount = accounts.data[action.accountKeyIndex] ?: return - // Show toast if we have many accounts, as it is unnecessary for 1 account - DataStoreHelper.setAccount(loginAccount, showToast = accounts.data.size > 1) - MainActivity.reloadAccountEvent(true) - updateState { copy(loggedInWith = loginAccount.keyIndex, lastLoginKeyIndex = loginAccount.keyIndex) } - } - - is AccountAction.EditAccount -> { - updateState { copy(editAccount = action.account) } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt index 19c006b28..0d35d5670 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt @@ -10,7 +10,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.api.Log -import com.lagradost.cloudstream3.CloudStreamApp import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.isEpisodeBased import com.lagradost.cloudstream3.mvvm.Resource @@ -37,7 +36,6 @@ import com.lagradost.cloudstream3.utils.ResourceLiveData import com.lagradost.cloudstream3.utils.downloader.DownloadObjects import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.deleteFilesAndUpdateSettings -import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.downloadDeleteEvent import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.getDownloadFileInfo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -69,17 +67,6 @@ class DownloadViewModel : ViewModel() { private val _selectedItemIds = ConsistentLiveData?>(null) val selectedItemIds: LiveData?> = _selectedItemIds - init { - // Keep the Downloads list in sync when a download is deleted/cancelled from - // anywhere in the app (result page button, queue, notification, etc.). See - // onDownloadDeleted for the rationale (issue #1227). - downloadDeleteEvent += ::onDownloadDeleted - } - - override fun onCleared() { - downloadDeleteEvent -= ::onDownloadDeleted - super.onCleared() - } fun cancelSelection() { updateSelectedItems { null } @@ -402,18 +389,6 @@ class DownloadViewModel : ViewModel() { postChildren(_childCards.success?.filter { it.data.id !in idsToRemove }) } - /** - * Refreshes the Downloads screen in real time when a download is deleted/cancelled. - */ - private fun onDownloadDeleted(id: Int) { - // Keep multi-select state consistent: forget the removed id if it was selected. - updateSelectedItems { it?.minus(id) } - - val context = CloudStreamApp.context ?: return - updateHeaderList(context) - postChildren(_childCards.success?.filterNot { it.data.id == id }) - } - private fun updateStorageStats(visual: List) { try { val stat = StatFs(Environment.getExternalStorageDirectory().path) @@ -609,4 +584,4 @@ class DownloadViewModel : ViewModel() { val names: List, val parentName: String? ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index 450dc9a61..382a770cd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -164,11 +164,9 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } } - fun downloadDeleteEvent(data: Int) { - if (data == persistentId) { - resetView() - } - } + /*fun downloadDeleteEvent(data: Int) { + + }*/ /*fun downloadEvent(data: Pair) { val (id, action) = data @@ -187,7 +185,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : override fun onAttachedToWindow() { VideoDownloadManager.downloadStatusEvent += ::downloadStatusEvent - VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent + // VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent // VideoDownloadManager.downloadEvent += ::downloadEvent VideoDownloadManager.downloadProgressEvent += ::downloadProgressEvent @@ -202,7 +200,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : override fun onDetachedFromWindow() { VideoDownloadManager.downloadStatusEvent -= ::downloadStatusEvent - VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent + // VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent // VideoDownloadManager.downloadEvent -= ::downloadEvent VideoDownloadManager.downloadProgressEvent -= ::downloadProgressEvent diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 04f8b0b26..b68ef5962 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -18,22 +18,17 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.appcompat.app.AlertDialog import androidx.core.net.toUri -import androidx.core.view.doOnLayout import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.chip.Chip import com.lagradost.api.Log -import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainAPI @@ -48,7 +43,6 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable -import com.lagradost.cloudstream3.plugins.Plugin import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi import com.lagradost.cloudstream3.ui.BaseFragment @@ -69,7 +63,6 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettin import com.lagradost.cloudstream3.utils.AppContextUtils.isNetworkAvailable import com.lagradost.cloudstream3.utils.AppContextUtils.isRecyclerScrollable import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult -import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser import com.lagradost.cloudstream3.utils.AppContextUtils.ownHide import com.lagradost.cloudstream3.utils.AppContextUtils.ownShow import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus @@ -113,14 +106,6 @@ class HomeFragment : BaseFragment( val errorProfilePic = errorProfilePics.random() - fun Context.getDisplayName(apiName: String?): String? { - return when (apiName) { - noneApi.name -> getString(R.string.none) - randomApi.name -> getString(R.string.home_random) - else -> apiName - } - } - //fun Activity.loadHomepageList( // item: HomePageList, // deleteCallback: (() -> Unit)? = null, @@ -439,30 +424,11 @@ class HomeFragment : BaseFragment( .inflate(R.layout.sort_bottom_single_provider_choice, parent, false) val titleText = view.findViewById(R.id.text1) val pinIcon = view.findViewById(R.id.pinicon) - val settingsIcon = view.findViewById(R.id.action_settings) - val name = getItem(position) titleText?.text = name - val providerApi = currentValidApis[position] val isPinned = - pinnedphashset.contains(providerApi.name) + pinnedphashset.contains(currentValidApis[position].name) pinIcon.visibility = if (isPinned) View.VISIBLE else View.GONE - - val pluginInstance = providerApi.sourcePlugin?.let { PluginManager.plugins[it] } as? Plugin - val isDownloadedPluginWithSettings = pluginInstance?.openSettings != null && !isLayout(TV) - - settingsIcon.visibility = if (isDownloadedPluginWithSettings) View.VISIBLE else View.GONE - if (isDownloadedPluginWithSettings) { - settingsIcon.setOnClickListener { - try { - val activityContext = it.context.getActivity() ?: it.context - pluginInstance.openSettings?.invoke(activityContext) - } catch (e: Throwable) { - logError(e) - } - } - } - return view } } @@ -485,14 +451,7 @@ class HomeFragment : BaseFragment( arrayAdapter.clear() val sortedApis = validAPIs .filter { - val isPinned = pinnedphashset.contains(it.name) - - // Hide pinned NSFW when NSFW not selected. NSFW is distracting when not chosen. - if (isPinned && !preSelectedTypes.contains(TvType.NSFW)) { - if (it.supportedTypes.all { type -> type == TvType.NSFW }) return@filter false - } - - it.hasMainPage && (isPinned || it.supportedTypes.any( + it.hasMainPage && (pinnedphashset.contains(it.name) || it.supportedTypes.any( preSelectedTypes::contains )) } @@ -514,10 +473,8 @@ class HomeFragment : BaseFragment( addAll(remainingApis) } - val names = currentValidApis.map { - val displayName = getDisplayName(it.name) - if (isMultiLang) "${getFlagFromIso(it.lang)?.plus(" ") ?: ""}$displayName" else displayName - } + val names = + currentValidApis.map { if (isMultiLang) "${getFlagFromIso(it.lang)?.plus(" ") ?: ""}${it.name}" else it.name } val index = currentValidApis.map { it.name }.indexOf(currentApiName) listView?.setItemChecked(index, true) arrayAdapter.addAll(names) @@ -708,6 +665,7 @@ class HomeFragment : BaseFragment( fromUI = true ) showToast(R.string.action_reload, Toast.LENGTH_SHORT) + true } homePreviewSearchButton.setOnClickListener { _ -> @@ -715,38 +673,6 @@ class HomeFragment : BaseFragment( homeViewModel.queryTextSubmit("") } - homePreviewSettingsButton.setOnClickListener { view -> - val apiName = homeViewModel.apiName.value - val plugin = APIHolder.getApiFromNameNull(apiName) - ?.sourcePlugin?.let { PluginManager.plugins[it] } as? Plugin - val openSettings = plugin?.openSettings - if (openSettings != null) { - try { - val activityContext = view.context.getActivity() ?: view.context - openSettings.invoke(activityContext) - } catch (e: Throwable) { - logError(e) - } - } - } - - // Load value for toggling Tv layout real time clock. Hide by default at startup - // set visibility first, to apply a scroll effect later - context?.let { - if (isLayout(TV)) { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(it) - val toggleClock = - settingsManager.getBoolean( - getString(R.string.tv_layout_clock_key), - false - ) - binding.homeClock.isVisible = toggleClock - } else { - binding.homeClock.isVisible = false - } - } - - homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (isLayout(PHONE)) { @@ -786,26 +712,12 @@ class HomeFragment : BaseFragment( view.getLocationInWindow(rect) scrollParent.isVisible = true scrollParent.translationY = rect[1].toFloat() - 60.toPx - - // Move the TV layout real time clock out of the way too - // We check if we have the correct layout and if the clock is enabled - if(isLayout(TV) && binding.homeClock.isVisible) { - val scrollParent = binding.homeClock - - val rect = IntArray(2) - view.getLocationInWindow(rect) - scrollParent.isVisible = true - scrollParent.translationY = rect[1].toFloat() - 60.toPx - } } } super.onScrolled(recyclerView, dx, dy) - updateTvRailFocus() } }) - homeMasterRecycler.doOnLayout { updateTvRailFocus() } - } //Load value for toggling Random button. Hide at startup @@ -822,10 +734,9 @@ class HomeFragment : BaseFragment( observe(homeViewModel.apiName) { apiName -> currentApiName = apiName - val displayApiName = context?.getDisplayName(apiName) ?: apiName binding.apply { - homeApiFab.text = displayApiName - homeChangeApi.text = displayApiName + homeApiFab.text = apiName + homeChangeApi.text = apiName homePreviewReloadProvider.isGone = (apiName == noneApi.name) homePreviewSearchButton.isGone = (apiName == noneApi.name) } @@ -833,12 +744,6 @@ class HomeFragment : BaseFragment( observe(homeViewModel.page) { data -> binding.apply { - if (isLayout(TV or EMULATOR)) { - val plugin = APIHolder.getApiFromNameNull(homeViewModel.apiName.value) - ?.sourcePlugin?.let { PluginManager.plugins[it] } as? Plugin - homePreviewSettingsButton.isGone = plugin?.openSettings == null - } - when (data) { is Resource.Success -> { val d = data.value @@ -854,7 +759,6 @@ class HomeFragment : BaseFragment( homeLoadingError.isVisible = false homeMasterRecycler.isVisible = true homeLoadingShimmer.stopShimmer() - //home_loaded?.isVisible = true if (toggleRandomButton) { val distinct = d.values @@ -875,16 +779,26 @@ class HomeFragment : BaseFragment( homeRandomButtonTv.isGone = true } } - //Open browser directly, without a menu. + is Resource.Failure -> { homeLoadingShimmer.stopShimmer() homeReloadConnectionerror.setOnClickListener(apiChangeClickListener) - homeReloadConnectionOpenInBrowser.setOnClickListener { - val currentApi = currentApiName?.let { getApiFromNameNull(it) } - ?: homeViewModel.apiName.value?.let { getApiFromNameNull(it) } - val mainUrl = currentApi?.mainUrl - if (!mainUrl.isNullOrBlank()) { - context?.openBrowser(mainUrl) + homeReloadConnectionOpenInBrowser.setOnClickListener { view -> + val validAPIs = apis//.filter { api -> api.hasMainPage } + + view.popupMenuNoIconsAndNoStringRes(validAPIs.mapIndexed { index, api -> + Pair( + index, + api.name + ) + }) { + try { + val i = Intent(Intent.ACTION_VIEW) + i.data = validAPIs[itemId].mainUrl.toUri() + startActivity(i) + } catch (e: Exception) { + logError(e) + } } } @@ -979,29 +893,6 @@ class HomeFragment : BaseFragment( }*/ } - private fun updateTvRailFocus() { - if (!isLayout(TV or EMULATOR)) return - // home_api_holder is glued to the header row's top by the mirror logic in - // the scroll listener above. Only target the provider button while the - // holder is actually on screen; once the header scrolls off (y < 0) let - // the rail fall back to default focus search so RIGHT enters content rows - val holder = binding?.homeApiHolder - val target = - if (holder != null && holder.isVisible && holder.y >= 0f) R.id.home_change_api - else View.NO_ID - val rail = activity?.findViewById(R.id.nav_rail_view) ?: return - rail.nextFocusRightId = target - for (focusView in arrayOf( - R.id.navigation_downloads, - R.id.navigation_home, - R.id.navigation_search, - R.id.navigation_library, - R.id.navigation_settings, - )) { - rail.findViewById(focusView)?.nextFocusRightId = target - } - } - private fun handleTvBackPress(helper: BackPressedCallbackHelper.CallbackHelper) { // Only apply custom behavior on TV interface if (!isLayout(TV)) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index 1f721ee20..6bdd1bf49 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -159,7 +159,7 @@ open class ParentItemAdapter( }) //(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged() - if (isLayout(PHONE or EMULATOR)) { + if (isLayout(PHONE)) { homeChildMoreInfo.setOnClickListener { moreInfoClickCallback.invoke(item) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 1d1a88fe9..959806e56 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -310,9 +310,6 @@ class HomeParentItemAdapterPreview( private val previewViewpager: ViewPager2 = itemView.findViewById(R.id.home_preview_viewpager) - private val homePreviewShimmer: com.facebook.shimmer.ShimmerFrameLayout? = - itemView.findViewById(R.id.home_preview_shimmer) - private val previewViewpagerText: ViewGroup = itemView.findViewById(R.id.home_preview_viewpager_text) @@ -665,8 +662,6 @@ class HomeParentItemAdapterPreview( when (preview) { is Resource.Success -> { - homePreviewShimmer?.stopShimmer() - homePreviewShimmer?.isGone = true previewAdapter.submitList(preview.value.second) previewAdapter.hasMoreItems = preview.value.first /*if (!.setItems( @@ -698,16 +693,7 @@ class HomeParentItemAdapterPreview( } } - is Resource.Loading -> { - previewViewpager.isGone = true - previewViewpagerText.isGone = true - homePreviewShimmer?.isVisible = true - homePreviewShimmer?.startShimmer() - } - else -> { - homePreviewShimmer?.stopShimmer() - homePreviewShimmer?.isGone = true previewAdapter.submitList(listOf()) previewViewpager.setCurrentItem(0, false) previewViewpager.isVisible = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt index 1020205ac..e42e774b5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt @@ -58,7 +58,7 @@ class HomeScrollAdapter( when (binding) { is HomeScrollViewBinding -> { - binding.homeScrollPreview.loadImage(posterUrl, item.posterHeaders) + binding.homeScrollPreview.loadImage(posterUrl) binding.homeScrollPreviewTags.apply { text = item.tags?.joinToString(" • ") ?: "" isGone = item.tags.isNullOrEmpty() @@ -79,8 +79,8 @@ class HomeScrollAdapter( binding.homeScrollPreview.setOnClickListener { view -> callback.invoke(view ?: return@setOnClickListener, position, item) } - binding.homeScrollPreview.loadImage(posterUrl, item.posterHeaders) + binding.homeScrollPreview.loadImage(posterUrl) } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 8d48f5a68..e0609c0e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -133,7 +133,7 @@ class HomeViewModel : ViewModel() { private var currentShuffledList: List = listOf() private fun autoloadRepo(): APIRepository { - return APIRepository(apis.withLock { apis.first { it.hasMainPage } }) + return APIRepository(synchronized(apis) { apis.first { it.hasMainPage } }) } private val _availableWatchStatusTypes = diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index e73a8c75a..6e28c128d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -20,7 +20,6 @@ import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView -import com.fasterxml.jackson.annotation.JsonProperty import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import com.lagradost.cloudstream3.APIHolder @@ -53,13 +52,12 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.util.concurrent.CopyOnWriteArrayList import kotlin.math.abs const val LIBRARY_FOLDER = "library_folder" + enum class LibraryOpenerType(@StringRes val stringRes: Int) { Default(R.string.action_default), Provider(R.string.none), @@ -69,15 +67,13 @@ enum class LibraryOpenerType(@StringRes val stringRes: Int) { } /** Used to store how the user wants to open said poster */ -@Serializable data class LibraryOpener( - @JsonProperty("openType") @SerialName("openType") val openType: LibraryOpenerType, - @JsonProperty("providerData") @SerialName("providerData") val providerData: ProviderLibraryData?, + val openType: LibraryOpenerType, + val providerData: ProviderLibraryData?, ) -@Serializable data class ProviderLibraryData( - @JsonProperty("apiName") @SerialName("apiName") val apiName: String, + val apiName: String ) class LibraryFragment : BaseFragment( @@ -214,13 +210,14 @@ class LibraryFragment : BaseFragment( syncId: SyncIdName, apiName: String? = null, ) { - val availableProviders = allProviders.filter { - it.supportedSyncNames.contains(syncId) - }.map { it.name } + - // Add the api if it exists - (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } - ?: emptyList()) - + val availableProviders = synchronized(allProviders) { + allProviders.filter { + it.supportedSyncNames.contains(syncId) + }.map { it.name } + + // Add the api if it exists + (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } + ?: emptyList()) + } val baseOptions = listOf( LibraryOpenerType.Default, LibraryOpenerType.None, @@ -572,4 +569,4 @@ class LibraryFragment : BaseFragment( } } -class MenuSearchView(context: Context) : SearchView(context) +class MenuSearchView(context: Context) : SearchView(context) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index b63191c19..aa44b9235 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -46,9 +46,7 @@ import androidx.media3.exoplayer.DecoderCounters import androidx.media3.exoplayer.DecoderReuseEvaluation import androidx.media3.exoplayer.DefaultLivePlaybackSpeedControl import androidx.media3.exoplayer.DefaultLoadControl -import androidx.media3.common.audio.AudioProcessor import androidx.media3.exoplayer.DefaultRenderersFactory -import androidx.media3.exoplayer.audio.DefaultAudioSink import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.Renderer.STATE_ENABLED import androidx.media3.exoplayer.Renderer.STATE_STARTED @@ -98,7 +96,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyle import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus -import com.lagradost.cloudstream3.utils.CLEARKEY_DRM_UUID +import com.lagradost.cloudstream3.utils.CLEARKEY_UUID import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount @@ -106,11 +104,10 @@ import com.lagradost.cloudstream3.utils.DrmExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.PLAYREADY_DRM_UUID +import com.lagradost.cloudstream3.utils.PLAYREADY_UUID import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToLanguageName -import com.lagradost.cloudstream3.utils.WIDEVINE_DRM_UUID +import com.lagradost.cloudstream3.utils.WIDEVINE_UUID import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp -import com.lagradost.cloudstream4.AppSettings import kotlinx.coroutines.delay import okhttp3.Interceptor import org.chromium.net.CronetEngine @@ -121,7 +118,6 @@ import java.util.concurrent.Executors import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession -import kotlin.uuid.toJavaUuid const val TAG = "CS3ExoPlayer" const val PREFERRED_AUDIO_LANGUAGE_KEY = "preferred_audio_language" @@ -164,14 +160,6 @@ class CS3IPlayer : IPlayer { private var ignoreSSL: Boolean = true private var playBackSpeed: Float = 1.0f - /** - * Shared compressor instance — only created when the setting is enabled. - * When disabled, this stays null and the audio pipeline is completely - * unmodified (no custom AudioProcessor, no custom AudioSink at all). - */ - var compressor: DynamicRangeCompressor? = null - private set - private var lastMuteVolume: Float = 1.0f private var currentLink: ExtractorLink? = null @@ -730,7 +718,7 @@ class CS3IPlayer : IPlayer { **/ var preferredAudioTrackLanguage: String? = null get() { - return field ?: getKey( + return field ?: getKey( "$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", field )?.also { @@ -1120,17 +1108,8 @@ class CS3IPlayer : IPlayer { else -> isLayout(PHONE or EMULATOR) to false } - // Only create the compressor when the setting is actually enabled. - // When it's off, `compressor` stays null and nothing about the audio - // pipeline is touched — same behaviour as before this feature existed. - val isCompressorEnabled = settingsManager.getBoolean( - context.getString(R.string.compressor_enabled_key), - false - ) - compressor = if (isCompressorEnabled) DynamicRangeCompressor().apply { restoreSavedSettings() } else null - val factory = if (isSoftwareDecodingEnabled) { - FixedNextRenderersFactory(context, compressor).apply { + FixedNextRenderersFactory(context).apply { setEnableDecoderFallback(true) setExtensionRendererMode( if (isSoftwareDecodingPreferred) @@ -1140,24 +1119,8 @@ class CS3IPlayer : IPlayer { ) } } else { - val activeCompressor = compressor - if (activeCompressor == null) { - // no nextlib = EXTENSION_RENDERER_MODE_OFF, no compressor = fully default sink - DefaultRenderersFactory(context) - } else { - object : DefaultRenderersFactory(context) { - @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) - override fun buildAudioSink( - ctx: Context, - enableFloatOutput: Boolean, - enableAudioTrackPlaybackParams: Boolean - ) = DefaultAudioSink.Builder(ctx) - .setEnableFloatOutput(enableFloatOutput) - .setEnableAudioOutputPlaybackParameters(enableAudioTrackPlaybackParams) - .setAudioProcessors(arrayOf(activeCompressor)) - .build() - } - } + // no nextlib = EXTENSION_RENDERER_MODE_OFF + DefaultRenderersFactory(context) } val style = CustomDecoder.style @@ -1315,7 +1278,7 @@ class CS3IPlayer : IPlayer { item.drm?.let { drm -> when (drm.uuid) { - CLEARKEY_DRM_UUID.toJavaUuid() -> { + CLEARKEY_UUID -> { // Use headers from DrmMetadata for media requests val client = dataSourceFactory ?: throw IllegalArgumentException("Must supply onlineSource") @@ -1336,8 +1299,8 @@ class CS3IPlayer : IPlayer { .createMediaSource(item.mediaItem) } - WIDEVINE_DRM_UUID.toJavaUuid(), - PLAYREADY_DRM_UUID.toJavaUuid() -> { + WIDEVINE_UUID, + PLAYREADY_UUID -> { // Use headers from DrmMetadata for media requests val client = dataSourceFactory ?: throw IllegalArgumentException("Must supply onlineSource") @@ -1425,17 +1388,10 @@ class CS3IPlayer : IPlayer { context.getString(if (context.isUsingMobileData()) R.string.quality_pref_mobile_data_key else R.string.quality_pref_key), Int.MAX_VALUE ) - val settings = AppSettings(context) try { hasUsedFirstRender = false - val playWhenReady = if (settings.player.startPaused.get()) { - false - } else { - isPlaying // this keep the current state of the player - } - // ye this has to be a val for whatever reason // this makes no sense exoPlayer = buildExoPlayer( @@ -1447,7 +1403,7 @@ class CS3IPlayer : IPlayer { playBackSpeed, cacheSize = cacheSize, videoBufferMs = videoBufferMs, - playWhenReady = playWhenReady, + playWhenReady = isPlaying, // this keep the current state of the player subtitleOffset = currentSubtitleOffset, maxVideoHeight = maxVideoHeight, audioSources = audioSources, @@ -1958,7 +1914,7 @@ class CS3IPlayer : IPlayer { drm = DrmMetadata( kid = link.kid, key = link.key, - uuid = link.uuid.toJavaUuid(), + uuid = link.uuid, kty = link.kty, licenseUrl = link.licenseUrl, keyRequestParameters = link.keyRequestParameters, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index a086cc16f..7a42cea93 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -58,23 +58,9 @@ class DownloadedPlayerActivity : AppCompatActivity() { enableEdgeToEdgeCompat() setContentView(R.layout.empty_layout) Log.i(TAG, "onCreate") - handleIntent(intent) - /** - * Use moveTaskToBack instead of finish() so there is always exactly one task - * entry in recents, always reflecting the current file. - * - * finish() destroys the Activity but may leave the task in recents. Each new file - * open can create a new task entry, so recents accumulates stale entries for old - * files. The user then taps a stale entry and gets the wrong file. - * - * moveTaskToBack keeps the Activity alive in the background. There is only ever - * one task entry in recents. New files opened from the file manager arrive via - * onNewIntent on the live instance, updating the player immediately. The single - * recents entry always reflects the current state, ensuring we load the - * correct file. - */ - attachBackPressedCallback("DownloadedPlayerActivity") { moveTaskToBack(true) } + handleIntent(intent) + attachBackPressedCallback("DownloadedPlayerActivity") { finish() } } private fun handleIntent(intent: Intent) { @@ -97,11 +83,11 @@ class DownloadedPlayerActivity : AppCompatActivity() { url != null -> playLink(this, url) data != null -> playUri(this, data) extraText != null -> playLink(this, extraText) - else -> finishAndRemoveTask() + else -> { finish(); return } } } else if (data?.scheme == "content") { playUri(this, data) - } else finishAndRemoveTask() + } else finish() } override fun onResume() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DynamicRangeCompressor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DynamicRangeCompressor.kt deleted file mode 100644 index 8a2581dd8..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DynamicRangeCompressor.kt +++ /dev/null @@ -1,266 +0,0 @@ -package com.lagradost.cloudstream3.ui.player - -import androidx.annotation.OptIn -import androidx.media3.common.C -import androidx.media3.common.audio.AudioProcessor -import androidx.media3.common.audio.AudioProcessor.AudioFormat -import androidx.media3.common.util.UnstableApi -import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey -import java.nio.ByteBuffer -import java.nio.ByteOrder -import kotlin.math.exp -import kotlin.math.ln -import kotlin.math.pow - -/** - * A dynamic range compressor for the audio player. - * - * ## What it does - * A dynamic range compressor automatically turns down loud sounds and turns up - * quiet ones, narrowing the gap between the loudest and quietest moments. - * This is useful for watching movies or TV shows where action/explosion scenes - * are very loud but dialogue scenes are very quiet — the compressor brings - * everything to a more comfortable, even volume. - * - * ## Algorithm - * Implements a standard single-band feed-forward peak compressor using a - * 1-pole IIR envelope follower. This is the same algorithm used in most - * audio software (Audacity, VLC, etc.) and is well-established DSP. It is - * NOT a port or derivative of any specific implementation — the math is - * standard and covered extensively in audio engineering literature - * (e.g. Zölzer "Digital Audio Signal Processing"). - * - * Uses a single shared envelope across all channels so left and right always - * receive identical gain — independent per-channel envelopes would destroy - * the stereo image. - * - * Handles both PCM_16BIT and PCM_FLOAT (both are used by ExoPlayer depending - * on the source and device). Always stays "active" in the pipeline so that - * enable/disable works instantly without reloading the player — when disabled, - * samples are copied unchanged (passthrough). - */ -internal const val COMPRESSOR_ENABLED_KEY = "player_compressor_enabled" -internal const val COMPRESSOR_THRESHOLD_KEY = "player_compressor_threshold" -internal const val COMPRESSOR_RATIO_KEY = "player_compressor_ratio" -internal const val COMPRESSOR_ATTACK_KEY = "player_compressor_attack" -internal const val COMPRESSOR_RELEASE_KEY = "player_compressor_release" -internal const val COMPRESSOR_MAKEUP_KEY = "player_compressor_makeup" - -@OptIn(UnstableApi::class) -class DynamicRangeCompressor : AudioProcessor { - - @Volatile var enabled: Boolean = false - - /** - * The level (in dB) above which compression kicks in. Signals quieter than - * this pass through unchanged; louder signals get compressed. - * Lower = compresses more content (including quieter sounds like dialogue). - * -24 dB is a good starting point for movies: it catches action peaks - * while leaving quiet dialogue mostly untouched before makeup gain. - */ - @Volatile var threshold: Float = -24f // dB, range -30..0 - - /** - * How aggressively to compress sounds that exceed the threshold. - * A ratio of 8:1 means an 8 dB increase above the threshold becomes only - * 1 dB in the output. Higher = more "squashed" dynamic range. - * Below ~2 is barely noticeable; above ~10 sounds very processed/radio-like. - */ - @Volatile var ratio: Float = 8f // n:1, range 1..20 - - /** - * How quickly (in ms) the compressor clamps down when a loud sound starts. - * Too low (< 1 ms): no transient punch, sounds dull. - * Too high (> 50 ms): loud transients slip through before gain reduces. - * 5 ms preserves punch while still catching most action scene peaks. - */ - @Volatile var attackMs: Float = 5f // ms, range 1..400 - - /** - * How quickly (in ms) the compressor lets go after a loud sound ends. - * Too low (< 50 ms): audible "pumping" — volume visibly breathes up/down. - * Too high (> 800 ms): gain stays low too long, quieter sounds after - * an action scene stay suppressed for a noticeable time. - * 400 ms is slow enough to be transparent on most movie content. - */ - @Volatile var releaseMs: Float = 400f // ms, range 2..800 - - /** - * Output gain (in dB) applied after compression. - * Compression reduces overall loudness so makeup gain brings it back up. - * +12 dB compensates for the typical reduction at a 8:1 ratio with a - * -24 dB threshold, and also lifts quiet dialogue to a more audible level. - * Too high risks clipping on uncompressed peaks below the threshold. - */ - @Volatile var makeupGain: Float = 12f // dB, range 0..24 - - fun restoreSavedSettings() { - enabled = getKey(COMPRESSOR_ENABLED_KEY) ?: false - threshold = getKey(COMPRESSOR_THRESHOLD_KEY) ?: -24f - ratio = getKey(COMPRESSOR_RATIO_KEY) ?: 8f - attackMs = getKey(COMPRESSOR_ATTACK_KEY) ?: 5f - releaseMs = getKey(COMPRESSOR_RELEASE_KEY) ?: 400f - makeupGain = getKey(COMPRESSOR_MAKEUP_KEY) ?: 12f - } - - private var format = AudioFormat.NOT_SET - private var isFloat = false - private var sampleRate = 44100 - private var channelCount = 2 - - // Single shared envelope across all channels — keeps L/R gain identical - // so stereo image is preserved and no crackling from gain mismatch. - private var envelope = 0f - - private var attackCoeff = 0f - private var releaseCoeff = 0f - private var lastAttackMs = -1f - private var lastReleaseMs = -1f - private var lastSampleRate = -1 - - private var outputBuffer: ByteBuffer = AudioProcessor.EMPTY_BUFFER - private var inputEnded = false - - // ── AudioProcessor ──────────────────────────────────────────────────────── - - override fun configure(inputAudioFormat: AudioFormat): AudioFormat { - if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT && - inputAudioFormat.encoding != C.ENCODING_PCM_FLOAT) { - return inputAudioFormat // pass unsupported formats through unchanged - } - format = inputAudioFormat - isFloat = inputAudioFormat.encoding == C.ENCODING_PCM_FLOAT - sampleRate = inputAudioFormat.sampleRate - channelCount = inputAudioFormat.channelCount - envelope = 0f - return inputAudioFormat - } - - // Always active when format is set — toggling isActive() mid-stream has no - // effect in media3 (only checked at configure() time). We do passthrough - // in queueInput() instead so enable/disable works immediately. - override fun isActive(): Boolean = - format != AudioFormat.NOT_SET && - (format.encoding == C.ENCODING_PCM_16BIT || format.encoding == C.ENCODING_PCM_FLOAT) - - override fun queueInput(inputBuffer: ByteBuffer) { - if (!inputBuffer.hasRemaining()) return - val remaining = inputBuffer.remaining() - val out = replaceOutputBuffer(remaining) - if (!enabled) { out.put(inputBuffer); out.flip(); return } - updateCoefficients() - val makeupLinear = dbToLinear(makeupGain) - val threshLinear = dbToLinear(threshold) - val bytesPerSample = if (isFloat) 4 else 2 - val frameCount = remaining / (channelCount * bytesPerSample) - if (isFloat) processFloat(inputBuffer, out, frameCount, threshLinear, makeupLinear) - else processShort(inputBuffer, out, frameCount, threshLinear, makeupLinear) - out.flip() - } - - /** Processes PCM_FLOAT frames. Two-pass: peak detection then gain application. */ - private fun processFloat( - input: ByteBuffer, out: ByteBuffer, - frameCount: Int, threshLinear: Float, makeupLinear: Float - ) { - repeat(frameCount) { - val frameStart = input.position() - var peak = 0f - repeat(channelCount) { val s = input.getFloat(); val a = if (s < 0f) -s else s; if (a > peak) peak = a } - advanceEnvelope(peak) - val gain = computeGain(threshLinear) * makeupLinear - input.position(frameStart) - repeat(channelCount) { out.putFloat(input.getFloat() * gain) } - } - } - - /** Processes PCM_16BIT frames. Two-pass: peak detection then gain application. */ - private fun processShort( - input: ByteBuffer, out: ByteBuffer, - frameCount: Int, threshLinear: Float, makeupLinear: Float - ) { - repeat(frameCount) { - val frameStart = input.position() - var peak = 0f - repeat(channelCount) { val s = input.getShort() / 32768f; val a = if (s < 0f) -s else s; if (a > peak) peak = a } - advanceEnvelope(peak) - val gain = computeGain(threshLinear) * makeupLinear - input.position(frameStart) - repeat(channelCount) { - val s = input.getShort() / 32768f - out.putShort((s * gain).coerceIn(-1f, 1f).let { (it * 32768f).toInt().toShort() }) - } - } - } - - private fun advanceEnvelope(peak: Float) { - val coeff = if (peak > envelope) attackCoeff else releaseCoeff - envelope = peak + coeff * (envelope - peak) - } - - override fun queueEndOfStream() { inputEnded = true } - - override fun getOutput(): ByteBuffer { - val out = outputBuffer - outputBuffer = AudioProcessor.EMPTY_BUFFER - return out - } - - override fun isEnded(): Boolean = - inputEnded && outputBuffer === AudioProcessor.EMPTY_BUFFER - - // AudioProcessor.flush() (no-arg) is deprecated upstream in favor of a - // seek-aware overload, but this interface implementation still only exposes - // the no-arg version to override, so this suppresses that specific warning - // without adding @Deprecated (which would cascade to our own reset() below, - // since it calls flush() internally). - @Suppress("OVERRIDE_DEPRECATION") - override fun flush() { - // Called on every seek. Reset envelope so there's no gain burst - // from stale state — which is what caused crackling after skipping. - envelope = 0f - outputBuffer = AudioProcessor.EMPTY_BUFFER - inputEnded = false - } - - override fun reset() { - flush() - format = AudioFormat.NOT_SET - isFloat = false - } - - // ── Helpers ─────────────────────────────────────────────────────────────── - - private fun computeGain(threshLinear: Float): Float { - return if (envelope <= threshLinear) { - 1f - } else { - val overDb = linearToDb(envelope) - threshold - dbToLinear(-(overDb * (1f - 1f / ratio))) - } - } - - private fun replaceOutputBuffer(size: Int): ByteBuffer { - if (outputBuffer.capacity() < size) { - outputBuffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()) - } else { - outputBuffer.clear() - } - return outputBuffer - } - - private fun updateCoefficients() { - if (attackMs == lastAttackMs && - releaseMs == lastReleaseMs && - sampleRate == lastSampleRate) return - lastAttackMs = attackMs - lastReleaseMs = releaseMs - lastSampleRate = sampleRate - attackCoeff = exp(-1.0 / (sampleRate * attackMs / 1000.0)).toFloat() - releaseCoeff = exp(-1.0 / (sampleRate * releaseMs / 1000.0)).toFloat() - } - - private fun dbToLinear(db: Float): Float = 10f.pow(db / 20f) - private fun linearToDb(linear: Float): Float = - if (linear <= 0f) -120f else (20.0 * ln(linear.toDouble()) / ln(10.0)).toFloat() -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FixedNextRenderersFactory.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FixedNextRenderersFactory.kt index 3fdd7081b..025267cc9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FixedNextRenderersFactory.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FixedNextRenderersFactory.kt @@ -2,21 +2,14 @@ package com.lagradost.cloudstream3.ui.player import android.content.Context import android.os.Looper -import androidx.annotation.OptIn -import androidx.media3.common.audio.AudioProcessor import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.Renderer -import androidx.media3.exoplayer.audio.AudioSink -import androidx.media3.exoplayer.audio.DefaultAudioSink import androidx.media3.exoplayer.text.TextOutput import androidx.media3.exoplayer.text.TextRenderer import io.github.anilbeesetti.nextlib.media3ext.ffdecoder.NextRenderersFactory @UnstableApi -class FixedNextRenderersFactory( - context: Context, - private val compressor: DynamicRangeCompressor? = null, -) : NextRenderersFactory(context) { +class FixedNextRenderersFactory(context: Context) : NextRenderersFactory(context) { /** Somehow the nextlib authors decided that we need a text renderer that causes * "ERROR_CODE_FAILED_RUNTIME_CHECK". * @@ -32,26 +25,4 @@ class FixedNextRenderersFactory( ) { out.add(TextRenderer(output, outputLooper)) } - - /** - * Only builds a custom [DefaultAudioSink] when a compressor is actually supplied. - * When [compressor] is null (the setting is off, or unsupported on this device) we - * fall through to the completely unmodified default sink from the parent factory, - * so playback behaves exactly as it did before this feature existed. - */ - @OptIn(UnstableApi::class) - override fun buildAudioSink( - context: Context, - enableFloatOutput: Boolean, - enableAudioTrackPlaybackParams: Boolean - ): AudioSink? { - val activeCompressor = compressor - ?: return super.buildAudioSink(context, enableFloatOutput, enableAudioTrackPlaybackParams) - - return DefaultAudioSink.Builder(context) - .setEnableFloatOutput(enableFloatOutput) - .setEnableAudioOutputPlaybackParameters(enableAudioTrackPlaybackParams) - .setAudioProcessors(arrayOf(activeCompressor)) - .build() - } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index d337d53a6..4141aef51 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -4,7 +4,6 @@ import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.app.Activity import android.app.Dialog -import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey import android.content.Context import android.content.DialogInterface import android.content.pm.ActivityInfo @@ -103,12 +102,12 @@ open class FullScreenPlayer : AbstractPlayerFragment( private var hideControlsNames = false protected var subtitleDelay set(value) = try { - player.setSubtitleOffset(value) + player.setSubtitleOffset(-value) } catch (e: Exception) { logError(e) } get() = try { - player.getSubtitleOffset() + -player.getSubtitleOffset() } catch (e: Exception) { logError(e) 0L @@ -158,15 +157,6 @@ open class FullScreenPlayer : AbstractPlayerFragment( autoHide() } } - protected var selectCompressorDialog: Dialog? = null - set(value) { - val prevField = field - field = value - if (value == null && prevField != null) { - autoHide() - } - } - protected var playBackCompressorEnabled = false /** Checks if any top level dialog is open and showing */ fun isDialogOpen() = @@ -174,7 +164,6 @@ open class FullScreenPlayer : AbstractPlayerFragment( || selectTrackDialog?.isShowing == true || selectSpeedDialog?.isShowing == true || selectSubtitlesDialog?.isShowing == true - || selectCompressorDialog?.isShowing == true || isShowingEpisodeOverlay private fun scheduleMetadataVisibility() { @@ -278,72 +267,84 @@ open class FullScreenPlayer : AbstractPlayerFragment( // The lib uses Invisible instead of Gone for no reason binding.previewFrameLayout.height - binding.bottomPlayerBar.height ) else -sStyle.elevation.toPx - - sView.animateY(move.toFloat()) + ObjectAnimator.ofFloat(sView, "translationY", move.toFloat()).apply { + duration = 200 + start() + } } - private fun View.animateY(value: Float) { - ObjectAnimator.ofFloat(this, "translationY", value).apply { - duration = 200 - start() - } - } - - private fun View.animateX(value: Float) { - ObjectAnimator.ofFloat(this, "translationX", value).apply { - duration = 200 - start() - } - } - protected fun animateLayoutChanges() { + if (isLayout(PHONE)) { // isEnabled also disables the onKeyDown + playerBinding?.exoProgress?.isEnabled = isShowing // Prevent accidental clicks/drags + } + + if (isShowing) { + updateUIVisibility() + } else { + toggleEpisodesOverlay(false) + playerBinding?.playerHolder?.postDelayed({ updateUIVisibility() }, 200) + } + + val titleMove = if (isShowing) 0f else -50.toPx.toFloat() + playerBinding?.playerVideoTitleHolder?.let { + ObjectAnimator.ofFloat(it, "translationY", titleMove).apply { + duration = 200 + start() + } + } + playerBinding?.playerVideoTitleRez?.let { + ObjectAnimator.ofFloat(it, "translationY", titleMove).apply { + duration = 200 + start() + } + } + playerBinding?.playerVideoInfo?.let { + ObjectAnimator.ofFloat(it, "translationY", titleMove).apply { + duration = 200 + start() + } + } + playerBinding?.playerMetadataScrim?.let { + ObjectAnimator.ofFloat(it, "translationY", 1f).apply { + duration = 200 + start() + } + } + + val playerBarMove = if (isShowing) 0f else 50.toPx.toFloat() + playerBinding?.bottomPlayerBar?.let { + ObjectAnimator.ofFloat(it, "translationY", playerBarMove).apply { + duration = 200 + start() + } + } + if (isLayout(PHONE)) { + playerBinding?.playerEpisodesButton?.let { + ObjectAnimator.ofFloat(it, "translationX", if (isShowing) 0f else 50.toPx.toFloat()) + .apply { + duration = 200 + start() + } + } + } + val fadeTo = if (isShowing) 1f else 0f + val fadeAnimation = AlphaAnimation(1f - fadeTo, fadeTo) + + fadeAnimation.duration = 100 + fadeAnimation.fillAfter = true + + animateLayoutChangesForSubtitles() + + val playerSourceMove = if (isShowing) 0f else -50.toPx.toFloat() playerBinding?.apply { - - if (isLayout(PHONE)) { // isEnabled also disables the onKeyDown - exoProgress.isEnabled = isShowing // Prevent accidental clicks/drags + playerOpenSource.let { + ObjectAnimator.ofFloat(it, "translationY", playerSourceMove).apply { + duration = 200 + start() + } } - if (isShowing) { - updateUIVisibility() - } else { - toggleEpisodesOverlay(false) - playerHolder.postDelayed({ updateUIVisibility() }, 200) - } - - val titleMove = if (isShowing) 0f else -50.toPx.toFloat() - - listOfNotNull( - playerVideoTitleHolder, - playerVideoTitleRez, - playerVideoInfo, - playerGoBackHolder, - playerVideoClock, - ).forEach { - it.animateY(titleMove) - } - - playerMetadataScrim.animateY(1f) - - val playerBarMove = if (isShowing) 0f else 50.toPx.toFloat() - bottomPlayerBar.animateY(playerBarMove) - - if (isLayout(PHONE)) { - playerEpisodesButton.animateX(if (isShowing) 0f else 50.toPx.toFloat()) - } - - val fadeTo = if (isShowing) 1f else 0f - val fadeAnimation = AlphaAnimation(1f - fadeTo, fadeTo) - - fadeAnimation.duration = 100 - fadeAnimation.fillAfter = true - - animateLayoutChangesForSubtitles() - - val playerSourceMove = if (isShowing) 0f else -50.toPx.toFloat() - - playerOpenSource.animateY(playerSourceMove) - if (!isLocked) { playerHostView?.gestureHelper?.animateCenterControls(fadeTo) shadowOverlay.isVisible = true @@ -530,29 +531,16 @@ open class FullScreenPlayer : AbstractPlayerFragment( var currentOffset = subtitleDelay binding.apply { - var subtitleAdapter: SubtitleOffsetItemAdapter? = null - subtitleOffsetInput.doOnTextChanged { text, _, _, _ -> text?.toString()?.toLongOrNull()?.let { time -> currentOffset = time - - // Scroll to the first active subtitle - val playerPosition = player.getPosition() ?: 0 - val totalPosition = playerPosition + currentOffset - subtitleAdapter?.updateTime(totalPosition) - - subtitleAdapter?.getLatestActiveItem(totalPosition) - ?.let { subtitlePos -> - subtitleOffsetRecyclerview.scrollToPosition(subtitlePos) - } - val str = when { time > 0L -> { - txt(R.string.subtitle_offset_extra_hint_before_format, time) + txt(R.string.subtitle_offset_extra_hint_later_format, time) } time < 0L -> { - txt(R.string.subtitle_offset_extra_hint_later_format, -time) + txt(R.string.subtitle_offset_extra_hint_before_format, -time) } else -> { @@ -570,12 +558,12 @@ open class FullScreenPlayer : AbstractPlayerFragment( subtitleOffsetRecyclerview.isVisible = subtitles.isNotEmpty() noSubtitlesLoadedNotice.isVisible = subtitles.isEmpty() - val initialSubtitlePosition = (player.getPosition() ?: 0) + currentOffset - subtitleAdapter = + val initialSubtitlePosition = (player.getPosition() ?: 0) - currentOffset + val subtitleAdapter = SubtitleOffsetItemAdapter(initialSubtitlePosition) { subtitleCue -> val playerPosition = player.getPosition() ?: 0 subtitleOffsetInput.text = Editable.Factory.getInstance() - ?.newEditable((subtitleCue.startTimeMs - playerPosition).toString()) + ?.newEditable((playerPosition - subtitleCue.startTimeMs).toString()) }.apply { submitList(subtitles) } @@ -708,268 +696,6 @@ open class FullScreenPlayer : AbstractPlayerFragment( //} } - // getColorStateList(index) on a TypedArray built from a raw intArrayOf(...) can't be - // statically verified by lint as a styleable resource index (it isn't tied to an - // ), so it always flags a false-positive ResourceType error here — - // this is the standard, expected suppression for this obtainStyledAttributes pattern. - @SuppressLint("ResourceType") - private fun showCompressorDialog() { - val act = activity ?: return - val compressor = (player as? CS3IPlayer)?.compressor ?: return - - // Always restore — CS3IPlayer is recreated between videos - restoreCompressorSettings() - - // Snapshot for Cancel revert - data class Snap(val enabled: Boolean, val threshold: Float, val makeupGain: Float) - val snap = Snap(compressor.enabled, compressor.threshold, compressor.makeupGain) - - val binding = com.lagradost.cloudstream3.databinding.CompressorDialogBinding - .inflate(android.view.LayoutInflater.from(act)) - - // ── Visual update helpers ────────────────────────────────────────── - fun updateCurrentLabel() { - binding.compressorCurrentLabel.text = if (compressor.enabled) { - act.getString( - R.string.compressor_on_format, - compressor.threshold.toInt(), - compressor.makeupGain.toInt() - ) - } else { - act.getString(R.string.compressor_off) - } - } - - fun updateThresholdLabel() { - binding.compressorThresholdLabel.text = - act.getString(R.string.compressor_threshold_label, compressor.threshold.toInt()) - } - - fun updateMakeupLabel() { - binding.compressorMakeupLabel.text = - act.getString(R.string.compressor_makeup_label, compressor.makeupGain.toInt()) - } - - // Short, plain-language explanation of what each slider does, updated live — - // same idea as the "Use this if the subtitles are shown X ms too late" hint. - fun updateThresholdHint() { - binding.compressorThresholdHint.text = - act.getString(R.string.compressor_threshold_hint, compressor.threshold.toInt()) - } - - fun updateMakeupHint() { - binding.compressorMakeupHint.text = - act.getString(R.string.compressor_makeup_hint, compressor.makeupGain.toInt()) - } - - // WhiteButton = selected/active, BlackButton = unselected — same as speed presets - fun syncEnableButtons() { - val ctx = context ?: return - listOf( - binding.compressorEnableBtt to compressor.enabled, - binding.compressorDisableBtt to !compressor.enabled, - ).forEach { (btn, active) -> - // Apply the full WhiteButton or BlackButton style — backgroundTint only. - // Also update setTextColor so we don't get white text on white background. - val styleAttr = if (active) R.style.WhiteButton else R.style.BlackButton - val ta = ctx.obtainStyledAttributes( - styleAttr, - intArrayOf( - com.google.android.material.R.attr.backgroundTint, - android.R.attr.textColor, - ) - ) - btn.backgroundTintList = ta.getColorStateList(0) - ta.getColorStateList(1)?.let { btn.setTextColor(it) } - ta.recycle() - } - updateCurrentLabel() - } - - // ── Presets — declared before the sliders so their listeners can call - // syncPresetButtons(matchingPresetButton()) to keep the highlight accurate ── - val allPresets = listOf( - binding.compressorPresetLight, - binding.compressorPresetDialog, - binding.compressorPresetHeavy, - ) - - fun syncPresetButtons(active: com.google.android.material.button.MaterialButton?) { - val ctx = context ?: return - allPresets.forEach { btn -> - val isActive = btn == active - val ta = ctx.obtainStyledAttributes( - if (isActive) R.style.WhiteButton else R.style.BlackButton, - intArrayOf( - com.google.android.material.R.attr.backgroundTint, - android.R.attr.textColor, - ) - ) - btn.backgroundTintList = ta.getColorStateList(0) - ta.getColorStateList(1)?.let { btn.setTextColor(it) } - ta.recycle() - } - } - - // Figures out which preset (if any) matches the current values, so the - // correct button stays highlighted when the dialog is reopened, instead - // of always resetting to "none selected". - fun matchingPresetButton(): com.google.android.material.button.MaterialButton? = when { - compressor.threshold == -18f && compressor.makeupGain == 4f -> binding.compressorPresetLight - compressor.threshold == -24f && compressor.makeupGain == 12f -> binding.compressorPresetDialog - compressor.threshold == -30f && compressor.makeupGain == 16f -> binding.compressorPresetHeavy - else -> null - } - - fun applyPreset( - threshold: Float, - makeup: Float, - activeBtn: com.google.android.material.button.MaterialButton - ) { - compressor.threshold = threshold - compressor.makeupGain = makeup - binding.compressorThresholdBar.value = threshold.coerceIn(-30f, 0f) - binding.compressorRatioBar.value = makeup.coerceIn(0f, 24f) - updateThresholdLabel() - updateMakeupLabel() - updateThresholdHint() - updateMakeupHint() - updateCurrentLabel() - syncPresetButtons(activeBtn) - } - - // ── Restore UI to current compressor state ───────────────────────── - binding.compressorThresholdBar.value = compressor.threshold.coerceIn(-30f, 0f) - binding.compressorRatioBar.value = compressor.makeupGain.coerceIn(0f, 24f) - updateThresholdLabel() - updateMakeupLabel() - updateThresholdHint() - updateMakeupHint() - syncEnableButtons() - syncPresetButtons(matchingPresetButton()) - - // ── On / Off ─────────────────────────────────────────────────────── - binding.compressorEnableBtt.setOnClickListener { - compressor.enabled = true - syncEnableButtons() - } - binding.compressorDisableBtt.setOnClickListener { - compressor.enabled = false - syncEnableButtons() - } - - // ── Threshold slider + FABs ──────────────────────────────────────── - binding.compressorThresholdBar.addOnChangeListener { _, value, fromUser -> - if (fromUser) { - compressor.threshold = value - updateThresholdLabel() - updateThresholdHint() - updateCurrentLabel() - syncPresetButtons(matchingPresetButton()) - } - } - binding.thresholdMinus.setOnClickListener { - val v = (compressor.threshold - 1f).coerceIn(-30f, 0f) - compressor.threshold = v - binding.compressorThresholdBar.value = v - updateThresholdLabel() - updateThresholdHint() - updateCurrentLabel() - syncPresetButtons(matchingPresetButton()) - } - binding.thresholdPlus.setOnClickListener { - val v = (compressor.threshold + 1f).coerceIn(-30f, 0f) - compressor.threshold = v - binding.compressorThresholdBar.value = v - updateThresholdLabel() - updateThresholdHint() - updateCurrentLabel() - syncPresetButtons(matchingPresetButton()) - } - - // ── Makeup gain slider + FABs (reusing ratio_minus/plus ids) ────── - binding.compressorRatioBar.addOnChangeListener { _, value, fromUser -> - if (fromUser) { - compressor.makeupGain = value - updateMakeupLabel() - updateMakeupHint() - updateCurrentLabel() - syncPresetButtons(matchingPresetButton()) - } - } - binding.ratioMinus.setOnClickListener { - val v = (compressor.makeupGain - 1f).coerceIn(0f, 24f) - compressor.makeupGain = v - binding.compressorRatioBar.value = v - updateMakeupLabel() - updateMakeupHint() - updateCurrentLabel() - syncPresetButtons(matchingPresetButton()) - } - binding.ratioPlus.setOnClickListener { - val v = (compressor.makeupGain + 1f).coerceIn(0f, 24f) - compressor.makeupGain = v - binding.compressorRatioBar.value = v - updateMakeupLabel() - updateMakeupHint() - updateCurrentLabel() - syncPresetButtons(matchingPresetButton()) - } - - // ── Preset buttons ───────────────────────────────────────────────── - binding.compressorPresetLight.setOnClickListener { - applyPreset(-18f, 4f, binding.compressorPresetLight) - } - binding.compressorPresetDialog.setOnClickListener { - applyPreset(-24f, 12f, binding.compressorPresetDialog) - } - binding.compressorPresetHeavy.setOnClickListener { - applyPreset(-30f, 16f, binding.compressorPresetHeavy) - } - - // ── Dialog lifecycle ─────────────────────────────────────────────── - val dialog = AlertDialog.Builder(act, R.style.AlertDialogCustom) - .setView(binding.root) - .setOnDismissListener { - activity?.hideSystemUI() - selectCompressorDialog = null - } - .create() - selectCompressorDialog = dialog - - binding.applyBtt.setOnClickListener { - saveCompressorSettings(compressor) - dialog.dismiss() - } - binding.resetBtt.setOnClickListener { - applyPreset(-24f, 12f, binding.compressorPresetDialog) - compressor.enabled = true - syncEnableButtons() - } - binding.cancelBtt.setOnClickListener { - compressor.enabled = snap.enabled - compressor.threshold = snap.threshold - compressor.makeupGain = snap.makeupGain - dialog.dismiss() - } - - dialog.show() - } - - private fun saveCompressorSettings(c: DynamicRangeCompressor) { - setKey(COMPRESSOR_ENABLED_KEY, c.enabled) - setKey(COMPRESSOR_THRESHOLD_KEY, c.threshold) - setKey(COMPRESSOR_RATIO_KEY, c.ratio) - setKey(COMPRESSOR_ATTACK_KEY, c.attackMs) - setKey(COMPRESSOR_RELEASE_KEY, c.releaseMs) - setKey(COMPRESSOR_MAKEUP_KEY, c.makeupGain) - } - - protected fun restoreCompressorSettings() { - val c = (player as? CS3IPlayer)?.compressor ?: return - c.restoreSavedSettings() - } - private fun onClickChange() { isShowing = !isShowing if (isShowing) autoHide() @@ -1046,8 +772,8 @@ open class FullScreenPlayer : AbstractPlayerFragment( val showPlayerEpisodes = !isGone && isThereEpisodes() playerEpisodesButtonRoot.isVisible = showPlayerEpisodes playerEpisodesButton.isVisible = showPlayerEpisodes - playerVideoTitleHolder.isGone = togglePlayerTitleGone || playerVideoTitle.text.isBlank() - playerVideoTitleRez.isGone = isGone || playerVideoTitleRez.text.isBlank() + playerVideoTitleHolder.isGone = togglePlayerTitleGone + playerVideoTitleRez.isGone = isGone playerEpisodeFiller.isGone = isGone playerCenterMenu.isGone = isGone playerLock.isGone = !isShowing @@ -1219,18 +945,12 @@ open class FullScreenPlayer : AbstractPlayerFragment( player.handleEvent(CSPlayerEvent.SkipCurrentChapter) } - KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER -> { // space is not captured due to navigation + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation player.handleEvent(CSPlayerEvent.PlayPauseToggle) } - // KEYCODE_DPAD_CENTER and KEYCODE_ENTER both act as a "select/confirm" button. - // Some remotes (e.g. LG Magic Remote) send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER. - // When the player UI or a dialog is visible, we let the event pass through (return null) - // so the focused button/item can handle the click normally, rather than always toggling - // play/pause. Only when the UI is hidden do we treat it as a play/pause toggle. - KeyEvent.KEYCODE_DPAD_CENTER, - KeyEvent.KEYCODE_ENTER -> { - if (isShowing || isDialogOpen()) { + KeyEvent.KEYCODE_DPAD_CENTER -> { + if (isShowing) { return null } // If UI is not shown make click instantly skip to next chapter even if locked @@ -1287,7 +1007,6 @@ open class FullScreenPlayer : AbstractPlayerFragment( } toggleEpisodesOverlay(true) } - else -> return null // Avoid capturing all input } return true @@ -1412,10 +1131,6 @@ open class FullScreenPlayer : AbstractPlayerFragment( ctx.getString(R.string.playback_speed_enabled_key), false ) - playBackCompressorEnabled = settingsManager.getBoolean( - ctx.getString(R.string.compressor_enabled_key), - false - ) playerRotateEnabled = settingsManager.getBoolean( ctx.getString(R.string.rotate_video_key), false @@ -1442,10 +1157,6 @@ open class FullScreenPlayer : AbstractPlayerFragment( } playerBinding?.apply { playerSpeedBtt.isVisible = playBackSpeedEnabled - playerCompressorBtt.isVisible = playBackCompressorEnabled - if (playBackCompressorEnabled) { - restoreCompressorSettings() - } playerResizeBtt.isVisible = playerResizeEnabled playerRotateBtt.isVisible = if (isLayout(TV or EMULATOR)) false else playerRotateEnabled @@ -1484,10 +1195,6 @@ open class FullScreenPlayer : AbstractPlayerFragment( } skipChapterButton.setOnClickListener { - // Switch focus for a better UX, as otherwise it is reset to a random button like "back button" - if(skipChapterButton.hasFocus()) { - playerPausePlay.requestFocus() - } player.handleEvent(CSPlayerEvent.SkipCurrentChapter) } @@ -1507,11 +1214,6 @@ open class FullScreenPlayer : AbstractPlayerFragment( showSpeedDialog() } - playerCompressorBtt.setOnClickListener { - autoHide() - showCompressorDialog() - } - playerSkipOp.setOnClickListener { autoHide() skipOp() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index e4616c93b..2dfd5ef4d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -8,7 +8,6 @@ import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.graphics.Bitmap -import android.graphics.Typeface import android.os.Build import android.os.Bundle import android.text.Spanned @@ -80,7 +79,6 @@ import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioT import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType import com.lagradost.cloudstream3.ui.player.source_priority.LinkSource -import com.lagradost.cloudstream3.ui.player.source_priority.ProfileSettings import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getLinkPriority import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog @@ -101,7 +99,6 @@ import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageTagIETF -import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragmentDialog2 import com.lagradost.cloudstream3.utils.AppContextUtils.getShortSeasonText import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs @@ -120,10 +117,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding -import com.lagradost.cloudstream3.utils.UIHelper.hideProgress import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage -import com.lagradost.cloudstream3.utils.UIHelper.showProgress import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.downloader.DownloadUtils.getImageBitmapFromUrl import com.lagradost.cloudstream3.utils.setText @@ -135,7 +130,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.io.Serializable -import java.lang.ref.WeakReference import java.util.Calendar import java.util.UUID import java.util.concurrent.ConcurrentHashMap @@ -513,8 +507,7 @@ class GeneratorPlayer : FullScreenPlayer() { showDownloadProgress(DownloadEvent(0, 0, 0, null)) - // uiReset() // Removed due to UX - + uiReset() currentSelectedLink = link // setEpisodes(viewModel.getAllMeta() ?: emptyList()) setPlayerDimen(null) @@ -797,58 +790,47 @@ class GeneratorPlayer : FullScreenPlayer() { } binding.applyBtt.setOnClickListener { - val currentSubtitle = currentSubtitle - if (currentSubtitle == null) { - dialog.dismissSafe() - return@setOnClickListener - } + currentSubtitle?.let { currentSubtitle -> + providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix }?.let { api -> + ioSafe { + when (val apiResource = + Resource.fromResult(api.resource(currentSubtitle))) { + is Resource.Success -> { + val subtitles = apiResource.value.getSubtitles().map { resource -> + SubtitleData( + originalName = resource.name ?: getName( + currentSubtitle, + true + ), + nameSuffix = "", + url = resource.url, + origin = resource.origin, + mimeType = resource.url.toSubtitleMimeType(), + headers = currentSubtitle.headers, + languageCode = currentSubtitle.lang + ) + } + if (subtitles.isEmpty()) { + showToast(R.string.no_subtitles) + return@ioSafe + } + runOnMainThread { + addAndSelectSubtitles(*subtitles.toTypedArray()) + } + } - val api = providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix } - if (api == null) { - dialog.dismissSafe() - return@setOnClickListener - } + is Resource.Failure -> { + showToast(apiResource.errorString) + } - binding.applyBtt.showProgress() - ioSafe { - val apiResource = - Resource.fromResult(api.resource(currentSubtitle)) - binding.applyBtt.hideProgress() - when (apiResource) { - is Resource.Success -> { - val subtitles = apiResource.value.getSubtitles().map { resource -> - SubtitleData( - originalName = resource.name ?: getName( - currentSubtitle, - true - ), - nameSuffix = "", - url = resource.url, - origin = resource.origin, - mimeType = resource.url.toSubtitleMimeType(), - headers = currentSubtitle.headers, - languageCode = currentSubtitle.lang - ) + is Resource.Loading -> { + // not possible + } } - if (subtitles.isEmpty()) { - showToast(R.string.no_subtitles) - return@ioSafe - } - dialog.dismissSafe() - runOnMainThread { - addAndSelectSubtitles(*subtitles.toTypedArray()) - } - } - - is Resource.Failure -> { - showToast(apiResource.errorString) - } - - is Resource.Loading -> { - // not possible } } } + dialog.dismissSafe() } dialog.setOnDismissListener { @@ -1047,7 +1029,7 @@ class GeneratorPlayer : FullScreenPlayer() { binding.subtitleSettingsBtt.setOnClickListener { safe { - val subtitlesFragment = SubtitlesFragmentDialog2() + val subtitlesFragment = SubtitlesFragment() subtitlesFragment.systemBarsAddPadding = true subtitlesFragment.show(this.parentFragmentManager, "SubtitleSettings") } @@ -1116,29 +1098,21 @@ class GeneratorPlayer : FullScreenPlayer() { var sourceIndex = 0 var startSource = 0 - // Filtered and sorted links - var currentHiddenFooter: View? = null - var filteredLinks: List = emptyList() + var sortedUrls = emptyList>() fun refreshLinks(qualityProfile: Int) { - val currentLinkUsed = currentSelectedLink - // Always display current linkFooter - val sortedLinks = viewModel.state.sortLinks(qualityProfile) - - filteredLinks = sortedLinks.filter { it.shouldUseLink || it.link == currentLinkUsed } - - if (sortedLinks.isEmpty()) { + sortedUrls = viewModel.state.sortLinks(qualityProfile) + if (sortedUrls.isEmpty()) { sourceDialog.findViewById(R.id.sort_sources_holder)?.isGone = true } else { - startSource = filteredLinks.indexOfFirst { it.link == currentLinkUsed } + startSource = sortedUrls.indexOf(currentSelectedLink) sourceIndex = startSource val sourcesArrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) - sourcesArrayAdapter.addAll(filteredLinks.map { displayLink -> - val (link, uri) = displayLink.link + sourcesArrayAdapter.addAll(sortedUrls.map { (link, uri) -> val name = link?.name ?: uri?.name ?: "NULL" "$name ${Qualities.getStringByInt(link?.quality)}" }) @@ -1154,7 +1128,7 @@ class GeneratorPlayer : FullScreenPlayer() { } providerList.setOnItemLongClickListener { _, _, position, _ -> - sortedLinks.getOrNull(position)?.link?.first?.url?.let { + sortedUrls.getOrNull(position)?.first?.url?.let { clipboardHelper( txt(R.string.video_source), it @@ -1162,25 +1136,6 @@ class GeneratorPlayer : FullScreenPlayer() { } true } - - val hiddenLinks = sortedLinks.size - filteredLinks.size - providerList.removeFooterView(currentHiddenFooter) - - if (hiddenLinks > 0) { - val hiddenLinksFooter: TextView = layoutInflater.inflate( - R.layout.sort_bottom_footer_add_choice, null - ) as TextView - - providerList.addFooterView(hiddenLinksFooter, null, false) - currentHiddenFooter = hiddenLinksFooter - - val hiddenLinksText = - ctx.resources.getQuantityString(R.plurals.links_hidden, hiddenLinks) - .format(hiddenLinks) - hiddenLinksFooter.text = hiddenLinksText - hiddenLinksFooter.setCompoundDrawables(null, null, null, null) - hiddenLinksFooter.setTypeface(null, Typeface.ITALIC) - } } } @@ -1394,8 +1349,8 @@ class GeneratorPlayer : FullScreenPlayer() { } } if (init) { - filteredLinks.getOrNull(sourceIndex)?.let { - loadLink(it.link, true) + sortedUrls.getOrNull(sourceIndex)?.let { + loadLink(it, true) } } sourceDialog.dismissSafe(activity) @@ -1563,10 +1518,6 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun playerError(exception: Throwable) { - currentSelectedLink?.let { link -> - viewModel.modifyState { this.addError(link) } - } - val currentUrl = currentSelectedLink?.let { it.first?.url ?: it.second?.uri?.toString() } ?: "unknown" val headers = currentSelectedLink?.first?.headers?.toString() ?: "none" @@ -1574,7 +1525,7 @@ class GeneratorPlayer : FullScreenPlayer() { Log.e( TAG, "playerError: $currentSelectedLink, " + - "type=${exception::class.qualifiedName}, " + + "type=${exception::class.java.canonicalName}, " + "message=${exception.message}, url=$currentUrl, headers=$headers, " + "referer=$referer, position=${player.getPosition() ?: "unknown"}, " + "duration=${player.getDuration() ?: "unknown"}, " + @@ -1589,22 +1540,8 @@ class GeneratorPlayer : FullScreenPlayer() { private fun noLinksFound() { viewModel.forceClearCache = true - val hiddenLinks = viewModel.state.sortLinks(currentQualityProfile).count { !it.shouldUseLink } - - context?.let { ctx -> - // Display that there are hidden links to the user. - if (hiddenLinks > 0) { - val noLinksString = ctx.getString(R.string.no_links_found_toast) - val hiddenString = - ctx.resources.getQuantityString(R.plurals.links_hidden, hiddenLinks) - .format(hiddenLinks) - val toastText = "$noLinksString\n($hiddenString)" - showToast(toastText, Toast.LENGTH_SHORT) - } else { - showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT) - } - } + showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT) activity?.popCurrentPage() } @@ -1615,9 +1552,7 @@ class GeneratorPlayer : FullScreenPlayer() { } val links = viewModel.state.sortLinks(currentQualityProfile) - - val firstAvailableLink = links.firstOrNull { it.shouldUseLink }?.link - if (firstAvailableLink == null) { + if (links.isEmpty()) { noLinksFound() return } @@ -1625,7 +1560,7 @@ class GeneratorPlayer : FullScreenPlayer() { if (!isPlayerActive.compareAndSet(false, true)) { return } - loadLink(firstAvailableLink, false) + loadLink(links.first(), false) showPlayerMetadata() } @@ -1649,7 +1584,7 @@ class GeneratorPlayer : FullScreenPlayer() { ) val meta = arrayOf( - load.tags?.takeIf { it.isNotEmpty() }?.take(6)?.joinToString(", "), + load.tags?.takeIf { it.isNotEmpty() }?.joinToString(", "), load.year?.toString(), if (!load.type.isMovieType()) context?.getShortSeasonText( @@ -1669,7 +1604,7 @@ class GeneratorPlayer : FullScreenPlayer() { if (!description.isNullOrBlank()) { descView.isVisible = true - descView.text = description.html() + descView.text = description } else { descView.isVisible = false @@ -1692,26 +1627,25 @@ class GeneratorPlayer : FullScreenPlayer() { } } - private fun getNextLink(): DisplayLink? { - val links = viewModel.state.sortLinks(currentQualityProfile) - val currentIndex = links.indexOfFirst { it.link == currentSelectedLink } - val nextPotentialLink = - links.withIndex().firstOrNull { it.index > currentIndex && it.value.shouldUseLink } - return nextPotentialLink?.value - } - override fun hasNextMirror(): Boolean { - return getNextLink() != null + val links = viewModel.state.sortLinks(currentQualityProfile) + return links.isNotEmpty() && links.indexOf(currentSelectedLink) + 1 < links.size } override fun nextMirror() { - val nextLink = getNextLink() - if (nextLink == null) { + val links = viewModel.state.sortLinks(currentQualityProfile) + if (links.isEmpty()) { noLinksFound() return } - loadLink(nextLink.link, true) + val newIndex = links.indexOf(currentSelectedLink) + 1 + if (newIndex >= links.size) { + noLinksFound() + return + } + + loadLink(links[newIndex], true) } override fun onDestroy() { @@ -1764,10 +1698,8 @@ class GeneratorPlayer : FullScreenPlayer() { if (settingsManager.getBoolean( ctx.getString(R.string.episode_sync_enabled_key), true ) - ) { - maxEpisodeSet = meta.episode - sync.modifyMaxEpisode(meta.totalEpisodeIndex ?: meta.episode) - } + ) maxEpisodeSet = meta.episode + sync.modifyMaxEpisode(meta.totalEpisodeIndex ?: meta.episode) } } @@ -1800,11 +1732,11 @@ class GeneratorPlayer : FullScreenPlayer() { ): SubtitleData? { val langCode = preferredAutoSelectSubtitles ?: return null if (downloads) { - sortSubs(subtitles).firstOrNull { + return sortSubs(subtitles).firstOrNull { it.origin == SubtitleOrigin.DOWNLOADED_FILE && it.matchesLanguageCode( langCode ) - }?.let { return it } + } } if (!settings) return null @@ -2218,7 +2150,6 @@ class GeneratorPlayer : FullScreenPlayer() { isPlayerActive.set(false) binding?.overlayLoadingSkipButton?.isVisible = false binding?.playerLoadingOverlay?.isVisible = true - viewModel.modifyState { setError(emptyList()) } uiReset() } @@ -2272,14 +2203,6 @@ class GeneratorPlayer : FullScreenPlayer() { fromTagToEnglishLanguageName(it)?.lowercase() ?: return@mapNotNull null } ?: listOf() } - - // Set up TV clock visibility - if (isLayout(TV)) { - val showTvClock = settingsManager.getBoolean(ctx.getString(R.string.tv_layout_clock_key), false) - playerBinding?.playerVideoClock?.isVisible = showTvClock - } else { - playerBinding?.playerVideoClock?.isVisible = false - } } unwrapBundle(savedInstanceState) @@ -2358,23 +2281,19 @@ class GeneratorPlayer : FullScreenPlayer() { } } - observe(viewModel.currentLinks) { (_, instance) -> + observe(viewModel.currentLinks) { (links, instance) -> if (instance != viewModel.state.instance) return@observe // Outdated observe - val sortedLinks = viewModel.state.sortLinks(currentQualityProfile) - val usableLinks = sortedLinks.count { link -> link.shouldUseLink } - - val turnVisible = usableLinks > 0 && viewModel.generator?.canSkipLoading == true + val turnVisible = links.isNotEmpty() && viewModel.generator?.canSkipLoading == true val wasGone = binding.overlayLoadingSkipButton.isGone binding.overlayLoadingSkipButton.apply { isVisible = turnVisible - - if (usableLinks == 0) { + if (links.isEmpty()) { setText(R.string.skip_loading) } else { @SuppressLint("SetTextI18n") - text = "${context.getString(R.string.skip_loading)} (${usableLinks})" + text = "${context.getString(R.string.skip_loading)} (${links.size})" } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt index dcf976612..ac25347b6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.content.Intent import android.net.Uri import androidx.core.content.ContextCompat.getString -import androidx.navigation.NavOptions import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.actions.temp.CloudStreamPackage import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson @@ -13,15 +12,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.safefile.SafeFile object OfflinePlaybackHelper { - /** - * Pop any existing player off the nav back stack before pushing the new one, - * keeping the stack flat (at most one player at a time). This prevents an - * OOM when many files are opened in sequence via DownloadedPlayerActivity. - */ - private val replacePlayerNavOptions = NavOptions.Builder() - .setPopUpTo(R.id.navigation_player, inclusive = true, saveState = false) - .build() - fun playLink(activity: Activity, url: String) { activity.navigate( R.id.global_to_navigation_player, GeneratorPlayer.newInstance( @@ -30,8 +20,7 @@ object OfflinePlaybackHelper { BasicLink(url) ), id = url.hashCode() ), 0 - ), - replacePlayerNavOptions + ) ) } @@ -63,8 +52,7 @@ object OfflinePlaybackHelper { subs, if (id != -1) id else null, ), 0 - ), - replacePlayerNavOptions + ) ) return true } @@ -88,8 +76,7 @@ object OfflinePlaybackHelper { ) ) ), 0 - ), - replacePlayerNavOptions + ) ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index cb8cf8bff..e3c390d50 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -10,8 +10,6 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall -import com.lagradost.cloudstream3.ui.player.source_priority.ProfileSettings -import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getLinkPriority import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -42,20 +40,12 @@ data class GeneratorState( val id: Int?, ) -data class DisplayLink( - val link: VideoLink, - // If the link should be displayed and used by the player - val shouldUseLink: Boolean, - val priority: Int -) - /** Immutable state of all current links relevant to displaying the video */ // @MustUseReturnValues // @Immutable data class VideoState( val subtitles: PersistentSet = persistentSetOf(), val links: PersistentSet = persistentSetOf(), - val erroredLinks: PersistentSet = persistentSetOf(), val stamps: PersistentList = persistentListOf(), val loading: Resource = Resource.Loading(), val generatorState: GeneratorState? = null, @@ -66,52 +56,19 @@ data class VideoState( * * sortedBy is not exactly expensive, but each hasNextMirror does it again, so this alleviates unnecessary recomputation * */ - private val sortedLinks: ConcurrentHashMap> = ConcurrentHashMap() + private val sortedLinks: ConcurrentHashMap> = ConcurrentHashMap() - /** - * The cache is guaranteed to be up to date link-wise due to the immutable links. - * However, hideNegativeSources and hideErrorSources could be updated, which requires clearing the cache. - */ fun clearSortedLinksCache() = sortedLinks.clear() - private fun hasLinkErrored(link: VideoLink): Boolean { - return erroredLinks.any { it == link } - } - - private fun VideoLink.toDisplayLink( - qualityProfile: Int, - hideNegativeSources: Boolean, - hideErrorSources: Boolean - ): DisplayLink { - val priority = getLinkPriority(qualityProfile, this.first) - val shouldHideLink = - (hideNegativeSources && priority < 0) || (hideErrorSources && hasLinkErrored(this)) - val displayLink = DisplayLink(this, !shouldHideLink, priority) - - return displayLink - } - // Modifying sortedLinks is not considered a "visible" side effect, and rerunning it does not change the result // It is by all standards, idempotent and by extension also pure as it has no "visible" side effect /** Returns .links in the sorted order according to the qualityProfile. * Use .links if order is not needed */ @Contract(pure = true) - fun sortLinks(qualityProfile: Int): List { - sortedLinks[qualityProfile]?.let { - return it - } - - val hideNegativeSources = - QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideNegativeSources) - val hideErrorSources = - QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideErrorSources) - - return links.map { link -> + fun sortLinks(qualityProfile: Int): List { + return sortedLinks[qualityProfile] ?: links.sortedBy { link -> // negative because we want to sort highest quality first - link.toDisplayLink(qualityProfile, hideNegativeSources, hideErrorSources) - }.sortedBy { - // negative because we want to sort highest quality first - -it.priority + -getLinkPriority(qualityProfile, link.first) }.also { value -> sortedLinks[qualityProfile] = value } } @@ -156,12 +113,6 @@ data class VideoState( @JvmName("setVideoSkipStamp") @Contract(pure = true) fun set(items: Collection): VideoState = copy(stamps = items.toPersistentList()) - - @Contract(pure = true) - fun addError(item: VideoLink): VideoState = copy(erroredLinks = erroredLinks.add(item)) - - @Contract(pure = true) - fun setError(items: Collection): VideoState = copy(erroredLinks = items.toPersistentSet()) } data class VideoLive( @@ -190,8 +141,9 @@ class PlayerGeneratorViewModel : ViewModel() { var state = VideoState(instance = 0) private set - private val _currentLinks = MutableLiveData>>(null) - val currentLinks: LiveData>> = _currentLinks + private val _currentLinks = + MutableLiveData>>>(null) + val currentLinks: LiveData>>> = _currentLinks private val _currentSubtitles = MutableLiveData>>(null) val currentSubtitles: LiveData>> = _currentSubtitles diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGestureHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGestureHelper.kt index d3340cb57..1c7086d12 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGestureHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGestureHelper.kt @@ -887,6 +887,7 @@ class PlayerGestureHelper(private val playerView: PlayerView) { val wasShowing = playerView.callbacks?.isUIShowing() ?: false // Only expose the parent chain when controls are currently hidden. + val prevCenterMenuGone = playerView.playerCenterMenu?.isGone ?: false val prevVideoHolderVisible = playerView.playerVideoHolder?.isVisible ?: true if (!wasShowing) { playerView.playerCenterMenu?.isGone = false @@ -905,7 +906,7 @@ class PlayerGestureHelper(private val playerView: PlayerView) { resetRewindText() // Restore parent chain only if we changed it and controls are still hidden. if (!wasShowing && !(playerView.callbacks?.isUIShowing() ?: false)) { - playerView.playerCenterMenu?.isGone = true + playerView.playerCenterMenu?.isGone = prevCenterMenuGone playerView.playerVideoHolder?.isVisible = prevVideoHolderVisible rewHolder.alpha = 0f } @@ -926,6 +927,7 @@ class PlayerGestureHelper(private val playerView: PlayerView) { val ffwdText = playerView.exoFfwdText val wasShowing = playerView.callbacks?.isUIShowing() ?: false + val prevCenterMenuGone = playerView.playerCenterMenu?.isGone ?: false val prevVideoHolderVisible = playerView.playerVideoHolder?.isVisible ?: true if (!wasShowing) { playerView.playerCenterMenu?.isGone = false @@ -943,7 +945,7 @@ class PlayerGestureHelper(private val playerView: PlayerView) { ffwdText?.post { resetFastForwardText() if (!wasShowing && !(playerView.callbacks?.isUIShowing() ?: false)) { - playerView.playerCenterMenu?.isGone = true + playerView.playerCenterMenu?.isGone = prevCenterMenuGone playerView.playerVideoHolder?.isVisible = prevVideoHolderVisible ffwdHolder.alpha = 0f } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index f62bad58d..ee6170aa5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -8,14 +8,11 @@ import androidx.annotation.OptIn import androidx.media3.common.MimeTypes import androidx.media3.common.util.UnstableApi import androidx.media3.ui.SubtitleView -import com.fasterxml.jackson.annotation.JsonIgnore import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.setSubtitleViewStyle import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable enum class SubtitleStatus { IS_ACTIVE, @@ -35,19 +32,17 @@ enum class SubtitleOrigin { * @param url Url for the subtitle, when EMBEDDED_IN_VIDEO this variable is used as the real backend id * @param headers if empty it will use the base onlineDataSource headers else only the specified headers * @param languageCode usually, tags such as "en", "es-mx", or "zh-hant-TW". But it could be something like "English 4" - */ -@Serializable + * */ data class SubtitleData( - @SerialName("originalName") val originalName: String, - @SerialName("nameSuffix") val nameSuffix: String, - @SerialName("url") val url: String, - @SerialName("origin") val origin: SubtitleOrigin, - @SerialName("mimeType") val mimeType: String, - @SerialName("headers") val headers: Map, - @SerialName("languageCode") val languageCode: String?, + val originalName: String, + val nameSuffix: String, + val url: String, + val origin: SubtitleOrigin, + val mimeType: String, + val headers: Map, + val languageCode: String?, ) { - /** Internal ID for media3, unique for each link. */ - @JsonIgnore + /** Internal ID for exoplayer, unique for each link*/ fun getId(): String { return if (origin == SubtitleOrigin.EMBEDDED_IN_VIDEO) url else "$url|$name" @@ -59,22 +54,22 @@ data class SubtitleData( } /** Tries hard to figure out a valid IETF tag based on language code and name. Will return null if not found. */ - @JsonIgnore fun getIETF_tag(): String? { return fromLanguageToTagIETF(this.languageCode) ?: fromLanguageToTagIETF(this.originalName, halfMatch = true) } - @SerialName("name") val name = "$originalName $nameSuffix" + val name = "$originalName $nameSuffix" /** * Gets the URL, but tries to fix it if it is malformed. */ - @JsonIgnore fun getFixedUrl(): String { // Some extensions fail to include the protocol, this helps with that. val fixedSubUrl = if (this.url.startsWith("//")) { "https:${this.url}" - } else this.url + } else { + this.url + } return fixedSubUrl } } @@ -147,4 +142,4 @@ class PlayerSubtitleHelper { setSubStyle(it) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/Torrent.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/Torrent.kt index 7e5d04eb0..2e554f75e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/Torrent.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/Torrent.kt @@ -9,8 +9,6 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.newExtractorLink -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import torrServer.TorrServer import java.io.File import java.net.ConnectException @@ -34,14 +32,14 @@ object Torrent { /** Returns true if the server is up */ private suspend fun echo(): Boolean { - if (TORRENT_SERVER_URL.isEmpty()) { + if(TORRENT_SERVER_URL.isEmpty()) { return false } return try { app.get( "$TORRENT_SERVER_URL/echo", ).text.isNotEmpty() - } catch (_: ConnectException) { + } catch (e: ConnectException) { // `Failed to connect to /127.0.0.1:8090` if the server is down false } catch (t: Throwable) { @@ -54,7 +52,7 @@ object Torrent { /** Gracefully shutdown the server. * should not be used because I am unable to start it again, and the stopTorrentServer() crashes the app */ suspend fun shutdown(): Boolean { - if (TORRENT_SERVER_URL.isEmpty()) { + if(TORRENT_SERVER_URL.isEmpty()) { return false } return try { @@ -70,7 +68,7 @@ object Torrent { /** Lists all torrents by the server */ @Throws private suspend fun list(): Array { - if (TORRENT_SERVER_URL.isEmpty()) { + if(TORRENT_SERVER_URL.isEmpty()) { throw ErrorLoadingException("Not initialized") } return app.post( @@ -85,7 +83,7 @@ object Torrent { /** Drops a single torrent, (I think) this means closing the stream. Returns returns if it is successful */ private suspend fun drop(hash: String): Boolean { - if (TORRENT_SERVER_URL.isEmpty()) { + if(TORRENT_SERVER_URL.isEmpty()) { return false } return try { @@ -106,7 +104,7 @@ object Torrent { /** Removes a single torrent from the server registry */ private suspend fun rem(hash: String): Boolean { - if (TORRENT_SERVER_URL.isEmpty()) { + if(TORRENT_SERVER_URL.isEmpty()) { return false } return try { @@ -128,7 +126,7 @@ object Torrent { /** Removes all torrents from the server, and returns if it is successful */ suspend fun clearAll(): Boolean { - if (TORRENT_SERVER_URL.isEmpty()) { + if(TORRENT_SERVER_URL.isEmpty()) { return true } return try { @@ -166,8 +164,10 @@ object Torrent { /** Gets all the metadata of a torrent, will throw if that hash does not exists * https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/c18f58e51b6738f053261bc863177078aa9c1c98/web/api/torrents.go#L126 */ @Throws - suspend fun get(hash: String): TorrentStatus { - if (TORRENT_SERVER_URL.isEmpty()) { + suspend fun get( + hash: String, + ): TorrentStatus { + if(TORRENT_SERVER_URL.isEmpty()) { throw ErrorLoadingException("Not initialized") } return app.post( @@ -184,7 +184,7 @@ object Torrent { /** Adds a torrent to the server, this is needed for us to get the hash for further modification, as well as start streaming it*/ @Throws private suspend fun add(url: String): TorrentStatus { - if (TORRENT_SERVER_URL.isEmpty()) { + if(TORRENT_SERVER_URL.isEmpty()) { throw ErrorLoadingException("Not initialized") } return app.post( @@ -204,7 +204,7 @@ object Torrent { return true } val port = TorrServer.startTorrentServer(dir, 0) - if (port < 0) { + if(port < 0) { return false } TORRENT_SERVER_URL = "http://127.0.0.1:$port" @@ -278,55 +278,94 @@ object Torrent { // https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/c18f58e51b6738f053261bc863177078aa9c1c98/web/api/torrents.go#L18 // https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/main/web/api/route.go#L7 - @Serializable data class TorrentRequest( - @JsonProperty("action") @SerialName("action") val action: String, - @JsonProperty("hash") @SerialName("hash") val hash: String = "", - @JsonProperty("link") @SerialName("link") val link: String = "", - @JsonProperty("title") @SerialName("title") val title: String = "", - @JsonProperty("poster") @SerialName("poster") val poster: String = "", - @JsonProperty("data") @SerialName("data") val data: String = "", - @JsonProperty("save_to_db") @SerialName("save_to_db") val saveToDB: Boolean = false, + @JsonProperty("action") + val action: String, + @JsonProperty("hash") + val hash: String = "", + @JsonProperty("link") + val link: String = "", + @JsonProperty("title") + val title: String = "", + @JsonProperty("poster") + val poster: String = "", + @JsonProperty("data") + val data: String = "", + @JsonProperty("save_to_db") + val saveToDB: Boolean = false, ) // https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/c18f58e51b6738f053261bc863177078aa9c1c98/torr/state/state.go#L33 // omitempty = nullable - @Serializable data class TorrentStatus( - @JsonProperty("title") @SerialName("title") var title: String, - @JsonProperty("poster") @SerialName("poster") var poster: String, - @JsonProperty("data") @SerialName("data") var data: String?, - @JsonProperty("timestamp") @SerialName("timestamp") var timestamp: Long, - @JsonProperty("name") @SerialName("name") var name: String?, - @JsonProperty("hash") @SerialName("hash") var hash: String?, - @JsonProperty("stat") @SerialName("stat") var stat: Int, - @JsonProperty("stat_string") @SerialName("stat_string") var statString: String, - @JsonProperty("loaded_size") @SerialName("loaded_size") var loadedSize: Long?, - @JsonProperty("torrent_size") @SerialName("torrent_size") var torrentSize: Long?, - @JsonProperty("preloaded_bytes") @SerialName("preloaded_bytes") var preloadedBytes: Long?, - @JsonProperty("preload_size") @SerialName("preload_size") var preloadSize: Long?, - @JsonProperty("download_speed") @SerialName("download_speed") var downloadSpeed: Double?, - @JsonProperty("upload_speed") @SerialName("upload_speed") var uploadSpeed: Double?, - @JsonProperty("total_peers") @SerialName("total_peers") var totalPeers: Int?, - @JsonProperty("pending_peers") @SerialName("pending_peers") var pendingPeers: Int?, - @JsonProperty("active_peers") @SerialName("active_peers") var activePeers: Int?, - @JsonProperty("connected_seeders") @SerialName("connected_seeders") var connectedSeeders: Int?, - @JsonProperty("half_open_peers") @SerialName("half_open_peers") var halfOpenPeers: Int?, - @JsonProperty("bytes_written") @SerialName("bytes_written") var bytesWritten: Long?, - @JsonProperty("bytes_written_data") @SerialName("bytes_written_data") var bytesWrittenData: Long?, - @JsonProperty("bytes_read") @SerialName("bytes_read") var bytesRead: Long?, - @JsonProperty("bytes_read_data") @SerialName("bytes_read_data") var bytesReadData: Long?, - @JsonProperty("bytes_read_useful_data") @SerialName("bytes_read_useful_data") var bytesReadUsefulData: Long?, - @JsonProperty("chunks_written") @SerialName("chunks_written") var chunksWritten: Long?, - @JsonProperty("chunks_read") @SerialName("chunks_read") var chunksRead: Long?, - @JsonProperty("chunks_read_useful") @SerialName("chunks_read_useful") var chunksReadUseful: Long?, - @JsonProperty("chunks_read_wasted") @SerialName("chunks_read_wasted") var chunksReadWasted: Long?, - @JsonProperty("pieces_dirtied_good") @SerialName("pieces_dirtied_good") var piecesDirtiedGood: Long?, - @JsonProperty("pieces_dirtied_bad") @SerialName("pieces_dirtied_bad") var piecesDirtiedBad: Long?, - @JsonProperty("duration_seconds") @SerialName("duration_seconds") var durationSeconds: Double?, - @JsonProperty("bit_rate") @SerialName("bit_rate") var bitRate: String?, - @JsonProperty("file_stats") @SerialName("file_stats") var fileStats: List?, - @JsonProperty("trackers") @SerialName("trackers") var trackers: List?, + @JsonProperty("title") + var title: String, + @JsonProperty("poster") + var poster: String, + @JsonProperty("data") + var data: String?, + @JsonProperty("timestamp") + var timestamp: Long, + @JsonProperty("name") + var name: String?, + @JsonProperty("hash") + var hash: String?, + @JsonProperty("stat") + var stat: Int, + @JsonProperty("stat_string") + var statString: String, + @JsonProperty("loaded_size") + var loadedSize: Long?, + @JsonProperty("torrent_size") + var torrentSize: Long?, + @JsonProperty("preloaded_bytes") + var preloadedBytes: Long?, + @JsonProperty("preload_size") + var preloadSize: Long?, + @JsonProperty("download_speed") + var downloadSpeed: Double?, + @JsonProperty("upload_speed") + var uploadSpeed: Double?, + @JsonProperty("total_peers") + var totalPeers: Int?, + @JsonProperty("pending_peers") + var pendingPeers: Int?, + @JsonProperty("active_peers") + var activePeers: Int?, + @JsonProperty("connected_seeders") + var connectedSeeders: Int?, + @JsonProperty("half_open_peers") + var halfOpenPeers: Int?, + @JsonProperty("bytes_written") + var bytesWritten: Long?, + @JsonProperty("bytes_written_data") + var bytesWrittenData: Long?, + @JsonProperty("bytes_read") + var bytesRead: Long?, + @JsonProperty("bytes_read_data") + var bytesReadData: Long?, + @JsonProperty("bytes_read_useful_data") + var bytesReadUsefulData: Long?, + @JsonProperty("chunks_written") + var chunksWritten: Long?, + @JsonProperty("chunks_read") + var chunksRead: Long?, + @JsonProperty("chunks_read_useful") + var chunksReadUseful: Long?, + @JsonProperty("chunks_read_wasted") + var chunksReadWasted: Long?, + @JsonProperty("pieces_dirtied_good") + var piecesDirtiedGood: Long?, + @JsonProperty("pieces_dirtied_bad") + var piecesDirtiedBad: Long?, + @JsonProperty("duration_seconds") + var durationSeconds: Double?, + @JsonProperty("bit_rate") + var bitRate: String?, + @JsonProperty("file_stats") + var fileStats: List?, + @JsonProperty("trackers") + var trackers: List?, ) { fun streamUrl(url: String): String { val fileName = @@ -342,10 +381,12 @@ object Torrent { } } - @Serializable data class TorrentFileStat( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("path") @SerialName("path") val path: String?, - @JsonProperty("length") @SerialName("length") val length: Long?, + @JsonProperty("id") + val id: Int?, + @JsonProperty("path") + val path: String?, + @JsonProperty("length") + val length: Long?, ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt index d8d47bd10..02470484e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt @@ -12,16 +12,12 @@ import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities -import java.util.EnumMap -import java.util.concurrent.ConcurrentHashMap -import kotlin.also import kotlin.math.abs object QualityDataHelper { private const val VIDEO_SOURCE_PRIORITY = "video_source_priority" private const val VIDEO_PROFILE_NAME = "video_profile_name" private const val VIDEO_QUALITY_PRIORITY = "video_quality_priority" - const val VIDEO_PROFILE_SETTINGS = "video_profile_settings" // Old key only supporting one type per profile @Deprecated("Changed to support multiple types per profile") @@ -57,21 +53,13 @@ object QualityDataHelper { val types: Set ) - - // Map profile and name to priority - val sourcePriorityCache: ConcurrentHashMap> = ConcurrentHashMap() - fun getSourcePriority(profile: Int, name: String?): Int { if (name == null) return DEFAULT_SOURCE_PRIORITY - - return sourcePriorityCache[profile]?.get(name) ?: (getKey( + return getKey( "$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile", name, DEFAULT_SOURCE_PRIORITY - ) ?: DEFAULT_SOURCE_PRIORITY).also { - sourcePriorityCache.getOrPut(profile) { hashMapOf() } - sourcePriorityCache[profile]?.set(name, it) - } + ) ?: DEFAULT_SOURCE_PRIORITY } fun getAllSourcePriorityNames(profile: Int): List { @@ -89,8 +77,6 @@ object QualityDataHelper { } else { setKey(folder, name, priority) } - - sourcePriorityCache[profile]?.set(name, priority) } fun setProfileName(profile: Int, name: String?) { @@ -107,17 +93,12 @@ object QualityDataHelper { ?: txt(R.string.profile_number, profile) } - // Map profile and quality to priority - val qualityPriorityCache: ConcurrentHashMap> = ConcurrentHashMap() fun getQualityPriority(profile: Int, quality: Qualities): Int { - return qualityPriorityCache[profile]?.get(quality) ?: (getKey( + return getKey( "$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile", quality.value.toString(), quality.defaultPriority - )?.also { - qualityPriorityCache.getOrPut(profile) { EnumMap(Qualities::class.java) } - qualityPriorityCache[profile]?.set(quality, it) - }) ?: quality.defaultPriority + ) ?: quality.defaultPriority } fun setQualityPriority(profile: Int, quality: Qualities, priority: Int) { @@ -126,24 +107,8 @@ object QualityDataHelper { quality.value.toString(), priority ) - qualityPriorityCache[profile]?.set(quality, priority) } - fun setProfileSetting(profile: Int, setting: ProfileSettings, value: T) { - val folder = "$currentAccount/$VIDEO_PROFILE_SETTINGS/$profile" - // Prevent unnecessary keys - if (value == setting.defaultValue) { - removeKey(folder, setting.key) - } else { - setKey(folder, setting.key, value) - } - } - - inline fun getProfileSetting(profile: Int, setting: ProfileSettings): T { - val folder = "$currentAccount/$VIDEO_PROFILE_SETTINGS/$profile" - val value = getKey(folder, setting.key) - return value ?: setting.defaultValue - } @Suppress("DEPRECATION") fun getQualityProfileTypes(profile: Int): Set { @@ -258,9 +223,4 @@ object QualityDataHelper { if (target == null) return Qualities.Unknown return Qualities.entries.minBy { abs(it.value - target) } } -} - -sealed class ProfileSettings(val key: String, val defaultValue: T) { - object HideErrorSources : ProfileSettings("hide_error_sources", false) - object HideNegativeSources : ProfileSettings("hide_negative_sources", false) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt index e1b8ad479..6a0f12e9a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.player.source_priority -import android.app.Activity import android.app.Dialog import androidx.annotation.StyleRes import androidx.core.view.isVisible @@ -27,7 +26,7 @@ data class LinkSource( class QualityProfileDialog private constructor( - val activity: Activity, + val activity: FragmentActivity, @StyleRes val themeRes: Int, private val links: List, private val usedProfile: Int?, @@ -35,7 +34,7 @@ class QualityProfileDialog private constructor( private val useProfileSelection: Boolean ) : Dialog(activity, themeRes) { constructor( - activity: Activity, + activity: FragmentActivity, @StyleRes themeRes: Int, links: List, usedProfile: Int, @@ -43,7 +42,7 @@ class QualityProfileDialog private constructor( ) : this(activity, themeRes, links, usedProfile, profileSelectionCallback, true) constructor( - activity: Activity, + activity: FragmentActivity, @StyleRes themeRes: Int, links: List ) : this(activity, themeRes, links, null, null, false) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt index 604ec3149..c8ac96ebb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt @@ -14,7 +14,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding class SourcePriorityDialog( val ctx: Context, - @StyleRes val themeRes: Int, + @StyleRes themeRes: Int, val links: List, private val profile: QualityDataHelper.QualityProfile, /** @@ -28,79 +28,76 @@ class SourcePriorityDialog( PlayerSelectSourcePriorityBinding.inflate(LayoutInflater.from(ctx), null, false) setContentView(binding.root) fixSystemBarsPadding(binding.root) + val sourcesRecyclerView = binding.sortSources + val qualitiesRecyclerView = binding.sortQualities + val profileText = binding.profileTextEditable + val saveBtt = binding.saveBtt + val exitBtt = binding.closeBtt + val helpBtt = binding.helpBtt - binding.apply { - profileTextEditable.setText( - QualityDataHelper.getProfileName(profile.id).asString(context) - ) - profileTextEditable.hint = txt(R.string.profile_number, profile.id).asString(context) + profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context)) + profileText.hint = txt(R.string.profile_number, profile.id).asString(context) - sortSources.adapter = PriorityAdapter( - ).apply { - val sortedLinks = links.map { link -> - SourcePriority( - null, - link.source, - QualityDataHelper.getSourcePriority(profile.id, link.source) - ) - }.distinctBy { it.name }.sortedBy { -it.priority } - - submitList(sortedLinks) - } - - sortQualities.adapter = PriorityAdapter( - ).apply { - submitList(Qualities.entries.mapNotNull { - SourcePriority( - it, - Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, - QualityDataHelper.getQualityPriority(profile.id, it) - ) - }.sortedBy { -it.priority }) - } - - @Suppress("UNCHECKED_CAST") // We know the types - saveBtt.setOnClickListener { - val qualityAdapter = sortQualities.adapter as? PriorityAdapter - val sourcesAdapter = sortSources.adapter as? PriorityAdapter - - val qualities = qualityAdapter?.immutableCurrentList ?: emptyList() - val sources = sourcesAdapter?.immutableCurrentList ?: emptyList() - - qualities.forEach { - QualityDataHelper.setQualityPriority(profile.id, it.data, it.priority) - } - - sources.forEach { - QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority) - } - - qualityAdapter?.submitList(qualities.sortedBy { -it.priority }) - sourcesAdapter?.submitList(sources.sortedBy { -it.priority }) - - val savedProfileName = profileTextEditable.text.toString() - if (savedProfileName.isBlank()) { - QualityDataHelper.setProfileName(profile.id, null) - } else { - QualityDataHelper.setProfileName(profile.id, savedProfileName) - } - updatedCallback.invoke() - } - - closeBtt.setOnClickListener { - dismissSafe() - } - - helpBtt.setOnClickListener { - AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { - setMessage(R.string.quality_profile_help) - }.show() - } - - settingsBtt.setOnClickListener { - SourceProfileSettingsDialog(ctx, themeRes, profile.id).show() - } + sourcesRecyclerView.adapter = PriorityAdapter( + ).apply { + submitList(links.map { link -> + SourcePriority( + null, + link.source, + QualityDataHelper.getSourcePriority(profile.id, link.source) + ) + }.distinctBy { it.name }.sortedBy { -it.priority }) } + + qualitiesRecyclerView.adapter = PriorityAdapter( + ).apply { + submitList(Qualities.entries.mapNotNull { + SourcePriority( + it, + Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, + QualityDataHelper.getQualityPriority(profile.id, it) + ) + }.sortedBy { -it.priority }) + } + + @Suppress("UNCHECKED_CAST") // We know the types + saveBtt.setOnClickListener { + val qualityAdapter = qualitiesRecyclerView.adapter as? PriorityAdapter + val sourcesAdapter = sourcesRecyclerView.adapter as? PriorityAdapter + + val qualities = qualityAdapter?.immutableCurrentList ?: emptyList() + val sources = sourcesAdapter?.immutableCurrentList ?: emptyList() + + qualities.forEach { + QualityDataHelper.setQualityPriority(profile.id, it.data, it.priority) + } + + sources.forEach { + QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority) + } + + qualityAdapter?.submitList(qualities.sortedBy { -it.priority }) + sourcesAdapter?.submitList(sources.sortedBy { -it.priority }) + + val savedProfileName = profileText.text.toString() + if (savedProfileName.isBlank()) { + QualityDataHelper.setProfileName(profile.id, null) + } else { + QualityDataHelper.setProfileName(profile.id, savedProfileName) + } + updatedCallback.invoke() + } + + exitBtt.setOnClickListener { + this.dismissSafe() + } + + helpBtt.setOnClickListener { + AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { + setMessage(R.string.quality_profile_help) + }.show() + } + super.show() } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt deleted file mode 100644 index c26955b5e..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.lagradost.cloudstream3.ui.player.source_priority - -import android.app.Dialog -import android.content.Context -import android.view.LayoutInflater -import androidx.annotation.StyleRes -import com.lagradost.cloudstream3.databinding.SourceProfileSettingsDialogBinding -import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding - -class SourceProfileSettingsDialog( - val ctx: Context, - @StyleRes themeRes: Int, - val profile: Int -) : Dialog(ctx, themeRes) { - override fun show() { - val binding = - SourceProfileSettingsDialogBinding.inflate(LayoutInflater.from(ctx), null, false) - setContentView(binding.root) - fixSystemBarsPadding(binding.root) - - binding.apply { - var hideErrorSources = QualityDataHelper.getProfileSetting(profile, ProfileSettings.HideErrorSources) - var hideNegativeSources = QualityDataHelper.getProfileSetting(profile, ProfileSettings.HideNegativeSources) - - profileHideErrorSources.isChecked = hideErrorSources - profileHideErrorSources.setOnCheckedChangeListener { _, bool -> - hideErrorSources = bool - } - - profileHideNegativeSources.isChecked = hideNegativeSources - profileHideNegativeSources.setOnCheckedChangeListener { _, bool -> - hideNegativeSources = bool - } - - applyBtt.setOnClickListener { - QualityDataHelper.setProfileSetting(profile, ProfileSettings.HideErrorSources, hideErrorSources) - QualityDataHelper.setProfileSetting(profile, ProfileSettings.HideNegativeSources, hideNegativeSources) - dismissSafe() - } - - cancelBtt.setOnClickListener { - dismissSafe() - } - } - super.show() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index fe8c617f2..056588d0b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -5,9 +5,6 @@ import android.content.Intent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.animation.Animation -import android.view.animation.OvershootInterpolator -import android.view.animation.ScaleAnimation import androidx.core.view.isVisible import com.lagradost.cloudstream3.ActorData import com.lagradost.cloudstream3.ActorRole @@ -49,24 +46,6 @@ class ActorAdaptor( } } - override fun onUpdateContent(holder: ViewHolderState, item: ActorData, position: Int) { - when (val binding = holder.view) { - is CastItemBinding -> { - val anim: Animation = ScaleAnimation( - 0.8f, 1f, - 0.8f, 1f, - Animation.RELATIVE_TO_SELF, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f - ) - anim.fillAfter = true - anim.duration = 200 - anim.interpolator = OvershootInterpolator() - binding.voiceActorImageHolder2.startAnimation(anim) - } - } - super.onUpdateContent(holder, item, position) - } - override fun onBindContent(holder: ViewHolderState, item: ActorData, position: Int) { when (val binding = holder.view) { is CastItemBinding -> { @@ -158,4 +137,4 @@ class ActorAdaptor( } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 71909c5d3..cbf94fd97 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -21,8 +21,6 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.ImageLoader.loadImage import com.lagradost.cloudstream3.utils.UiImage -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable const val START_ACTION_RESUME_LATEST = 1 const val START_ACTION_LOAD_EP = 2 @@ -36,32 +34,33 @@ enum class VideoWatchState { Watched } -@Serializable data class ResultEpisode( - @SerialName("headerName") val headerName: String, - @SerialName("name") val name: String?, - @SerialName("poster") val poster: String?, - @SerialName("episode") val episode: Int, - @SerialName("seasonIndex") val seasonIndex: Int?, // this is the "season" index used season names - @SerialName("season") val season: Int?, // this is the display - @SerialName("data") val data: String, - @SerialName("apiName") val apiName: String, - @SerialName("id") val id: Int, - @SerialName("index") val index: Int, - @SerialName("position") val position: Long, // time in MS - @SerialName("duration") val duration: Long, // duration in MS - @SerialName("score") val score: Score?, - @SerialName("description") val description: String?, - @SerialName("isFiller") val isFiller: Boolean?, - @SerialName("tvType") val tvType: TvType, - @SerialName("parentId") val parentId: Int, - /** Conveys if the episode itself is marked as watched. */ - @SerialName("videoWatchState") val videoWatchState: VideoWatchState, - /** Sum of all previous season episode counts + episode. */ - @SerialName("totalEpisodeIndex") val totalEpisodeIndex: Int? = null, - @SerialName("airDate") val airDate: Long? = null, - @SerialName("runTime") val runTime: Int? = null, - @SerialName("seasonData") val seasonData: SeasonData? = null, + val headerName: String, + val name: String?, + val poster: String?, + val episode: Int, + val seasonIndex: Int?, // this is the "season" index used season names + val season: Int?, // this is the display + val data: String, + val apiName: String, + val id: Int, + val index: Int, + val position: Long, // time in MS + val duration: Long, // duration in MS + val score: Score?, + val description: String?, + val isFiller: Boolean?, + val tvType: TvType, + val parentId: Int, + /** + * Conveys if the episode itself is marked as watched + **/ + val videoWatchState: VideoWatchState, + /** Sum of all previous season episode counts + episode */ + val totalEpisodeIndex: Int? = null, + val airDate: Long? = null, + val runTime: Int? = null, + val seasonData: SeasonData? = null, ) fun ResultEpisode.getRealPosition(): Long { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index c316fae17..cfbacc5d1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -874,9 +874,10 @@ class ResultFragmentTv : BaseFragment( resultMetaRating.setText(d.ratingText) resultMetaStatus.setText(d.onGoingText) resultMetaContentRating.setText(d.contentRatingText) + resultCastText.setText(d.actorsText) resultNextAiring.setText(d.nextAiringEpisode) resultNextAiringTime.setText(d.nextAiringDate) - resultPoster.loadImage(d.posterImage, headers = d.posterHeaders) + resultPoster.loadImage(d.posterImage) var isExpanded = false resultDescription.apply { @@ -909,7 +910,7 @@ class ResultFragmentTv : BaseFragment( R.drawable.profile_bg_teal ).random() - backgroundPoster.loadImage(d.posterBackgroundImage, headers = d.posterHeaders) { + backgroundPoster.loadImage(d.posterBackgroundImage) { error { getImageFromDrawable(context ?: return@error null, error) } } @@ -931,7 +932,6 @@ class ResultFragmentTv : BaseFragment( true ) - resultCastText.setText(if (showCast) d.actorsText else null) resultCastItems.isGone = !showCast || d.actors.isNullOrEmpty() (resultCastItems.adapter as? ActorAdaptor)?.submitList(if (showCast) d.actors else emptyList()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index c519e0de2..7dfe3cf59 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -83,7 +83,6 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.AppContextUtils.isConnectedToChromecast import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs -import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWork @@ -1325,7 +1324,7 @@ class ResultViewModel2 : ViewModel() { episodeIds: Array, watchState: VideoWatchState ) { - val watchStateString = watchState.toJson() + val watchStateString = DataStore.mapper.writeValueAsString(watchState) episodeIds.forEach { if (getVideoWatchState(it.toInt()) != watchState) { editor.setKeyRaw( @@ -1686,13 +1685,14 @@ class ResultViewModel2 : ViewModel() { } val realRecommendations = ArrayList() - val apiNames = apis.filter { - it.name.contains("gogoanime", true) || - it.name.contains("9anime", true) - }.map { - it.name + val apiNames = synchronized(apis) { + apis.filter { + it.name.contains("gogoanime", true) || + it.name.contains("9anime", true) + }.map { + it.name + } } - meta.recommendations?.forEach { rec -> apiNames.forEach { name -> realRecommendations.add(rec.copy(apiName = name)) @@ -2706,4 +2706,4 @@ class ResultViewModel2 : ViewModel() { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt index 3161a1204..6c5c64ff8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt @@ -182,7 +182,6 @@ class SyncViewModel : ViewModel() { fun publishUserData() = ioSafe { Log.i(TAG, "publishUserData") val user = userData.value - _userDataResponse.postValue(Resource.Loading()) if (user is Resource.Success) { syncs.forEach { (prefix, id) -> repos.firstOrNull { it.idPrefix == prefix }?.updateStatus(id, user.value) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index dd5b97711..5f5b064b5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -149,7 +149,6 @@ class SearchFragment : BaseFragment( override fun onResume() { super.onResume() - searchViewModel.clearSuggestions() afterPluginsLoadedEvent += ::reloadRepos } @@ -168,7 +167,6 @@ class SearchFragment : BaseFragment( **/ fun search(query: String?) { if (query == null) return - searchViewModel.clearSuggestions() // don't resume state from prev search (binding?.searchMasterRecycler?.adapter as? BaseAdapter<*, *>)?.clearState() context?.let { ctx -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt index 793d96734..4868abb3d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt @@ -13,15 +13,12 @@ import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -@Serializable data class SearchHistoryItem( - @JsonProperty("searchedAt") @SerialName("searchedAt") val searchedAt: Long, - @JsonProperty("searchText") @SerialName("searchText") val searchText: String, - @JsonProperty("type") @SerialName("type") val type: List, - @JsonProperty("key") @SerialName("key") val key: String, + @JsonProperty("searchedAt") val searchedAt: Long, + @JsonProperty("searchText") val searchText: String, + @JsonProperty("type") val type: List, + @JsonProperty("key") val key: String, ) data class SearchHistoryCallback( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt index a22c2fcef..8dbd78178 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt @@ -4,8 +4,6 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.nicehttp.NiceResponse -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable /** * API for fetching search suggestions from external sources. @@ -15,18 +13,16 @@ object SearchSuggestionApi { private const val TMDB_API_URL = "https://api.themoviedb.org/3/search/multi" private const val TMDB_API_KEY = "e6333b32409e02a4a6eba6fb7ff866bb" - @Serializable data class TmdbSearchResult( - @JsonProperty("results") @SerialName("results") val results: List?, + @JsonProperty("results") val results: List? ) - @Serializable data class TmdbSearchItem( - @JsonProperty("media_type") @SerialName("media_type") val mediaType: String?, - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("name") @SerialName("name") val name: String?, - @JsonProperty("original_title") @SerialName("original_title") val originalTitle: String?, - @JsonProperty("original_name") @SerialName("original_name") val originalName: String?, + @JsonProperty("media_type") val mediaType: String?, + @JsonProperty("title") val title: String?, + @JsonProperty("name") val name: String?, + @JsonProperty("original_title") val originalTitle: String?, + @JsonProperty("original_name") val originalName: String? ) /** @@ -61,7 +57,7 @@ object SearchSuggestionApi { * Parses the TMDB search response and extracts movie/TV show titles. * Filters to only include movies, TV shows, and anime. */ - private suspend fun parseSuggestions(response: NiceResponse): List { + private fun parseSuggestions(response: NiceResponse): List { return try { val parsed = response.parsed() parsed.results diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index f60588e35..27db8d1ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -49,7 +49,7 @@ class SearchViewModel : ViewModel() { private var suggestionJob: Job? = null - private var repos = apis.withLock { apis.map { APIRepository(it) } } + private var repos = synchronized(apis) { apis.map { APIRepository(it) } } fun clearSearch() { _searchResponse.postValue(Resource.Success(ExpandableSearchList(emptyList(), 0, false))) @@ -68,7 +68,7 @@ class SearchViewModel : ViewModel() { private var onGoingSearch: Job? = null fun reloadRepos() { - repos = apis.withLock { apis.map { APIRepository(it) } } + repos = synchronized(apis) { apis.map { APIRepository(it) } } } fun searchAndCancel( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/ApkUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/ApkUpdater.kt deleted file mode 100644 index 34fea2938..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/ApkUpdater.kt +++ /dev/null @@ -1,274 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import android.annotation.SuppressLint -import android.app.Activity -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.pm.PackageInstaller -import android.os.Build -import androidx.core.content.FileProvider -import com.lagradost.cloudstream3.BuildConfig -import com.lagradost.cloudstream3.CommonActivity -import com.lagradost.cloudstream3.ErrorLoadingException -import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.mvvm.safe -import com.lagradost.cloudstream3.receivers.PackageInstallerStatusReceiver -import com.lagradost.cloudstream4.AppSettings -import com.lagradost.cloudstream4.viewmodels.AppUpdater -import com.lagradost.cloudstream4.viewmodels.DigestPair -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.withContext -import java.io.File -import java.io.InputStream -import java.io.OutputStream -import java.security.DigestException -import java.security.MessageDigest - -object ApkUpdater : AppUpdater { - private const val APP_UPDATE_NAME = "CloudStream" - private const val APP_UPDATE_SUFFIX = "apk" - - @Throws - override suspend fun update( - settings: AppSettings, - url: String, - digest: DigestPair?, - downloadProgress: (Long, Long?) -> Unit - ) { - val activity = CommonActivity.activity ?: throw ErrorLoadingException("No activity found") - clearOldFiles(activity) - - val request = app.get(url) - val length = request.size - val body = request.body - body.use { body -> - val length = length ?: body.contentLength() - val readStream = body.byteStream() - - when (settings.updates.apkInstaller.get()) { - 0 -> { - packageInstallerDownloader( - activity, - readStream, - length, - digest, - downloadProgress - ) - } - - else -> { - legacyDownloader(activity, readStream, length, digest, downloadProgress) - } - } - } - } - - fun clearOldFiles(activity: Activity) { - // Delete old files - activity.cacheDir.listFiles()?.filter { - it.name.startsWith(APP_UPDATE_NAME) && it.extension == APP_UPDATE_SUFFIX - }?.forEach { - deleteFileOnExit(it) - } - } - - /** https://medium.com/@solrudev/painless-building-of-an-android-package-installer-app-d5a09b5df432 */ - @SuppressLint("RequestInstallPackagesPolicy") - @Throws - suspend fun packageInstallerDownloader( - activity: Activity, - readStream: InputStream, - length: Long?, - digest: DigestPair?, - downloadProgress: (Long, Long?) -> Unit, - ) = withContext(Dispatchers.IO) { - var sessionId: Int? = null - val packageInstaller = activity.packageManager.packageInstaller - try { - val installParams = - PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - installParams.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED) - } - if (length != null) { - installParams.setSize(length) - } - - sessionId = packageInstaller.createSession(installParams) - val session = packageInstaller.openSession(sessionId) - - // We do not need to buffer this because transfer has large writes - session.openWrite(activity.packageName, 0, length ?: -1L) - .use { writeStream -> - transfer(writeStream, readStream, length, downloadProgress, digest) - session.fsync(writeStream) - } - - val receiverIntent = Intent(activity, PackageInstallerStatusReceiver::class.java) - val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - } else { - PendingIntent.FLAG_UPDATE_CURRENT - } - val receiverPendingIntent = - PendingIntent.getBroadcast(activity, 0, receiverIntent, flags) - - // Avoid delayed updates, and just commit instantly - session.commit(receiverPendingIntent.intentSender) - session.close() - } catch (t: Throwable) { - sessionId?.let { sessionId -> - packageInstaller.abandonSession(sessionId) - } - throw t - } - } - - /** - * Write the "readStream" to the "writeStream", while notifying the "downloadProgress" and - * calculating the digest - * - * ------------------------------------------------------------------------------------------ - * - * throws a DigestException if the Digest can be calculated (non-null + correct algorithm), - * and is mismatched - * - * throws a CancellationException if canceled, but may not be "instant" if the read is blocking - * - * throws IOException and similar for reading - * - * ------------------------------------------------------------------------------------------ - * - * In case of crashes from the digest, we ignore it as we do not want to "block" someone from - * updating if they got a broken OS without the desired algorithm or implementation. - * - * This is because a malicious CDN should not be able to "crash" the algorithm, but a broken - * OS will. And recovering from a broken OS by ignoring it is better than refusing it. - * */ - @Throws - suspend fun transfer( - writeStream: OutputStream, - readStream: InputStream, - length: Long?, - downloadProgress: (Long, Long?) -> Unit, - digest: DigestPair?, - ) = withContext(Dispatchers.IO) { - val md = digest?.algorithm?.let { digestAlgorithm -> - safe { - MessageDigest.getInstance(digestAlgorithm) - } - } - - val context = currentCoroutineContext() - val buffer = ByteArray(DEFAULT_BUFFER_SIZE) - var read: Int - var transferred: Long = 0 - while ((readStream.read(buffer, 0, DEFAULT_BUFFER_SIZE) - .also { read = it }) >= 0 - ) { - writeStream.write(buffer, 0, read) - - safe { - md?.update(buffer, 0, read) - } - - transferred += read.toLong() - downloadProgress(transferred, length) - context.ensureActive() - } - writeStream.flush() - - val check = safe { md?.digest() } - if (check != null && !check.contentEquals(digest?.digest)) { - throw DigestException("Mismatched digest on transfer ${digest?.algorithm} : ${check.toHexString()} / ${digest?.digest?.toHexString()}") - } - } - - /** - * Returns true if we have a non-null digest that matches a digest on the specified existing file - * */ - suspend fun isFileDigest(file: File, digest: DigestPair?): Boolean { - if (digest == null) { - return false - } - try { - val fileDigest = calculateDigest( - digestAlgorithm = digest.algorithm, - readStream = file.inputStream() - ) - return fileDigest != null && digest.digest.contentEquals(fileDigest) - } catch (_: Throwable) { - // File not found/security/read issues, not very useful errors - } - return false - } - - /** - * Calculates the digest of a stream with the specified algorithm, use transfer instead if - * you already download something. - * - * Will throw read errors - * Will return null if the algorithm is null or does not exist/has some error - * */ - @Throws - suspend fun calculateDigest(digestAlgorithm: String?, readStream: InputStream) = - withContext(Dispatchers.IO) { - if (digestAlgorithm == null) return@withContext null - - val md = - safe { - MessageDigest.getInstance(digestAlgorithm) - } ?: return@withContext null - - val context = currentCoroutineContext() - val buffer = ByteArray(DEFAULT_BUFFER_SIZE) - var read: Int - while ((readStream.read(buffer, 0, DEFAULT_BUFFER_SIZE) - .also { read = it }) >= 0 - ) { - try { - md.update(buffer, 0, read) - } catch (_: Throwable) { - return@withContext null - } - context.ensureActive() - } - return@withContext safe { md.digest() } - } - - @Throws - suspend fun legacyDownloader( - activity: Activity, - readStream: InputStream, - length: Long?, - digest: DigestPair?, - downloadProgress: (Long, Long?) -> Unit - ) = withContext(Dispatchers.IO) { - val downloadedFile = File.createTempFile(APP_UPDATE_NAME, ".$APP_UPDATE_SUFFIX") - - // We do not need to buffer this because transfer has large writes - downloadedFile.outputStream().use { writeStream -> - transfer(writeStream, readStream, length, downloadProgress, digest) - } - - openApk(activity, downloadedFile) - } - - fun openApk(context: Context, file: File) { - val contentUri = FileProvider.getUriForFile( - context, BuildConfig.APPLICATION_ID + ".provider", file - ) - val installIntent = Intent(Intent.ACTION_VIEW).apply { - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true) - data = contentUri - } - context.startActivity(installIntent) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/GithubReleases.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/GithubReleases.kt deleted file mode 100644 index b03bda8ce..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/GithubReleases.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream4.viewmodels.GithubFile -import com.lagradost.cloudstream4.viewmodels.GithubRepository -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import java.io.FileNotFoundException - -object GithubReleases : GithubRepository { - @Serializable - private data class GithubAsset( - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("size") @SerialName("size") val size: Int, // Size in bytes - @JsonProperty("browser_download_url") @SerialName("browser_download_url") val browserDownloadUrl: String, - @JsonProperty("content_type") @SerialName("content_type") val contentType: String, // application/vnd.android.package-archive - @JsonProperty("digest") @SerialName("digest") val digest: String? = null, // sha256:..., may be null - ) - - @Serializable - private data class GithubRelease( - @JsonProperty("tag_name") @SerialName("tag_name") val tagName: String, // Version code - @JsonProperty("body") @SerialName("body") val body: String, // Description - @JsonProperty("assets") @SerialName("assets") val assets: List, - @JsonProperty("target_commitish") @SerialName("target_commitish") val targetCommitish: String, // Branch - @JsonProperty("prerelease") @SerialName("prerelease") val prerelease: Boolean, - @JsonProperty("node_id") @SerialName("node_id") val nodeId: String, - @JsonProperty("created_at") @SerialName("created_at") val createdAt: String, // YYYY-MM-DDTHH:MM:SSZ - ) - - @Serializable - private data class GithubObject( - @JsonProperty("sha") @SerialName("sha") val sha: String, // SHA-256 hash - //@JsonProperty("type") @SerialName("type") val type: String, - ///@JsonProperty("url") @SerialName("url") val url: String, - ) - - @Serializable - private data class GithubTag( - //@JsonProperty("node_id") @SerialName("node_id") val nodeId: String, - @JsonProperty("object") @SerialName("object") val githubObject: GithubObject, - ) - - const val APK_USERNAME = "recloudstream" - const val APK_REPOSITORY = "cloudstream" - const val APK_PRERELEASE = "pre-release" - const val APK_CONTENT_TYPE = "application/vnd.android.package-archive" - - private val defaultHeaders = mapOf("Accept" to "application/vnd.github.v3+json") - - @Throws - override suspend fun getSha( - userName: String, - repository: String, - tag: String, - ): String { - return app.get( - url = "https://api.github.com/repos/$userName/$repository/git/ref/tags/$tag", - headers = defaultHeaders - ).parsed().githubObject.sha - } - - @Throws - override suspend fun getRelease( - userName: String, - repository: String, - prerelease: Boolean, - prereleaseTag: String, - contentType: String, - ): GithubFile { - val latestReleaseUrl = if (prerelease) { - // Find the release object - // https://docs.github.com/en/rest/releases/releases?apiVersion=2026-03-10#get-a-release-by-tag-name - "https://api.github.com/repos/$userName/$repository/releases/tags/$prereleaseTag" - } else { - // Just get the latest release - // https://docs.github.com/en/rest/releases/releases?apiVersion=2026-03-10#get-the-latest-release - // The latest release is the most recent non-prerelease, non-draft release, sorted by the created_at attribute - "https://api.github.com/repos/$userName/$repository/releases/latest" - } - - val latestRelease = app.get( - url = latestReleaseUrl, - headers = defaultHeaders - ).parsed() - - // Find the first correct asset, given that we might have other binaries we release in the same version - val foundAsset = latestRelease.assets.firstOrNull { asset -> - asset.contentType == contentType - } - - if (foundAsset == null) { - throw FileNotFoundException() - } - - return GithubFile( - digest = foundAsset.digest, - downloadUrl = foundAsset.browserDownloadUrl, - displayName = foundAsset.name.substringBeforeLast("."), - changeLog = latestRelease.body, - tagName = latestRelease.tagName, - nodeId = latestRelease.nodeId - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt index 70fd2a19d..93e469a4d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt @@ -1,18 +1,19 @@ package com.lagradost.cloudstream3.ui.settings +import android.app.UiModeManager import android.content.Context import android.content.res.Configuration import android.content.res.Resources +import android.os.Build import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream4.compose.DeviceLayout.Companion.isAutoTv object Globals { var beneneCount = 0 - const val PHONE: Int = 0b00001 - const val TV: Int = 0b00010 - const val EMULATOR: Int = 0b00100 + const val PHONE : Int = 0b001 + const val TV : Int = 0b010 + const val EMULATOR : Int = 0b100 private const val INVALID = -1 private var layoutId = INVALID @@ -21,9 +22,18 @@ object Globals { return settingsManager.getInt(this.getString(R.string.app_layout_key), -1) } - fun Context.updateTv() { - layoutId = when (getLayoutInt()) { - -1 -> if (isAutoTv(this)) TV else PHONE + private fun Context.isAutoTv(): Boolean { + val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager? + // AFT = Fire TV + val model = Build.MODEL.lowercase() + return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains( + "AFT" + ) || model.contains("firestick") || model.contains("fire tv") || model.contains("chromecast") + } + + private fun Context.layoutIntCorrected(): Int { + return when(getLayoutInt()) { + -1 -> if (isAutoTv()) TV else PHONE 0 -> PHONE 1 -> TV 2 -> EMULATOR @@ -31,10 +41,14 @@ object Globals { } } + fun Context.updateTv() { + layoutId = layoutIntCorrected() + } + /** Returns true if the current orientation is landscape. */ fun isLandscape(): Boolean = isLayout(TV or EMULATOR) || - Resources.getSystem().configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + Resources.getSystem().configuration.orientation == Configuration.ORIENTATION_LANDSCAPE /** Returns true if the layout is any of the flags, * so isLayout(TV or EMULATOR) is a valid statement for checking if the layout is in the emulator @@ -42,7 +56,7 @@ object Globals { * * Valid flags are: PHONE, TV, EMULATOR * */ - fun isLayout(flags: Int): Boolean { + fun isLayout(flags: Int) : Boolean { return (layoutId and flags) != 0 } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 6f67ab826..8d96a6b14 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -1,7 +1,6 @@ package com.lagradost.cloudstream3.ui.settings import android.annotation.SuppressLint -import android.app.Activity import android.graphics.Bitmap import android.os.Bundle import android.os.CountDownTimer @@ -14,6 +13,7 @@ import androidx.appcompat.app.AlertDialog import androidx.core.content.edit import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceManager import androidx.preference.SwitchPreference import androidx.recyclerview.widget.RecyclerView @@ -29,8 +29,8 @@ import com.lagradost.cloudstream3.databinding.DeviceAuthBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.animeSkipApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.kitsuApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.kitsuApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subDlApi @@ -42,8 +42,8 @@ import com.lagradost.cloudstream3.syncproviders.SubtitleRepo import com.lagradost.cloudstream3.syncproviders.SyncRepo import com.lagradost.cloudstream3.ui.BasePreferenceFragmentCompat import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR -import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn @@ -65,8 +65,6 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogTe import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard -import com.lagradost.cloudstream3.utils.UIHelper.hideProgress -import com.lagradost.cloudstream3.utils.UIHelper.showProgress import com.lagradost.cloudstream3.utils.setText import com.lagradost.cloudstream3.utils.txt import qrcode.QRCode @@ -76,7 +74,7 @@ class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { /** Used by nginx plugin too */ @SuppressLint("StringFormatInvalid") fun showLoginInfo( - activity: Activity?, + activity: FragmentActivity?, api: AuthRepo, info: AuthUser?, index: Int, @@ -121,7 +119,7 @@ class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { } } - private fun showAccountSwitch(activity: Activity, api: AuthRepo) { + private fun showAccountSwitch(activity: FragmentActivity, api: AuthRepo) { val accounts = api.accounts val binding: AccountSwitchBinding = AccountSwitchBinding.inflate(activity.layoutInflater, null, false) @@ -153,7 +151,7 @@ class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { @UiThread - fun showPin(activity: Activity, api: AuthRepo) { + fun showPin(activity: FragmentActivity, api: AuthRepo) { val binding: DeviceAuthBinding = DeviceAuthBinding.inflate(activity.layoutInflater, null, false) @@ -273,7 +271,8 @@ class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { } - fun showAppLogin(activity: Activity, api: AuthRepo) { + fun showAppLogin(activity: FragmentActivity, api: AuthRepo) { + val binding: AddAccountInputBinding = AddAccountInputBinding.inflate(activity.layoutInflater, null, false) val builder = @@ -314,7 +313,7 @@ class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { binding.createAccount.setOnClickListener { openBrowser( api.createAccountUrl ?: return@setOnClickListener, - activity, + activity ) dialog.dismissSafe() } @@ -349,7 +348,6 @@ class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { email = if (req.email) binding.loginEmailInput.text?.toString() else null, server = if (req.server) binding.loginServerInput.text?.toString() else null, ) - binding.applyBtt.showProgress() ioSafe { try { if (api.login(loginData)) { @@ -379,8 +377,6 @@ class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { api.name ) ) - } finally { - binding.applyBtt.hideProgress() } } } @@ -390,7 +386,7 @@ class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { } @UiThread - fun addAccount(activity: Activity, api: AuthRepo) { + fun addAccount(activity: FragmentActivity, api: AuthRepo) { try { if (api.hasPin && !isLayout(PHONE)) { showPin(activity, api) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount2.kt deleted file mode 100644 index 0185370ce..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount2.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import com.lagradost.cloudstream3.utils.BaseComposeFragment -import com.mihon.presentation.settings.SearchableSettings - -/** Empty glue code to connect the old navigation graph to Compose */ -class SettingsAccount2 : BaseComposeFragment(), SearchableSettings by SettingsAccountScreen \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccountScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccountScreen.kt deleted file mode 100644 index 97890bbfa..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccountScreen.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import android.content.Context -import androidx.activity.compose.LocalActivity -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.fragment.app.FragmentActivity -import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.animeSkipApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.kitsuApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subDlApi -import com.lagradost.cloudstream3.syncproviders.PlainAuthRepo -import com.lagradost.cloudstream3.syncproviders.SubtitleRepo -import com.lagradost.cloudstream3.syncproviders.SyncRepo -import com.lagradost.cloudstream3.ui.settings.SettingsAccount.Companion.addAccount -import com.lagradost.cloudstream3.ui.settings.SettingsAccount.Companion.showLoginInfo -import com.lagradost.cloudstream3.utils.AppContextUtils.html -import com.lagradost.cloudstream3.utils.BackupUtils -import com.lagradost.cloudstream3.utils.BiometricAuthenticator -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication -import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText -import com.lagradost.cloudstream4.AppSettings -import com.lagradost.cloudstream4.compose.PHONE -import com.lagradost.cloudstream4.compose.isLayout -import com.lagradost.cloudstream4.rememberAppSettings -import com.mihon.presentation.settings.Preference -import com.mihon.presentation.settings.SearchableSettings -import kotlinx.collections.immutable.persistentListOf - -object SettingsAccountScreen : SearchableSettings, BiometricAuthenticator.BiometricCallback { - val syncApis = persistentListOf( - SyncRepo(malApi), - SyncRepo(kitsuApi), - SyncRepo(aniListApi), - SyncRepo(simklApi), - SubtitleRepo(openSubtitlesApi), - SubtitleRepo(subDlApi), - PlainAuthRepo(animeSkipApi), - ) - private fun updateAuthPreference(context: Context, enabled: Boolean) { - val settings = AppSettings(context) - settings.security.biometrics.set(enabled) - } - - override fun onAuthenticationError() { - val context = activity ?: return - updateAuthPreference(context, !isAuthEnabled(context)) - } - - override fun onAuthenticationSuccess() { - val context = activity ?: return - - if (isAuthEnabled(context)) { - updateAuthPreference(context, true) - BackupUtils.backup(context) - context.showBottomDialogText( - context.getString(R.string.biometric_setting), - context.getString(R.string.biometric_warning).html() - ) { onDialogDismissedEvent } - } else { - updateAuthPreference(context, false) - } - } - - @Composable - override fun getTitleRes(): String = stringResource(R.string.category_account) - - @Composable - override fun getPreferences(): List { - val settings = rememberAppSettings() - val activity = LocalActivity.current - val context = LocalContext.current - val hasSecurity = remember(context) { - try { - deviceHasPasswordPinLock(context) - } catch (_ : Throwable) { - // e.g preview - false - } - } - - return persistentListOf( - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_accounts), - preferenceItems = syncApis.map { api -> - Preference.PreferenceItem.TextPreference( - title = api.name, - icon = api.icon?.let { painterResource(it) }, - onClick = { - val activity = activity ?: return@TextPreference - val info = api.authUser() - val index = - api.accounts.indexOfFirst { account -> account.user.id == info?.id } - if (api.accounts.isNotEmpty()) { - showLoginInfo(activity, api, info, index) - } else { - addAccount(activity, api) - } - }) - } + Preference.PreferenceItem.SwitchPreference( - preference = settings.security.skipAccountSelection, - title = stringResource(R.string.skip_startup_account_select_pref), - icon = painterResource(R.drawable.ic_outline_account_circle_24) - ), - ), Preference.PreferenceGroup( - enabled = hasSecurity && isLayout(PHONE), - title = stringResource(R.string.pref_category_security), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - preference = settings.security.biometrics, - title = stringResource(R.string.biometric_setting), - subtitle = stringResource(R.string.biometric_setting_summary), - icon = painterResource(R.drawable.ic_fingerprint), - onValueChanged = { _ -> - val activity = - activity as? FragmentActivity ?: return@SwitchPreference false - - if (deviceHasPasswordPinLock(activity)) { - startBiometricAuthentication( - activity, R.string.biometric_authentication_title, false - ) - promptInfo?.let { - authCallback = this - biometricPrompt?.authenticate(it) - } - } - - return@SwitchPreference true - }) - ) - ) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 6943e5cf4..e41109b59 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -249,7 +249,7 @@ class SettingsFragment : BaseFragment( val appVersion = BuildConfig.VERSION_NAME val commitHash = activity?.currentCommitHash() ?: "" - val buildTimestamp = SimpleDateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM, + val buildTimestamp = SimpleDateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, Locale.getDefault() ).apply { timeZone = TimeZone.getTimeZone("UTC") }.format(Date(BuildConfig.BUILD_DATE)).replace("UTC", "") @@ -262,4 +262,4 @@ class SettingsFragment : BaseFragment( true } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment2.kt deleted file mode 100644 index 24867b898..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment2.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import com.lagradost.cloudstream3.utils.BaseComposeFragment -import com.lagradost.cloudstream4.compose.Screen - - -/** Empty glue code to connect the old navigation graph to Compose */ -class SettingsFragment2 : BaseComposeFragment(), Screen by SettingsFragmentScreen \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragmentScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragmentScreen.kt deleted file mode 100644 index 305b8ec28..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragmentScreen.kt +++ /dev/null @@ -1,478 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import android.annotation.SuppressLint -import android.os.Bundle -import androidx.compose.animation.Crossfade -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -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 -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.input.TextFieldLineLimits -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.text.input.rememberTextFieldState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ProvideTextStyle -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.NonRestartableComposable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEventType -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onPreviewKeyEvent -import androidx.compose.ui.input.key.type -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.tooling.preview.PreviewLightDark -import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage -import com.lagradost.cloudstream3.BuildConfig -import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.DataStoreHelper -import com.lagradost.cloudstream3.utils.DataStoreHelper.profileImages -import com.lagradost.cloudstream3.utils.GitInfo.currentCommitHash -import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper -import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.cloudstream3.utils.txt -import com.lagradost.cloudstream4.compose.Screen -import com.lagradost.cloudstream4.compose.TV -import com.lagradost.cloudstream4.compose.circleBorder -import com.lagradost.cloudstream4.compose.focusOutline -import com.lagradost.cloudstream4.compose.isLayout -import com.lagradost.cloudstream4.theme.CloudStreamPreviewTheme -import com.mihon.material.padding -import com.mihon.presentation.settings.SearchableSettings -import com.mihon.presentation.settings.SettingSearchResults -import com.mihon.presentation.settings.SettingsData -import com.mihon.presentation.settings.widget.TextPreferenceWidget -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.TimeZone - -object SettingsFragmentScreen : Screen { - val screens = persistentListOf( - SettingsNavigation( - title = R.string.category_general, - navigation = R.id.action_navigation_global_to_navigation_settings_general, - screen = SettingsGeneralScreen, - icon = R.drawable.build_24px, - subtitle = persistentListOf( - R.string.extension_language, - R.string.title_downloads, - R.string.pref_category_bypass, - R.string.pref_category_links - ) - ), - SettingsNavigation( - title = R.string.category_player, - navigation = R.id.action_navigation_global_to_navigation_settings_player, - screen = SettingsPlayerScreen, - icon = R.drawable.play_arrow_24px, - subtitle = persistentListOf( - R.string.pref_category_subtitles, - R.string.pref_category_player_features, - R.string.pref_category_gestures, - R.string.pref_category_player_layout, - R.string.pref_category_cache - ) - ), - /*SettingsNavigation( - title = R.string.category_providers, - navigation = R.id.action_navigation_global_to_navigation_settings_providers, - screen = SettingsProvidersScreen, - icon = R.drawable.build_24px, - ),*/ - SettingsNavigation( - title = R.string.category_ui, - navigation = R.id.action_navigation_global_to_navigation_settings_ui, - screen = SettingsUIScreen, - icon = R.drawable.format_paint_24px, - subtitle = persistentListOf( - R.string.pref_category_looks, - R.string.pref_category_ui_features, - R.string.search_poster_img_des, - R.string.poster_ui_settings - ) - ), - SettingsNavigation( - title = R.string.category_updates, - navigation = R.id.action_navigation_global_to_navigation_settings_updates, - screen = SettingsUpdatesScreen, - icon = R.drawable.mobile_arrow_down_24px, - subtitle = persistentListOf( - R.string.pref_category_app_updates, - R.string.pref_category_backup, - R.string.pref_category_extensions, - R.string.pref_category_actions - ) - ), - SettingsNavigation( - title = R.string.category_account, - navigation = R.id.action_navigation_global_to_navigation_settings_account, - screen = SettingsAccountScreen, - icon = R.drawable.encrypted_24px, - subtitle = persistentListOf( - R.string.pref_category_accounts, - R.string.pref_category_security - ) - ), - SettingsNavigation( - title = R.string.pref_category_extensions, - navigation = R.id.action_navigation_global_to_navigation_settings_extensions, - screen = null, - icon = R.drawable.extension_24px, - subtitle = persistentListOf(R.string.add_repository) - ), - ) - - @Immutable - data class SettingsNavigation( - val title: Int, - val navigation: Int, - val screen: SearchableSettings?, - val icon: Int, - val subtitle: PersistentList, - ) - - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") - override fun Content() { - val textFieldState = rememberTextFieldState() - - val outerListState = rememberScrollState() - - val parentFirstScrollConnection = remember { - object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - val delta = available.y - return if (delta < 0 && outerListState.canScrollForward) { - val consumed = outerListState.dispatchRawDelta(-delta) - Offset(0f, -consumed) - } else { - Offset.Zero - } - } - } - } - - Scaffold { _ -> - Column(modifier = Modifier.verticalScroll(outerListState)) { - Spacer(modifier = Modifier.height(MaterialTheme.padding.small)) - - val default = DataStoreHelper.getDefaultAccount( - LocalContext.current - ) - val flow by DataStoreHelper.selectedAccountNumberFlow.collectAsState() - val account = remember(flow) { - DataStoreHelper.getCurrentAccount() ?: default - } - - Row( - modifier = Modifier - .fillMaxSize() - .focusOutline() - .clickable { - activity.navigate( - R.id.accountSelectActivity, - Bundle().apply { putBoolean("isFromMainActivity", true) } - ) - } - .padding( - vertical = MaterialTheme.padding.large, - horizontal = MaterialTheme.padding.medium - ) - ) { - val image = - account.customImage ?: profileImages.getOrNull(account.defaultImageIndex) - ?: profileImages.first() - - AsyncImage( - contentScale = ContentScale.Crop, - model = image, - modifier = Modifier.circleBorder(50.dp), - contentDescription = null, - ) - Spacer(modifier = Modifier.width(MaterialTheme.padding.medium)) - Column { - Text( - text = account.name, - style = MaterialTheme.typography.titleLarge, - ) - Text( - text = stringResource(R.string.title_settings), - style = MaterialTheme.typography.bodyMedium, - ) - } - } - - Spacer(modifier = Modifier.height(MaterialTheme.padding.small)) - SettingsSearch(textFieldState = textFieldState) - Spacer(modifier = Modifier.height(MaterialTheme.padding.small)) - - SettingSearchResults( - nestedScrollConnection = parentFirstScrollConnection, - searchKey = textFieldState.text.toString(), - deferredItems = ::generateSearchItems, - onItemClick = { item -> - SearchableSettings.highlightKey = item.highlightKey - activity?.navigate(item.navigation) - }, empty = { - Column( - modifier = Modifier - .nestedScroll(parentFirstScrollConnection) - ) { - screens.forEach { settingsTab -> - SettingsTab(settingsTab) - } - BuildStamp() - } - }) - - } - } - } - - @Composable - @NonRestartableComposable - fun generateSearchItems() = - screens.mapNotNull { item -> - val contents = item.screen?.getPreferences() ?: return@mapNotNull null - SettingsData( - title = stringResource(item.title), - navigation = item.navigation, - contents = contents - ) - }.toPersistentList() - - @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) - @Composable - fun SettingsSearch(textFieldState: TextFieldState) { - var hasFocus by remember { mutableStateOf(false) } - var isWritable by remember { mutableStateOf(false) } - val focusProgress by animateFloatAsState(targetValue = if (hasFocus) 1.0f else 0.0f) - val focusManager = LocalFocusManager.current - val focusRequester = remember { FocusRequester() } - val isTv = isLayout(TV) - TextField( - state = textFieldState, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Search, - /** - * We do not want to pull up the keyboard when navigating to the item on TV, therefore - * we listen to when the TV user presses some key to "extra" focus it - * - * showKeyboardOnFocus is somewhat bugged, so need to change this to true when you - * press it, as otherwise no keyboard will show even with keyboardController - * */ - showKeyboardOnFocus = if (isTv) isWritable else true - ), - lineLimits = TextFieldLineLimits.SingleLine, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp - 12.dp * focusProgress) - .focusOutline(enabled = isTv, CircleShape) - .onFocusChanged { newFocus -> - hasFocus = newFocus.hasFocus - /** Loss of focus -> readonly */ - if (!newFocus.hasFocus) { - isWritable = false - } - } - .focusRequester(focusRequester) - .onPreviewKeyEvent { keyEvent -> - if (isWritable || !isTv || !hasFocus || keyEvent.type != KeyEventType.KeyUp) { - return@onPreviewKeyEvent false - } - - /** Focus + press once -> writable */ - return@onPreviewKeyEvent when (keyEvent.key) { - Key.Enter, Key.DirectionCenter, Key.NumPadEnter, Key.Spacebar -> { - isWritable = true - true - } - - else -> false - } - }, - placeholder = { - Text(text = stringResource(R.string.search_hint)) - }, - shape = CircleShape, - colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, - unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, - unfocusedLeadingIconColor = MaterialTheme.colorScheme.onBackground, - unfocusedTrailingIconColor = MaterialTheme.colorScheme.onBackground, - focusedTrailingIconColor = MaterialTheme.colorScheme.onBackground, - focusedLeadingIconColor = MaterialTheme.colorScheme.onBackground, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant, - unfocusedPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant, - cursorColor = MaterialTheme.colorScheme.onBackground, - ), - leadingIcon = { - Crossfade( - targetState = hasFocus, - label = "leftsearch", - ) { value -> - if (value) { - IconButton(onClick = { - textFieldState.edit { replace(0, length, "") } - focusManager.clearFocus() - }) { - Icon( - painter = painterResource(R.drawable.keyboard_arrow_left_24px), - contentDescription = null - ) - } - } else { - IconButton(onClick = { - focusRequester.requestFocus() - }) { - Icon( - painter = painterResource(R.drawable.search_icon), - contentDescription = null - ) - } - } - } - }, - trailingIcon = { - Crossfade( - targetState = hasFocus, - label = "rightsearch", - ) { value -> - if (value) { - IconButton(onClick = { - textFieldState.edit { replace(0, length, "") } - }) { - Icon( - painter = painterResource(R.drawable.close_24px), - contentDescription = null - ) - } - } - } - }, - ) - - val keyboardController = LocalSoftwareKeyboardController.current - DisposableEffect(Unit) { - onDispose { - keyboardController?.hide() - } - } - } - - @Composable - fun SettingsTab(settingsTab: SettingsNavigation) { - TextPreferenceWidget( - title = stringResource(settingsTab.title), - icon = painterResource(settingsTab.icon), - // This can not be converted to joinToString due to stringResource being composable - subtitle = - @Suppress("SimplifiableCallChain") - settingsTab.subtitle.map { stringResource(it) }.joinToString(), - // Clear it if we have already set it but navigated back instantly - onPreferenceClick = { - SearchableSettings.highlightKey = null - activity?.navigate(settingsTab.navigation) - }) - } - - @Composable - fun BuildStamp() { - val (commitHash, buildTimestamp) = remember { - val commitHash = activity?.currentCommitHash() ?: "" - val buildTimestamp = SimpleDateFormat.getDateTimeInstance( - DateFormat.LONG, DateFormat.MEDIUM, - Locale.getDefault() - ).apply { - timeZone = TimeZone.getTimeZone("UTC") - }.format(Date(BuildConfig.BUILD_DATE)).replace("UTC", "") - commitHash to buildTimestamp - } - - Row( - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .height(50.dp) - .focusOutline() - .clickable { - clipboardHelper( - txt(R.string.extension_version), - "${BuildConfig.VERSION_NAME} $commitHash $buildTimestamp" - ) - }, - ) { - ProvideTextStyle(MaterialTheme.typography.bodyMedium) { - Text(text = BuildConfig.VERSION_NAME) - if (commitHash != "") { - Text("•") - Text(text = commitHash) - } - Text("•") - Text(text = buildTimestamp) - } - } - } -} - - -@PreviewLightDark -@Composable -fun Preview() { - CloudStreamPreviewTheme { - SettingsFragmentScreen.Content() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index 32f60752e..dbf2ff1dc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.ui.settings import android.content.Context import android.net.Uri import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -11,14 +10,12 @@ import androidx.core.content.edit import androidx.core.os.ConfigurationCompat import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager -import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.CloudStreamApp import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey import com.lagradost.cloudstream3.CommonActivity -import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R @@ -51,10 +48,6 @@ import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement import com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.getBasePath import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonNames import java.util.Locale // Change local language settings in the app. @@ -79,7 +72,6 @@ val appLanguages = arrayListOf( Pair("Azərbaycan dili", "az"), Pair("Bahasa Indonesia", "in"), Pair("Bahasa Melayu", "ms"), - Pair("català", "ca"), Pair("Deutsch", "de"), Pair("English", "en"), Pair("Español", "es"), @@ -100,7 +92,6 @@ val appLanguages = arrayListOf( Pair("Português", "pt"), Pair("Português (Brasil)", "pt-BR"), Pair("Română", "ro"), - Pair("Shqip мова", "sq"), Pair("Slovenčina", "sk"), Pair("Soomaaliga", "so"), Pair("Svenska", "sv"), @@ -110,7 +101,6 @@ val appLanguages = arrayListOf( Pair("Wikang Filipino", "fil"), Pair("Čeština", "cs"), Pair("Ελληνικά", "el"), - Pair("беларуская мова", "be"), Pair("български", "bg"), Pair("македонски", "mk"), Pair("русский", "ru"), @@ -130,7 +120,6 @@ val appLanguages = arrayListOf( Pair("தமிழ்", "ta"), Pair("ಕನ್ನಡ", "kn"), Pair("മലയാളം", "ml"), - Pair("ພາສາລາວ", "lo"), Pair("ဗမာစာ", "my"), Pair("ትግርኛ", "ti"), Pair("አማርኛ", "am"), @@ -145,7 +134,7 @@ fun Pair.nameNextToFlagEmoji(): String { // fallback to [A][A] -> [?] question mak flag val flag = SubtitleHelper.getFlagFromIso(this.second) ?: "\ud83c\udde6\ud83c\udde6" - return "$flag\u00a0${this.first}" // \u00a0 non-breaking space + return "$flag\u00a0${this.first}" // \u00a0 non-breaking space } class SettingsGeneral : BasePreferenceFragmentCompat() { @@ -156,15 +145,15 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { setToolBarScrollFlags() } - @OptIn(ExperimentalSerializationApi::class) // JsonNames is an experimental annotation for now - @Serializable data class CustomSite( - @JsonProperty("parentClassName") @JsonAlias("parentJavaClass") - @SerialName("parentClassName") @JsonNames("parentJavaClass") - val parentClassName: String, // ::class.simpleName - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("url") @SerialName("url") val url: String, - @JsonProperty("lang") @SerialName("lang") val lang: String, + @JsonProperty("parentJavaClass") // javaClass.simpleName + val parentJavaClass: String, + @JsonProperty("name") + val name: String, + @JsonProperty("url") + val url: String, + @JsonProperty("lang") + val lang: String, ) companion object { @@ -178,96 +167,6 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { putString(context.getString(R.string.download_path_key_visual), visual) } } - - fun getCurrent(): MutableList { - return getKey>(USER_PROVIDER_API)?.toMutableList() - ?: mutableListOf() - } - fun showAdd() { - val providers = allProviders.distinctBy { it::class }.sortedBy { it.name } - val context = activity - context?.showDialog( - providers.map { "${it.name} (${it.mainUrl})" }, - -1, - context.getString(R.string.add_site_pref), - true, - {}) { selection -> - val provider = providers.getOrNull(selection) ?: return@showDialog - - val binding : AddSiteInputBinding = AddSiteInputBinding.inflate(LayoutInflater.from( - context - ),null,false) - - val builder = - AlertDialog.Builder(context, R.style.AlertDialogCustom) - .setView(binding.root) - - val dialog = builder.create() - dialog.show() - - binding.text2.text = provider.name - binding.applyBtt.setOnClickListener { - val name = binding.siteNameInput.text?.toString() - val url = binding.siteUrlInput.text?.toString() - val lang = binding.siteLangInput.text?.toString() - val realLang = if (lang.isNullOrBlank()) provider.lang else lang - val simpleName = provider::class.simpleName - if (url.isNullOrBlank() || name.isNullOrBlank() || simpleName == null) { - showToast(R.string.error_invalid_data, Toast.LENGTH_SHORT) - return@setOnClickListener - } - - val current = getCurrent() - val newSite = CustomSite(simpleName, name, url, realLang) - current.add(newSite) - setKey(USER_PROVIDER_API, current.toTypedArray()) - // reload apis - MainActivity.afterPluginsLoadedEvent.invoke(false) - - dialog.dismissSafe(activity) - } - binding.cancelBtt.setOnClickListener { - dialog.dismissSafe(activity) - } - } - } - - fun showDelete() { - val current = getCurrent() - val context = activity - context?.showMultiDialog( - current.map { it.name }, - listOf(), - context.getString(R.string.remove_site_pref), - {}) { indexes -> - current.removeAll(indexes.map { current[it] }) - setKey(USER_PROVIDER_API, current.toTypedArray()) - } - } - - fun showAddOrDelete() { - val context = activity - - val binding : AddRemoveSitesBinding = AddRemoveSitesBinding.inflate( - LayoutInflater.from( - context - ),null,false) - val builder = - AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom) - .setView(binding.root) - - val dialog = builder.create() - dialog.show() - - binding.addSite.setOnClickListener { - showAdd() - dialog.dismissSafe(activity) - } - binding.removeSite.setOnClickListener { - showDelete() - dialog.dismissSafe(activity) - } - } } private val pathPicker = getChooseFolderLauncher { uri, path -> @@ -279,6 +178,10 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { setPreferencesFromResource(R.xml.settings_general, rootKey) val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) + fun getCurrent(): MutableList { + return getKey>(USER_PROVIDER_API)?.toMutableList() + ?: mutableListOf() + } getPref(R.string.locale_key)?.setOnPreferenceClickListener { pref -> val current = getCurrentLocale(pref.context) @@ -315,7 +218,82 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { true } + fun showAdd() { + val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } } + activity?.showDialog( + providers.map { "${it.name} (${it.mainUrl})" }, + -1, + context?.getString(R.string.add_site_pref) ?: return, + true, + {}) { selection -> + val provider = providers.getOrNull(selection) ?: return@showDialog + val binding : AddSiteInputBinding = AddSiteInputBinding.inflate(layoutInflater,null,false) + + val builder = + AlertDialog.Builder(context ?: return@showDialog, R.style.AlertDialogCustom) + .setView(binding.root) + + val dialog = builder.create() + dialog.show() + + binding.text2.text = provider.name + binding.applyBtt.setOnClickListener { + val name = binding.siteNameInput.text?.toString() + val url = binding.siteUrlInput.text?.toString() + val lang = binding.siteLangInput.text?.toString() + val realLang = if (lang.isNullOrBlank()) provider.lang else lang + if (url.isNullOrBlank() || name.isNullOrBlank()) { + showToast(R.string.error_invalid_data, Toast.LENGTH_SHORT) + return@setOnClickListener + } + + val current = getCurrent() + val newSite = CustomSite(provider.javaClass.simpleName, name, url, realLang) + current.add(newSite) + setKey(USER_PROVIDER_API, current.toTypedArray()) + // reload apis + MainActivity.afterPluginsLoadedEvent.invoke(false) + + dialog.dismissSafe(activity) + } + binding.cancelBtt.setOnClickListener { + dialog.dismissSafe(activity) + } + } + } + + fun showDelete() { + val current = getCurrent() + + activity?.showMultiDialog( + current.map { it.name }, + listOf(), + context?.getString(R.string.remove_site_pref) ?: return, + {}) { indexes -> + current.removeAll(indexes.map { current[it] }) + setKey(USER_PROVIDER_API, current.toTypedArray()) + } + } + + fun showAddOrDelete() { + val binding : AddRemoveSitesBinding = AddRemoveSitesBinding.inflate(layoutInflater,null,false) + val builder = + AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom) + .setView(binding.root) + + val dialog = builder.create() + dialog.show() + + binding.addSite.setOnClickListener { + showAdd() + dialog.dismissSafe(activity) + } + binding.removeSite.setOnClickListener { + showDelete() + dialog.dismissSafe(activity) + } + } getPref(R.string.override_site_key)?.setOnPreferenceClickListener { _ -> @@ -342,7 +320,7 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { val prefValues = resources.getIntArray(R.array.dns_pref_values) val currentDns = - settingsManager.getInt(getString(R.string.dns_key), 0) + settingsManager.getInt(getString(R.string.dns_pref), 0) activity?.showBottomDialog( prefNames.toList(), @@ -350,7 +328,7 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { getString(R.string.dns_pref), true, {}) { - settingsManager.edit { putInt(getString(R.string.dns_key), prefValues[it]) } + settingsManager.edit { putInt(getString(R.string.dns_pref), prefValues[it]) } (context ?: CloudStreamApp.context)?.let { ctx -> app.initClient(ctx) } } return@setOnPreferenceClickListener true @@ -368,14 +346,14 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { (first + ctx.getExternalFilesDirs("").mapNotNull { it.path } + currentDir) - } catch (_: Exception) { + } catch (e: Exception) { first }).filterNotNull().distinct() } } ?: emptyList() } - settingsManager.edit { putBoolean(getString(R.string.jsdelivr_proxy_key), getKey(getString(R.string.jsdelivr_proxy_key), false) ?: false) } + settingsManager.edit { putBoolean(getString(R.string.jsdelivr_proxy_key), getKey(getString(R.string.jsdelivr_proxy_key), false) ?: false) } getPref(R.string.jsdelivr_proxy_key)?.setOnPreferenceChangeListener { _, newValue -> setKey(getString(R.string.jsdelivr_proxy_key), newValue) return@setOnPreferenceChangeListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral2.kt deleted file mode 100644 index 37b1812b9..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral2.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import com.lagradost.cloudstream3.utils.BaseComposeFragment -import com.mihon.presentation.settings.SearchableSettings - -/** Empty glue code to connect the old navigation graph to Compose */ -class SettingsGeneral2 : BaseComposeFragment(), SearchableSettings by SettingsGeneralScreen \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneralScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneralScreen.kt deleted file mode 100644 index 6aa94fa88..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneralScreen.kt +++ /dev/null @@ -1,305 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import android.content.Intent -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.integerArrayResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringArrayResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.PreviewLightDark -import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.AllLanguagesName -import com.lagradost.cloudstream3.CloudStreamApp -import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.CommonActivity.showToast -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.UnsafeSSL -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.insecureApp -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.network.initClient -import com.lagradost.cloudstream3.ui.settings.SettingsProvidersScreen.toStringRes -import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.isAppRestricted -import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.showRequestIgnoreBatteryOptDialog -import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToLanguageName -import com.lagradost.cloudstream3.utils.SubtitleHelper.getNameNextToFlagEmoji -import com.lagradost.cloudstream4.AppSettings -import com.lagradost.cloudstream4.compose.ActionDialog -import com.lagradost.cloudstream4.compose.PHONE -import com.lagradost.cloudstream4.compose.isLayout -import com.lagradost.cloudstream4.rememberAppSettings -import com.lagradost.cloudstream4.theme.CloudStreamPreviewTheme -import com.lagradost.safefile.SafeFile -import com.mihon.presentation.settings.Preference -import com.mihon.presentation.settings.SearchableSettings -import com.mihon.presentation.settings.collectAsState -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentMap - -object SettingsGeneralScreen : SearchableSettings { - @Composable - override fun getTitleRes(): String = stringResource(R.string.category_general) - - @Composable - override fun getPreferences(): List { - val settings = rememberAppSettings() - - // TODO Refactor entirely to use a different file path selector ect like QuickNovel - val selectFileSelector = - rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> - // It lies, it can be null if file manager quits. - if (uri == null) return@rememberLauncherForActivityResult - val context = CloudStreamApp.context ?: return@rememberLauncherForActivityResult - - try { - val settings = AppSettings(context) - // RW perms for the path - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - context.contentResolver.takePersistableUriPermission(uri, flags) - - val filePath = SafeFile.fromUri(context, uri)?.filePath() - println("Selected URI path: $uri - Full path: $filePath") - - // store the actual URI instead of the path due to permissions. - // filePath should only be used for cosmetic purposes. - val visual = filePath ?: uri.toString() - settings.general.downloadPath.set(uri.toString()) - settings.general.downloadPathVisual.set(visual) - } catch (t: Throwable) { - logError(t) - } - } - - val bananas by settings.general.bananas.collectAsState() - val downloadPathVisual by settings.general.downloadPathVisual.collectAsState() - //val downloadPath by settings.general.downloadPath.collectAsState() - - var isBatteryShown by remember { mutableStateOf(false) } - val context = LocalContext.current - - if (isBatteryShown) { - ActionDialog( - icon = painterResource(R.drawable.battery_alert_24px), - title = stringResource(R.string.battery_dialog_title), - text = stringResource(R.string.battery_dialog_message), - confirmText = stringResource(R.string.ok), - dismissText = stringResource(R.string.cancel), - dismiss = { - isBatteryShown = false - settings.general.batterOptimization.set(false) - }, - confirm = { - isBatteryShown = false - // The og impl never modified it to true? - // settings.general.batterOptimization.set(true) - context.showRequestIgnoreBatteryOptDialog() - } - ) - } - - val default = AllLanguagesName to stringResource(R.string.all_languages_preference) - val languages = APIHolder.apis.withLock { - APIHolder.apis.map { api -> api.lang }.distinct() - }.sortedBy { fromTagToLanguageName(it) ?: it } - - return persistentListOf( - Preference.PreferenceGroup(title = stringResource(R.string.extension_language), preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - preference = settings.general.locale, - entries = appLanguages.associate { (name, code) -> (code to (name to code).nameNextToFlagEmoji()) }, - title = stringResource(R.string.app_language), - icon = painterResource(R.drawable.language_korean_latin_24px), - onValueChanged = { value -> - settings.general.locale.set(value) - activity?.recreate() - return@ListPreference false - }, - subtitleProvider = { v, e -> e[v] ?: getCurrentLocale(LocalContext.current) } - ), - Preference.PreferenceItem.MultiSelectListPreference( - title = stringResource(R.string.provider_lang_settings), - icon = painterResource(R.drawable.plugin_lang), - entries = mapOf(default) + languages.associateWith { lang -> - (getNameNextToFlagEmoji( - lang - ) ?: lang) - }, - preference = settings.provider.extensionLanguages - ), - Preference.PreferenceItem.MultiSelectListPreference( - title = stringResource(R.string.preferred_media_settings), - icon = painterResource(R.drawable.movie_edit_24px), - preference = settings.provider.preferredMedia, - entries = TvType.entries.associate { - it.ordinal.toString() to stringResource(it.toStringRes()) - } - // Ok this looks strange af, but we do this to avoid double movie - .toPersistentMap().remove(TvType.AnimeMovie.ordinal.toString()), onValueChanged = { diff -> - if(diff.contains(TvType.Movie.ordinal.toString())) { - settings.provider.preferredMedia.set(diff + TvType.AnimeMovie.ordinal.toString()) - } else { - settings.provider.preferredMedia.set(diff - TvType.AnimeMovie.ordinal.toString()) - } - return@MultiSelectListPreference false - }), - )), - - Preference.PreferenceGroup( - title = stringResource(R.string.title_downloads), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - icon = painterResource(R.drawable.netflix_download), - subtitle = downloadPathVisual, - title = stringResource(R.string.download_path_pref), - onClick = { - // This is not a ListPreference because the old selection system is - // broken af. This needs to be refactored to QuickNovels download path - // system. - selectFileSelector.launch(Uri.EMPTY) - }, - ), - Preference.PreferenceItem.SliderPreference( - icon = painterResource(R.drawable.arrow_or_edge_24px), - preference = settings.general.parallelDownloads, - valueRange = 1..10, - title = stringResource(R.string.parallel_downloads), - subtitle = stringResource(R.string.download_parallel_settings_des), - ), - Preference.PreferenceItem.SliderPreference( - icon = painterResource(R.drawable.arrow_and_edge_24px), - preference = settings.general.concurrentConnections, - valueRange = 1..10, - title = stringResource(R.string.concurrent_connections), - subtitle = stringResource(R.string.concurrent_connections_settings_des), - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.battery_dialog_title), - icon = painterResource(R.drawable.battery_alert_24px), - enabled = isLayout(PHONE), - onClick = { - if (isAppRestricted(context)) { - isBatteryShown = true - } else { - showToast(R.string.app_unrestricted_toast) - } - } - ), - ) - ), - - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_bypass), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.add_site_pref), - subtitle = stringResource(R.string.add_site_summary), - icon = painterResource(R.drawable.copy_all_24px), - onClick = { - // TODO refactor into compose - if (SettingsGeneral.getCurrent().isEmpty()) { - SettingsGeneral.showAdd() - } else { - SettingsGeneral.showAddOrDelete() - } - } - ), - Preference.PreferenceItem.ListPreference( - title = stringResource(R.string.dns_pref), - subtitle = stringResource(R.string.dns_pref_summary), - icon = painterResource(R.drawable.dns_24px), - preference = settings.general.dns, - entries = integerArrayResource(R.array.dns_pref_values).zip( - stringArrayResource(R.array.dns_pref) - ).toMap(), - onValueChanged = { newValue -> - settings.general.dns.set(newValue) // Must set the value before initClient is called - (CloudStreamApp.context)?.let { ctx -> - app.initClient(ctx, ignoreSSL = false) - @OptIn(UnsafeSSL::class) - insecureApp.initClient(ctx, ignoreSSL = true) - } - return@ListPreference true - }, - ), - Preference.PreferenceItem.SwitchPreference( - title = stringResource(R.string.jsdelivr_proxy), - subtitle = stringResource(R.string.jsdelivr_proxy_summary), - icon = painterResource(R.drawable.wifi_proxy_24px), - preference = settings.general.jsdelivrProxy - ) - ) - ), - - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_links), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.github), - subtitle = "https://github.com/recloudstream/cloudstream", - icon = painterResource(R.drawable.ic_github_logo), - onClick = { - CloudStreamApp.openBrowser("https://github.com/recloudstream/cloudstream") - } - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.lightnovel), - subtitle = "https://github.com/LagradOst/QuickNovel", - icon = painterResource(R.drawable.quick_novel_icon), - onClick = { - CloudStreamApp.openBrowser("https://github.com/LagradOst/QuickNovel") - } - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.discord), - subtitle = "https://discord.gg/5Hus6fM", - icon = painterResource(R.drawable.ic_baseline_discord_24), - onClick = { - CloudStreamApp.openBrowser("https://discord.gg/5Hus6fM") - } - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.cs3wiki), - subtitle = "https://cloudstream.miraheze.org/", - icon = painterResource(R.drawable.description_24px), - onClick = { - CloudStreamApp.openBrowser("https://cloudstream.miraheze.org/") - } - ), - ) - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.benene), - subtitle = if (bananas == 0) { - stringResource(R.string.benene_count_text_none) - } else { - stringResource(R.string.benene_count_text, bananas) - }, - onClick = { - settings.general.bananas.set(bananas + 1) - }, - icon = painterResource(R.drawable.benene), - ), - Preference.PreferenceItem.InfoPreference(title = stringResource(R.string.legal_notice_text)), - ) - } -} - - -@PreviewLightDark -@Composable -private fun SettingGeneralPreview() { - CloudStreamPreviewTheme { - SettingsGeneralScreen.Content() - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 294928115..0a0fb33c8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -205,35 +205,29 @@ class SettingsPlayer : BasePreferenceFragmentCompat() { } getPref(R.string.player_default_key)?.setOnPreferenceClickListener { - // Pair each player with its display name, dropping any with none, - // which would mean something is definitely wrong. - val players = VideoClickActionHolder.getPlayers(activity).mapNotNull { player -> - (player.name.asStringNull(activity) ?: player::class.simpleName)?.let { player to it } - } - + val players = VideoClickActionHolder.getPlayers(activity) val prefNames = buildList { - add(getString(R.string.player_settings_play_in_app)) // built-in player display name - addAll(players.map { (_, name) -> name }) + add(getString(R.string.player_settings_play_in_app)) + addAll(players.map { it.name.asStringNull(activity) ?: it.javaClass.simpleName }) } - val prefValues = buildList { - add("") // "" = built-in player, matches default - addAll(players.map { (player, _) -> player.uniqueId() }) + add("") + addAll(players.map { it.uniqueId() }) } + val current = + settingsManager.getString(getString(R.string.player_default_key), "") ?: "" - val current = settingsManager.getString(getString(R.string.player_default_key), "") ?: "" activity?.showBottomDialog( prefNames.toList(), - prefValues.indexOf(current), // finds index of currently selected player + prefValues.indexOf(current), getString(R.string.player_pref), true, - {}, + {} ) { settingsManager.edit { putString(getString(R.string.player_default_key), prefValues[it]) } } - return@setOnPreferenceClickListener true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer2.kt deleted file mode 100644 index 3c6a272d7..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer2.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import com.lagradost.cloudstream3.utils.BaseComposeFragment -import com.mihon.presentation.settings.SearchableSettings - -/** Empty glue code to connect the old navigation graph to Compose */ -class SettingsPlayer2 : BaseComposeFragment(), SearchableSettings by SettingsPlayerScreen \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayerScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayerScreen.kt deleted file mode 100644 index 8f6f5bf4f..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayerScreen.kt +++ /dev/null @@ -1,358 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import android.text.format.Formatter.formatShortFileSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.integerArrayResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringArrayResource -import androidx.compose.ui.res.stringResource -import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.actions.VideoClickActionHolder -import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize -import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment -import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.cloudstream4.compose.EMULATOR -import com.lagradost.cloudstream4.compose.PHONE -import com.lagradost.cloudstream4.compose.TV -import com.lagradost.cloudstream4.compose.isLayout -import com.lagradost.cloudstream4.rememberAppSettings -import com.mihon.presentation.settings.Preference -import com.mihon.presentation.settings.SearchableSettings -import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlin.reflect.jvm.jvmName - -object SettingsPlayerScreen : SearchableSettings { - @Composable - override fun getTitleRes(): String = stringResource(R.string.category_player) - - @Composable - override fun getPreferences(): List { - val settings = rememberAppSettings() - - val context = LocalContext.current - val defaultPlayerName = stringResource(R.string.player_settings_play_in_app) - val players = remember(context) { - mapOf("" to defaultPlayerName) + VideoClickActionHolder.getPlayers() - .associate { player -> - player.uniqueId() to (player.name.asStringNull(context) - ?: player::class.simpleName ?: player::class.jvmName) - } - } - - var cacheSize by remember { mutableLongStateOf(0L) } - var cacheCleared by remember { mutableIntStateOf(0) } - val cacheDir = LocalContext.current.cacheDir - LaunchedEffect(cacheCleared) { - withContext(Dispatchers.IO) { - cacheSize = getFolderSize(cacheDir) - } - } - - return persistentListOf( - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_subtitles), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.player_subtitles_settings), - subtitle = stringResource(R.string.player_subtitles_settings_des), - icon = painterResource(R.drawable.subtitles_gear_24px), - onClick = { - SubtitlesFragment.push(activity, false) - }), - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.chromecast_subtitles_settings), - subtitle = stringResource(R.string.chromecast_subtitles_settings_des), - icon = painterResource(R.drawable.cast), - onClick = { - ChromecastSubtitlesFragment.push(activity, false) - }), - ), - ), - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_player_features), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - title = stringResource(R.string.player_pref), - icon = painterResource(R.drawable.play_arrow_24px), - preference = settings.player.defaultPlayer, - entries = players, - ), - - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.source_priority), - subtitle = stringResource(R.string.source_priority_help), - icon = painterResource(R.drawable.ic_baseline_people_24), - onClick = { - ioSafe { - val defaultSources = QualityProfileDialog.getAllDefaultSources() - val activity = activity ?: return@ioSafe - activity.runOnUiThread { - QualityProfileDialog( - activity, - R.style.DialogFullscreenPlayer, - defaultSources, - ).show() - } - } - }), - - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.pipEnabled, - title = stringResource(R.string.picture_in_picture), - subtitle = stringResource(R.string.picture_in_picture_des), - icon = painterResource(R.drawable.pip_24px), - ), - - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.resizeEnabled, - title = stringResource(R.string.player_size_settings), - subtitle = stringResource(R.string.player_size_settings_des), - icon = painterResource(R.drawable.aspect_ratio_24px), - ), - - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.speedEnabled, - title = stringResource(R.string.eigengraumode_settings), - subtitle = stringResource(R.string.speed_setting_summary), - icon = painterResource(R.drawable.ic_baseline_speed_24), - ), - - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.compressorEnabled, - title = stringResource(R.string.compressor_setting), - subtitle = stringResource(R.string.compressor_setting_summary), - icon = painterResource(R.drawable.ic_compressor_24), - ), - - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.tiktokEnabled, - title = stringResource(R.string.speedup_title), - subtitle = stringResource(R.string.speedup_summary), - icon = painterResource(R.drawable.speedup), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.autoPlayEnabled, - title = stringResource(R.string.autoplay_next_settings), - subtitle = stringResource(R.string.autoplay_next_settings_des), - icon = painterResource(R.drawable.skip_next_24px), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.startPaused, - title = stringResource(R.string.start_paused_settings), - subtitle = stringResource(R.string.start_paused_settings_des), - icon = painterResource(R.drawable.pause_24px), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.skipOpEnabled, - title = stringResource(R.string.video_skip_op), - subtitle = stringResource(R.string.enable_skip_op_from_database_des), - icon = painterResource(R.drawable.keyboard_double_arrow_right_24px), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.autoRotateEnabled, - title = stringResource(R.string.auto_rotate_video), - subtitle = stringResource(R.string.auto_rotate_video_desc), - icon = painterResource(R.drawable.screen_rotation_alt_24px), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.rotateButtonEnabled, - title = stringResource(R.string.rotate_video), - subtitle = stringResource(R.string.rotate_video_desc), - icon = painterResource(R.drawable.screen_rotation), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.previewBarEnabled, - enabled = isLayout(PHONE or EMULATOR), - title = stringResource(R.string.preview_seekbar), - subtitle = stringResource(R.string.preview_seekbar_desc), - icon = painterResource(R.drawable.picture_in_picture_center_24px), - ), - Preference.PreferenceItem.ListPreference( - preference = settings.player.softwareDecoding, - title = stringResource(R.string.software_decoding), - subtitle = stringResource(R.string.software_decoding_desc), - icon = painterResource(R.drawable.memory_24px), - entries = integerArrayResource(R.array.software_decoding_switch_values).zip( - stringArrayResource(R.array.software_decoding_switch) - ).toMap() - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.extraBrightnessEnabled, - title = stringResource(R.string.extra_brightness_settings), - subtitle = stringResource(R.string.extra_brightness_settings_des), - icon = painterResource(R.drawable.light_mode_24px), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.episodeSync, - title = stringResource(R.string.episode_sync_settings), - subtitle = stringResource(R.string.episode_sync_settings_des), - icon = painterResource(R.drawable.autorenew_24px) - ), - ), - ), - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_gestures), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.swipeHorizontalEnabled, - title = stringResource(R.string.swipe_to_seek_settings), - subtitle = stringResource(R.string.swipe_to_seek_settings_des), - icon = painterResource(R.drawable.swipe_24px), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.swipeVerticalEnabled, - title = stringResource(R.string.swipe_to_change_settings), - subtitle = stringResource(R.string.swipe_to_change_settings_des), - icon = painterResource(R.drawable.swipe_vertical_24px), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.doubleTapToSeekEnabled, - title = stringResource(R.string.double_tap_to_seek_settings), - subtitle = stringResource(R.string.double_tap_to_seek_settings_des), - icon = painterResource(R.drawable.touch_double_24px), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.doubleTapToPauseEnabled, - title = stringResource(R.string.double_tap_to_pause_settings), - subtitle = stringResource(R.string.double_tap_to_pause_settings_des), - icon = painterResource(R.drawable.touch_double_24px), - ), - - Preference.PreferenceItem.SliderPreference( - preference = settings.player.doubleTapTime, - title = stringResource(R.string.double_tap_to_seek_amount_settings), - icon = painterResource(R.drawable.go_forward_30), - valueRange = 5..60, - //steps = 10, - ), - ) - ), - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_android_tv), - enabled = isLayout(TV or EMULATOR), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SliderPreference( - preference = settings.player.tvSeekOnTime, - title = stringResource(R.string.android_tv_interface_on_seek_settings), - subtitle = stringResource(R.string.android_tv_interface_on_seek_settings_summary), - icon = painterResource(R.drawable.go_forward_30), - valueRange = 5..60, - //steps = 10, - ), - Preference.PreferenceItem.SliderPreference( - preference = settings.player.tvSeekOffTime, - title = stringResource(R.string.android_tv_interface_off_seek_settings), - subtitle = stringResource(R.string.android_tv_interface_off_seek_settings_summary), - icon = painterResource(R.drawable.go_forward_30), - valueRange = 5..60, - //steps = 10, - ), - ) - ), - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_player_layout), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - preference = settings.player.limitPlayerTitle, - title = stringResource(R.string.limit_title), - icon = painterResource(R.drawable.match_word_24px), - entries = integerArrayResource(R.array.limit_title_pref_values).zip( - stringArrayResource(R.array.limit_title_pref_names) - ).toMap() - ), - Preference.PreferenceItem.SwitchPreference( - enabled = isLayout(PHONE or EMULATOR), - preference = settings.player.hidePlayerControlNames, - icon = painterResource(R.drawable.visibility_off_24px), - title = stringResource(R.string.hide_player_control_names), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.showName, - icon = painterResource(R.drawable.label_24px), - title = stringResource(R.string.source_name), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.showMediaInfo, - icon = painterResource(R.drawable.movie_info_24px), - title = stringResource(R.string.video_info), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.player.showResolution, - title = stringResource(R.string.resolution), - icon = painterResource(R.drawable.high_res_24px), - ), - - // Unsure if we want to have MultiSelectListPreference or boolean - /*Preference.PreferenceItem.MultiSelectListPreference( - preference = settings.player.showPlayerInfo, - title = stringResource(R.string.limit_title_rez), - icon = painterResource(R.drawable.ic_baseline_text_format_24), - entries = persistentMapOf( - ShowPlayerInfo.Name to stringResource(R.string.source_name), - ShowPlayerInfo.Resolution to stringResource(R.string.resolution), - ShowPlayerInfo.VideoInfo to stringResource(R.string.video_info), - ) - ), */ - ) - ), - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_cache), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - preference = settings.player.bufferDiskMB, - title = stringResource(R.string.video_buffer_disk_settings), - subtitle = "%s\n" + stringResource(R.string.video_disk_description), - icon = painterResource(R.drawable.hard_drive_24px), - entries = integerArrayResource(R.array.video_buffer_size_values).zip( - stringArrayResource(R.array.video_buffer_size_names) - ).toMap() - ), - - Preference.PreferenceItem.ListPreference( - preference = settings.player.bufferRamMB, - title = stringResource(R.string.video_buffer_size_settings), - subtitle = "%s\n" + stringResource(R.string.video_ram_description), - icon = painterResource(R.drawable.memory_alt_24px), - entries = integerArrayResource(R.array.video_buffer_size_values).zip( - stringArrayResource(R.array.video_buffer_size_names) - ).toMap() - ), - - Preference.PreferenceItem.ListPreference( - preference = settings.player.bufferTimeSec, - title = stringResource(R.string.video_buffer_length_settings), - subtitle = "%s\n" + stringResource(R.string.video_ram_description), - icon = painterResource(R.drawable.history_toggle_off_24px), - entries = integerArrayResource(R.array.video_buffer_length_values).zip( - stringArrayResource(R.array.video_buffer_length_names) - ).toMap() - ), - - Preference.PreferenceItem.TextPreference( - icon = painterResource(R.drawable.ic_baseline_delete_outline_24), - title = stringResource(R.string.video_buffer_clear_settings), - subtitle = formatShortFileSize(LocalContext.current, cacheSize), - onClick = { - ioSafe { - cacheDir.deleteRecursively() - cacheCleared += 1 - } - }) - ) - ), - - ) - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 08b147f52..076f17a0a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -109,13 +109,12 @@ class SettingsProviders : BasePreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - getPref(R.string. - provider_lang_key)?.setOnPreferenceClickListener { + getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener { activity?.getApiProviderLangSettings()?.let { currentLangTags -> - val languagesTagName = APIHolder.apis.withLock { - listOf(Pair(AllLanguagesName, getString(R.string.all_languages_preference))) + - APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) } - .toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } + val languagesTagName = synchronized(APIHolder.apis) { + listOf( Pair(AllLanguagesName, getString(R.string.all_languages_preference)) ) + + APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) } + .toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } // name ignoring flag emoji } val currentIndexList = currentLangTags.map { langTag -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders2.kt deleted file mode 100644 index a0faeca93..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders2.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings -import com.lagradost.cloudstream3.utils.BaseComposeFragment -import com.mihon.presentation.settings.SearchableSettings - -/** Empty glue code to connect the old navigation graph to Compose */ -@Deprecated("This setting was merged into other places") -class SettingsProviders2 : BaseComposeFragment(), SearchableSettings by SettingsProvidersScreen \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProvidersScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProvidersScreen.kt deleted file mode 100644 index 4186994c7..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProvidersScreen.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.AllLanguagesName -import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.DubStatus -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToLanguageName -import com.lagradost.cloudstream3.utils.SubtitleHelper.getNameNextToFlagEmoji -import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.cloudstream4.rememberAppSettings -import com.mihon.presentation.settings.Preference -import com.mihon.presentation.settings.SearchableSettings -import kotlinx.collections.immutable.persistentListOf - -@Deprecated("This setting was merged into other places") -object SettingsProvidersScreen : SearchableSettings { - @Composable - override fun getTitleRes(): String = stringResource(R.string.category_providers) - - fun TvType.toStringRes() = when (this) { - TvType.TvSeries -> R.string.tv_series_singular - TvType.Anime -> R.string.anime_singular - TvType.OVA -> R.string.ova_singular - TvType.AnimeMovie -> R.string.movies_singular - TvType.Cartoon -> R.string.cartoons_singular - TvType.Documentary -> R.string.documentaries_singular - TvType.Movie -> R.string.movies_singular - TvType.Torrent -> R.string.torrent_singular - TvType.AsianDrama -> R.string.asian_drama_singular - TvType.Live -> R.string.live_singular - TvType.Others -> R.string.other_singular - TvType.NSFW -> R.string.nsfw_singular - TvType.Music -> R.string.music_singular - TvType.AudioBook -> R.string.audio_book_singular - TvType.CustomMedia -> R.string.custom_media_singular - TvType.Audio -> R.string.audio_singular - TvType.Podcast -> R.string.podcast_singular - TvType.Video -> R.string.video_singular - } - - @Composable - override fun getPreferences(): List { - val settings = rememberAppSettings() - - val default = AllLanguagesName to stringResource(R.string.all_languages_preference) - val languages = APIHolder.apis.withLock { - APIHolder.apis.map { api -> api.lang }.distinct() - }.sortedBy { fromTagToLanguageName(it) ?: it } - - return persistentListOf( - Preference.PreferenceItem.MultiSelectListPreference( - title = stringResource(R.string.provider_lang_settings), - icon = painterResource(R.drawable.plugin_lang), - entries = mapOf(default) + languages.associateWith { lang -> - (getNameNextToFlagEmoji( - lang - ) ?: lang) - }, - preference = settings.provider.extensionLanguages - ), Preference.PreferenceItem.MultiSelectListPreference( - title = stringResource(R.string.preferred_media_settings), - icon = painterResource(R.drawable.movie_edit_24px), - preference = settings.provider.preferredMedia, - entries = TvType.entries.associate { - it.ordinal.toString() to stringResource(it.toStringRes()) - }), - Preference.PreferenceItem.MultiSelectListPreference( - title = stringResource(R.string.display_subbed_dubbed_settings), - icon = painterResource(R.drawable.audio_capture_24px), - preference = settings.provider.displayDubSub, - entries = mapOf( - DubStatus.None.name to stringResource(R.string.none), - DubStatus.Dubbed.name to stringResource(R.string.app_dubbed_text), - DubStatus.Subbed.name to stringResource(R.string.app_subbed_text), - ) - ), Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.test_extensions), - subtitle = stringResource(R.string.test_extensions_summary), - icon = painterResource(R.drawable.baseline_network_ping_24), - onClick = { - activity?.navigate(R.id.navigation_test_providers) - })) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt index 5f86a3a5a..f4c522bf9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt @@ -228,8 +228,6 @@ class SettingsUI : BasePreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - getPref(R.string.tv_layout_clock_key)?.hideOn(PHONE or EMULATOR) - getPref(R.string.confirm_exit_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.confirm_exit) val prefValues = resources.getIntArray(R.array.confirm_exit_values) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI2.kt deleted file mode 100644 index 86f3371fa..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI2.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import com.lagradost.cloudstream3.utils.BaseComposeFragment -import com.mihon.presentation.settings.SearchableSettings - -/** Empty glue code to connect the old navigation graph to Compose */ -class SettingsUI2 : BaseComposeFragment(), SearchableSettings by SettingsUIScreen \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUIScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUIScreen.kt deleted file mode 100644 index 1092a9be6..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUIScreen.kt +++ /dev/null @@ -1,303 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import android.os.Build -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.integerArrayResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringArrayResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.DubStatus -import com.lagradost.cloudstream3.MainActivity -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.SearchQuality -import com.lagradost.cloudstream3.mvvm.safe -import com.lagradost.cloudstream3.ui.clear -import com.lagradost.cloudstream3.ui.home.HomeChildItemAdapter -import com.lagradost.cloudstream3.ui.home.ParentItemAdapter -import com.lagradost.cloudstream3.ui.search.SearchAdapter -import com.lagradost.cloudstream3.ui.settings.Globals.updateTv -import com.lagradost.cloudstream3.utils.UIHelper.toPx -import com.lagradost.cloudstream4.compose.TV -import com.lagradost.cloudstream4.compose.isLayout -import com.lagradost.cloudstream4.rememberAppSettings -import com.lagradost.cloudstream4.theme.CloudStreamPrimaryColor -import com.lagradost.cloudstream4.theme.modeToTheme -import com.lagradost.cloudstream4.theme.perfToColor -import com.lagradost.cloudstream4.theme.perfToMode -import com.mihon.presentation.settings.Preference -import com.mihon.presentation.settings.SearchableSettings -import kotlinx.collections.immutable.mutate -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentMap - -object SettingsUIScreen : SearchableSettings { - @Composable - override fun getTitleRes(): String = stringResource(R.string.category_ui) - - fun SearchQuality.toStringRes() = when (this) { - SearchQuality.BlueRay -> R.string.quality_blueray - SearchQuality.Cam -> R.string.quality_cam - SearchQuality.CamRip -> R.string.quality_cam_rip - SearchQuality.DVD -> R.string.quality_dvd - SearchQuality.HD -> R.string.quality_hd - SearchQuality.HQ -> R.string.quality_hq - SearchQuality.HdCam -> R.string.quality_cam_hd - SearchQuality.Telecine -> R.string.quality_tc - SearchQuality.Telesync -> R.string.quality_ts - SearchQuality.WorkPrint -> R.string.quality_workprint - SearchQuality.SD -> R.string.quality_sd - SearchQuality.FourK -> R.string.quality_4k - SearchQuality.UHD -> R.string.quality_uhd - SearchQuality.SDR -> R.string.quality_sdr - SearchQuality.HDR -> R.string.quality_hdr - SearchQuality.WebRip -> R.string.quality_webrip - } - - @Composable - fun RoundColor(color: Color) { - Box( - modifier = Modifier - .padding(start = 15.dp) - .size(20.dp) - .border(width = 1.5.dp, shape = CircleShape, color = MaterialTheme.colorScheme.onBackground) - .background(color, CircleShape) - ) - } - - @Composable - override fun getPreferences(): List { - val settings = rememberAppSettings() - - return persistentListOf( - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_looks), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - preference = settings.ui.primaryColor, - icon = painterResource(R.drawable.colors_24px), - title = stringResource(R.string.primary_color_settings), - entries = stringArrayResource(R.array.themes_overlay_names_values).zip( - stringArrayResource(R.array.themes_overlay_names) - ).toMap().toPersistentMap().mutate { map -> - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less - map.remove("Monet") - map.remove("Monet2") - } - }, - iconProvider = { k, _ -> - val color = perfToColor(k) - RoundColor(color.color) - }, - onValueChanged = { newValue -> - settings.ui.primaryColor.set(newValue) // We need to set before we recreate - safe { - activity?.recreate() - } - return@ListPreference false - }), - Preference.PreferenceItem.ListPreference( - preference = settings.ui.theme, - icon = painterResource(R.drawable.palette_24px), - title = stringResource(R.string.app_theme_settings), - entries = stringArrayResource(R.array.themes_names_values).zip( - stringArrayResource(R.array.themes_names) - ).toMap().toPersistentMap().mutate { map -> - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less - map.remove("Monet") - } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { // Remove system on android 9 and less - map.remove("System") - } - }, - iconProvider = { k, _ -> - val theme = modeToTheme(perfToMode(k), CloudStreamPrimaryColor.NORMAL) - RoundColor(theme.background) - }, - onValueChanged = { newValue -> - settings.ui.theme.set(newValue) // We need to set before we recreate - safe { - activity?.recreate() - } - return@ListPreference false - }), - Preference.PreferenceItem.ListPreference( - preference = settings.ui.layout, - icon = painterResource(R.drawable.responsive_layout_24px), - title = stringResource(R.string.app_layout), - entries = integerArrayResource(R.array.app_layout_values).zip( - stringArrayResource(R.array.app_layout) - ).toMap().toPersistentMap(), - onValueChanged = { newValue -> - settings.ui.layout.set(newValue) // We need to set before we recreate - safe { - activity?.updateTv() - activity?.recreate() - } - return@ListPreference false - }), - ) - ), - - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_ui_features), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SliderPreference( - preference = settings.ui.overscanDp, - title = stringResource(R.string.overscan_settings), - subtitle = stringResource(R.string.overscan_settings_des), - valueRange = 0..100, - icon = painterResource(R.drawable.arrows_input_24px), - enabled = isLayout(TV), - onValueChanged = { newValue -> - settings.ui.overscanDp.set(newValue) - val padding = newValue.toPx - (activity as? MainActivity)?.binding?.homeRoot?.setPadding( - padding, padding, padding, padding - ) - return@SliderPreference false - }), - - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.advancedSearch, - title = stringResource(R.string.advanced_search), - subtitle = stringResource(R.string.advanced_search_des), - icon = painterResource(R.drawable.search_icon) - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.searchSuggestions, - title = stringResource(R.string.search_suggestions), - subtitle = stringResource(R.string.search_suggestions_des), - icon = painterResource(R.drawable.tooltip_24px) - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.trailersEnabled, - title = stringResource(R.string.show_trailers_settings), - icon = painterResource(R.drawable.baseline_theaters_24) - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.kitsuPostersEnabled, - title = stringResource(R.string.kitsu_settings), - icon = painterResource(R.drawable.kitsu_icon) - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.castEnabled, - title = stringResource(R.string.show_cast_in_details), - icon = painterResource(R.drawable.face_24px) - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.fillersEnabled, - title = stringResource(R.string.show_fillers_settings), - icon = painterResource(R.drawable.skip_next_24px) - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.showMetadataOverlay, - title = stringResource(R.string.show_player_metadata_overlay), - icon = painterResource(R.drawable.metadata_overlay_icon) - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.showClock, - title = stringResource(R.string.tv_layout_clock_settings), - subtitle = stringResource(R.string.tv_layout_clock_settings_des), - icon = painterResource(R.drawable.ic_baseline_clock_24), - enabled = isLayout(TV), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.randomButtonEnabled, - title = stringResource(R.string.random_button_settings), - subtitle = stringResource(R.string.random_button_settings_desc), - icon = painterResource(R.drawable.shuffle_24px) - ), - Preference.PreferenceItem.ListPreference( - preference = settings.ui.confirmExit, - title = stringResource(R.string.confirm_before_exiting_title), - subtitle = stringResource(R.string.confirm_before_exiting_desc), - icon = painterResource(R.drawable.ic_baseline_exit_24), - entries = integerArrayResource(R.array.confirm_exit_values).zip( - stringArrayResource(R.array.confirm_exit) - ).toMap() - ), - ) - ), - - Preference.PreferenceGroup( - title = stringResource(R.string.search_poster_img_des), - preferenceItems = persistentListOf( - Preference.PreferenceItem.MultiSelectListPreference( - preference = settings.ui.filterQuality, - title = stringResource(R.string.pref_filter_search_quality), - icon = painterResource(R.drawable.filter_alt_24px), - entries = SearchQuality.entries.associateWith { stringResource(it.toStringRes()) }), - Preference.PreferenceItem.MultiSelectListPreference( - title = stringResource(R.string.display_subbed_dubbed_settings), - icon = painterResource(R.drawable.audio_capture_24px), - preference = settings.provider.displayDubSub, - entries = mapOf( - DubStatus.None.name to stringResource(R.string.none), - DubStatus.Dubbed.name to stringResource(R.string.app_dubbed_text), - DubStatus.Subbed.name to stringResource(R.string.app_subbed_text), - )), - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.bottomTitle, - title = stringResource(R.string.bottom_title_settings), - subtitle = stringResource(R.string.bottom_title_settings_des), - icon = painterResource(R.drawable.title_24px) - ), - Preference.PreferenceItem.SliderPreference( - preference = settings.ui.posterSize, - title = stringResource(R.string.poster_size_settings), - subtitle = stringResource(R.string.poster_size_settings_des), - valueRange = 0..15, - icon = painterResource(R.drawable.baseline_grid_view_24), - onValueChanged = { newValue -> - HomeChildItemAdapter.sharedPool.clear() - ParentItemAdapter.sharedPool.clear() - SearchAdapter.sharedPool.clear() - activity?.let { HomeChildItemAdapter.updatePosterSize(it, newValue) } - settings.ui.posterSize.set(newValue) - return@SliderPreference false - }), - ), - ), - Preference.PreferenceGroup( - title = stringResource(R.string.poster_ui_settings), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.posterShowRating, - title = stringResource(R.string.show_rating), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.posterShowEpisode, - title = stringResource(R.string.show_episode_text), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.posterShowTitle, - title = stringResource(R.string.show_title), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.posterShowHd, - title = stringResource(R.string.show_hd), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.posterShowDub, - title = stringResource(R.string.show_dub), - ), - Preference.PreferenceItem.SwitchPreference( - preference = settings.ui.posterShowSub, - title = stringResource(R.string.show_sub), - ), - ) - ) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates2.kt deleted file mode 100644 index 50070a384..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates2.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import com.lagradost.cloudstream3.utils.BaseComposeFragment -import com.mihon.presentation.settings.SearchableSettings - -/** Empty glue code to connect the old navigation graph to Compose */ -class SettingsUpdates2 : BaseComposeFragment(), SearchableSettings by SettingsUpdatesScreen \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdatesScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdatesScreen.kt deleted file mode 100644 index cc867ae25..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdatesScreen.kt +++ /dev/null @@ -1,236 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import android.content.Intent -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.res.integerArrayResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringArrayResource -import androidx.compose.ui.res.stringResource -import com.lagradost.cloudstream3.BuildConfig -import com.lagradost.cloudstream3.CloudStreamApp -import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.MainActivityScreen -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.services.BackupWorkManager -import com.lagradost.cloudstream3.utils.BackupUtils -import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt -import com.lagradost.cloudstream3.utils.InAppUpdater.installPreReleaseIfNeeded -import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.cloudstream4.AppSettings -import com.lagradost.cloudstream4.rememberAppSettings -import com.lagradost.cloudstream4.viewmodels.GithubAction -import com.lagradost.safefile.SafeFile -import com.mihon.presentation.settings.Preference -import com.mihon.presentation.settings.SearchableSettings -import com.mihon.presentation.settings.collectAsState -import kotlinx.collections.immutable.persistentListOf - -object SettingsUpdatesScreen : SearchableSettings { - @Composable - override fun getTitleRes(): String = stringResource(R.string.category_updates) - - @Composable - override fun getPreferences(): List { - val settings = rememberAppSettings() - - // TODO Refactor entirely to use a different file path selector ect like QuickNovel - val selectFileSelector = - rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> - // It lies, it can be null if file manager quits. - if (uri == null) return@rememberLauncherForActivityResult - val context = CloudStreamApp.context ?: return@rememberLauncherForActivityResult - - try { - val settings = AppSettings(context) - // RW perms for the path - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - context.contentResolver.takePersistableUriPermission(uri, flags) - - val filePath = SafeFile.fromUri(context, uri)?.filePath() - println("Selected URI path: $uri - Full path: $filePath") - - // store the actual URI instead of the path due to permissions. - // filePath should only be used for cosmetic purposes. - val visual = filePath ?: uri.toString() - settings.backup.path.set(uri.toString()) - settings.backup.visualPath.set(visual) - } catch (t: Throwable) { - logError(t) - } - } - - val visualBackupPath by settings.backup.visualPath.collectAsState() - var showDialog by remember { mutableStateOf(false) } - - if (showDialog) { - com.lagradost.cloudstream3.ui.settings.logcat.LogcatDialog { - showDialog = false - } - } - - val githubViewModel = MainActivityScreen.githubViewModel() - - return persistentListOf( - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_app_updates), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.check_for_update), - subtitle = BuildConfig.VERSION_NAME, - icon = painterResource(R.drawable.mobile_arrow_down_24px), - onClick = { - githubViewModel?.onAction(GithubAction.SearchForUpdate) - /*ioSafe { - if (activity?.runAutoUpdate(false) == false) { - activity?.runOnUiThread { - showToast( - R.string.no_update_found, - Toast.LENGTH_SHORT - ) - } - } - }*/ - } - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.install_prerelease), - icon = painterResource(R.drawable.mobile_code_24px), - enabled = BuildConfig.FLAVOR == "stable", - onClick = { - activity?.installPreReleaseIfNeeded() - } - ), - - Preference.PreferenceItem.ListPreference( - title = stringResource(R.string.apk_installer_settings), - subtitle = stringResource(R.string.apk_installer_settings_des), - icon = painterResource(R.drawable.mobile_wrench_24px), - entries = integerArrayResource(R.array.apk_installer_values).zip( - stringArrayResource(R.array.apk_installer_pref) - ).toMap(), - preference = settings.updates.apkInstaller - ), - - Preference.PreferenceItem.SwitchPreference( - title = stringResource(R.string.updates_settings), - subtitle = stringResource(R.string.updates_settings_des), - icon = painterResource(R.drawable.notifications_active_24px), - preference = settings.updates.showAppUpdates - ) - ) - ), - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_backup), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.backup_settings), - icon = painterResource(R.drawable.save_as_24px), - onClick = { - BackupUtils.backup(activity) - } - ), - Preference.PreferenceItem.ListPreference( - title = stringResource(R.string.backup_frequency), - icon = painterResource(R.drawable.save_clock_24px), - entries = integerArrayResource(R.array.periodic_work_values).zip( - stringArrayResource(R.array.periodic_work_names) - ).toMap(), - preference = settings.backup.frequency, - onValueChanged = { value -> - BackupWorkManager.enqueuePeriodicWork( - CloudStreamApp.context, - value.toLong() - ) - return@ListPreference true - } - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.backup_path_title), - icon = painterResource(R.drawable.folder_24px), - subtitle = visualBackupPath, - onClick = { - // This is not a ListPreference because the old selection system is - // broken af. This needs to be refactored to QuickNovels download path - // system. - selectFileSelector.launch(Uri.EMPTY) - } - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.restore_settings), - icon = painterResource(R.drawable.restore_page_24px), - onClick = { - activity?.restorePrompt() - } - ), - ) - ), - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_extensions), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - title = stringResource(R.string.automatic_plugin_updates), - icon = painterResource(R.drawable.extension_24px), - preference = settings.plugins.autoUpdate, - ), - - /*Preference.PreferenceItem.ListPreference( - title = stringResource(R.string.automatic_plugin_download), - subtitle = "%s\n" + stringResource(R.string.automatic_plugin_download_summary), - icon = painterResource(R.drawable.extention_renew2), - entries = AutoDownloadMode.entries.map { it.value }.sorted() - .zip(stringArrayResource((R.array.auto_download_plugin))).toMap(), - preference = settings.plugins.autoDownload - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.update_plugins), - subtitle = stringResource(R.string.update_plugins_manually), - icon = painterResource(R.drawable.extention_download), - onClick = { - ioSafe { - PluginManager.___DO_NOT_CALL_FROM_A_PLUGIN_manuallyReloadAndUpdatePlugins( - activity ?: return@ioSafe - ) - } - } - ),*/ - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.test_extensions), - subtitle = stringResource(R.string.test_extensions_summary), - icon = painterResource(R.drawable.baseline_network_ping_24), - onClick = { - activity?.navigate(R.id.navigation_test_providers) - }) - ) - ), - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_actions), - preferenceItems = persistentListOf( - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.show_log_cat), - icon = painterResource(R.drawable.article_24px), - onClick = { - showDialog = true - } - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.redo_setup_process), - icon = painterResource(R.drawable.construction_24px), - onClick = { - activity?.navigate(R.id.navigation_setup_language) - } - ), - ) - ) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/AndroidRepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/AndroidRepositoryManager.kt deleted file mode 100644 index 3fbc67a74..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/AndroidRepositoryManager.kt +++ /dev/null @@ -1,282 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings.extensions - -import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey -import com.lagradost.cloudstream3.CommonActivity -import com.lagradost.cloudstream3.ErrorLoadingException -import com.lagradost.cloudstream3.InternalAPI -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.cmap -import com.lagradost.cloudstream3.plugins.PLUGIN_VERSION_NOT_SET -import com.lagradost.cloudstream3.plugins.PluginData -import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.plugins.PluginManager.deletePlugin -import com.lagradost.cloudstream3.plugins.PluginManager.getPluginPath -import com.lagradost.cloudstream3.plugins.PluginManager.unloadPlugin -import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES -import com.lagradost.cloudstream3.plugins.RepositoryManager.addRepository -import com.lagradost.cloudstream3.plugins.RepositoryManager.convertRawGitUrl -import com.lagradost.cloudstream3.plugins.RepositoryManager.parseRepoUrl -import com.lagradost.cloudstream3.plugins.RepositoryManager.removeRepository -import com.lagradost.cloudstream3.plugins.SitePlugin -import com.lagradost.cloudstream3.ui.settings.ApkUpdater -import com.lagradost.cloudstream3.ui.settings.ApkUpdater.isFileDigest -import com.lagradost.cloudstream4.viewmodels.DigestPair -import com.lagradost.cloudstream4.viewmodels.Plugin -import com.lagradost.cloudstream4.viewmodels.PluginDownloadSource -import com.lagradost.cloudstream4.viewmodels.PluginInfo -import com.lagradost.cloudstream4.viewmodels.PluginStatus -import com.lagradost.cloudstream4.viewmodels.RepositoryInfo -import com.lagradost.cloudstream4.viewmodels.RepositoryManager -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.io.File -import java.nio.file.AtomicMoveNotSupportedException -import java.nio.file.Files -import java.nio.file.StandardCopyOption -import java.util.concurrent.TimeUnit -import kotlin.uuid.Uuid - -@InternalAPI -object AndroidRepositoryManager : RepositoryManager { - fun RepositoryInfo.toData() = - RepositoryData( - name = name, - url = url, - iconUrl = iconUrl, - description = description - ) - - fun RepositoryData.toInfo() = - RepositoryInfo( - name = name, - url = url, - iconUrl = iconUrl, - description = description - ) - - override fun getRepositories(): List { - return ((getKey>(REPOSITORIES_KEY) - ?: emptyArray()) + PREBUILT_REPOSITORIES).map { info -> - info.toInfo() - } - } - - override suspend fun getPlugins( - repository: RepositoryInfo, - repositoryUuid: Uuid - ): Pair> = - withContext(Dispatchers.IO) { - val activity = - CommonActivity.activity ?: throw ErrorLoadingException("No activity found") - - val url = parseRepoUrl(repository.url) - if (url.isNullOrBlank()) { - throw ErrorLoadingException(activity.getString(R.string.error_invalid_url)) - } - - val parsedRepository = app.get( - url = convertRawGitUrl(url), - cacheTime = 15, - cacheUnit = TimeUnit.MINUTES - ).parsed() - - /** - * No sane repository should be able to DDoS the app when navigating to settings, so - * we limit this to a maximum of 10 - * - * Note that this list is on all verified repos = 1, as pluginLists is a list of lists - * */ - require(parsedRepository.pluginLists.size <= 10) - - /** Add this as a new repository -> replaces the old data with new icon,name and description */ - addRepository( - RepositoryData( - iconUrl = parsedRepository.iconUrl, - url = url, - name = parsedRepository.name, - description = parsedRepository.description - ) - ) - - val plugins = parsedRepository.pluginLists.cmap { url -> - /** This should really be a list of size ~ 1 */ - app.get( - url = convertRawGitUrl(url), - cacheTime = 15, - cacheUnit = TimeUnit.MINUTES - ).parsed>() - }.flatMap { plugins -> - plugins.map { plugin -> - val file = getPluginPath( - context = activity, - internalName = plugin.internalName, - repositoryUrl = url - ) - val language = plugin.language?.trim() - Plugin( - info = PluginInfo( - name = plugin.name, - status = plugin.status, - internalName = plugin.internalName, - version = plugin.version, - tvTypes = plugin.tvTypes?.mapNotNull { runCatching { TvType.valueOf(it) }.getOrNull() } - ?.toPersistentList() - ?: persistentListOf(), - authors = plugin.authors.toPersistentList(), - iconUrl = plugin.iconUrl, - language = if (language.isNullOrBlank()) { - null - } else { - language - }, - description = plugin.description, - repositoryUrl = plugin.repositoryUrl - ), - source = PluginDownloadSource( - size = plugin.fileSize, - repositoryUrl = url, - pluginUrl = plugin.url, - internalName = plugin.internalName, - digest = DigestPair.parse(plugin.fileHash) - ), - status = PluginStatus.Idle, - downloadLocation = if (file.exists()) { - file.absolutePath - } else { - null - }, - repository = repositoryUuid, - ) - } - } - - val info = RepositoryInfo( - iconUrl = parsedRepository.iconUrl, - url = url, - name = parsedRepository.name, - description = parsedRepository.description, - ) - info to plugins - } - - override suspend fun downloadPlugin( - source: PluginDownloadSource, - downloadProgress: (Long, Long?) -> Unit - ): String = withContext(Dispatchers.IO) { - val context = CommonActivity.activity ?: throw ErrorLoadingException("No activity found") - val file = getPluginPath( - context = context, - internalName = source.internalName, - repositoryUrl = source.repositoryUrl - ) - - /** - * Before we do anything that needs a network connection, - * we actually check if we need to update it - * - * Because if the file digest is correct, then why do we need to do a network connection? - * */ - if (isFileDigest(file, source.digest)) { - return@withContext file.absolutePath - } - - val tempFile = File.createTempFile(file.name, ".tmp", context.cacheDir) - - tempFile.outputStream().use { writeStream -> - app.get(convertRawGitUrl(source.pluginUrl)).body.use { body -> - val readStream = body.byteStream() - ApkUpdater.transfer( - writeStream = writeStream, - readStream = readStream, - length = source.size, - downloadProgress = downloadProgress, - digest = source.digest - ) - } - } - - // Always load the plugin - unloadPlugin(file.absolutePath) - - file.delete() // delete if we somehow already have this downloaded? - file.parentFile?.mkdirs() // make the directories as Files.move needs this to work - - try { - Files.move( - tempFile.toPath(), - file.toPath(), - StandardCopyOption.REPLACE_EXISTING, - StandardCopyOption.ATOMIC_MOVE - ) - } catch (_: AtomicMoveNotSupportedException) { - Files.move( - tempFile.toPath(), - file.toPath(), - StandardCopyOption.REPLACE_EXISTING - ) - } - - val result = PluginManager.loadPlugin( - context = context, - file = file, - data = PluginData( - internalName = source.internalName, - url = source.pluginUrl, - isOnline = true, - filePath = file.absolutePath, - version = PLUGIN_VERSION_NOT_SET - ) - ) - if (!result) { - throw ErrorLoadingException("Unable to load plugin ${source.internalName}") - } - - return@withContext file.absolutePath - } - - override fun getLocalPlugins(localUuid: Uuid): List { - val plugins = PluginManager.getPluginsLocal() - return plugins.map { plugin -> - Plugin( - info = PluginInfo( - name = plugin.internalName, - internalName = plugin.internalName, - version = plugin.version, - tvTypes = persistentListOf(), - authors = persistentListOf(), - status = 1, - iconUrl = null, - language = null, - description = null, - repositoryUrl = null - ), - source = null, - status = PluginStatus.Idle, - downloadLocation = plugin.filePath, - repository = localUuid, - ) - } - } - - override suspend fun deletePlugin(location: String) { - deletePlugin(File(location)) - } - - override suspend fun deleteRepository(repository: RepositoryInfo) { - val context = CommonActivity.activity ?: throw ErrorLoadingException("No activity found") - removeRepository( - context, - repository.toData() - ) - } - - override suspend fun updateRepositoryInfo(repository: RepositoryInfo) { - addRepository( - repository.toData() - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 4d6567856..af0d3dfe7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -5,12 +5,10 @@ import android.content.Context import android.content.DialogInterface import android.os.Build import android.view.LayoutInflater -import android.view.MenuItem import android.view.View import android.widget.LinearLayout import android.widget.Toast import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.SearchView import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.marginBottom @@ -28,7 +26,6 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.ui.BaseFragment import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout -import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setSystemBarsPadding @@ -39,8 +36,6 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import com.lagradost.cloudstream3.utils.UIHelper.hideProgress -import com.lagradost.cloudstream3.utils.UIHelper.showProgress import com.lagradost.cloudstream3.utils.setText class ExtensionsFragment : BaseFragment( @@ -48,7 +43,6 @@ class ExtensionsFragment : BaseFragment( ) { private val extensionViewModel: ExtensionsViewModel by activityViewModels() - private val pluginViewModel: PluginsViewModel by activityViewModels() private fun View.setLayoutWidth(weight: Int) { val param = LinearLayout.LayoutParams( @@ -116,7 +110,11 @@ class ExtensionsFragment : BaseFragment( adapter = RepoAdapter(false, { findNavController().navigate( R.id.navigation_settings_extensions_to_navigation_settings_plugins, - PluginsFragment.newInstance(it) + PluginsFragment.newInstance( + it.name, + it.url, + false + ) ) }, { repo -> // Prompt user before deleting repo @@ -128,10 +126,7 @@ class ExtensionsFragment : BaseFragment( when (which) { DialogInterface.BUTTON_POSITIVE -> { ioSafe { - RepositoryManager.removeRepository( - uiContext.applicationContext, - repo - ) + RepositoryManager.removeRepository(uiContext.applicationContext, repo) extensionViewModel.loadStats() extensionViewModel.loadRepositories() } @@ -150,11 +145,10 @@ class ExtensionsFragment : BaseFragment( }) } - observe(extensionViewModel.repositories) { repos -> - binding.repoRecyclerView.isVisible = repos.isNotEmpty() - binding.blankRepoScreen.isVisible = repos.isEmpty() - (binding.repoRecyclerView.adapter as? RepoAdapter)?.submitList(repos.toList()) - pluginViewModel.updatePluginList(binding.root.context, repos.toList()) + observe(extensionViewModel.repositories) { + binding.repoRecyclerView.isVisible = it.isNotEmpty() + binding.blankRepoScreen.isVisible = it.isEmpty() + (binding.repoRecyclerView.adapter as? RepoAdapter)?.submitList(it.toList()) } observeNullable(extensionViewModel.pluginStats) { value -> @@ -183,75 +177,14 @@ class ExtensionsFragment : BaseFragment( binding.pluginStorageAppbar.setOnClickListener { findNavController().navigate( R.id.navigation_settings_extensions_to_navigation_settings_plugins, - PluginsFragment.newLocalInstance( + PluginsFragment.newInstance( getString(R.string.extensions), + "", + true ) ) } - binding.pluginRecyclerView.apply { - setLinearListLayout( - isHorizontal = false, - nextDown = FOCUS_SELF, - nextRight = FOCUS_SELF, - ) - setRecycledViewPool(PluginAdapter.sharedPool) - adapter = - PluginAdapter(true) { - val urls = extensionViewModel.repositories.value?.toList() ?: emptyList() - pluginViewModel.handlePluginAction(activity, urls, it, false) - } - } - - observe(pluginViewModel.filteredPlugins) { (scrollToTop, list) -> - (binding.pluginRecyclerView.adapter as? PluginAdapter)?.submitList(list) - if (scrollToTop) { - binding.pluginRecyclerView.scrollToPosition(0) - } - } - - binding.settingsToolbar.apply { - val searchItem = menu?.findItem(R.id.search_button) - val searchView = searchItem?.actionView as? SearchView - - searchItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { - override fun onMenuItemActionCollapse(p0: MenuItem): Boolean { - binding.pluginRecyclerView.isVisible = false - binding.repoRecyclerView.isVisible = true - return true - - } - - override fun onMenuItemActionExpand(p0: MenuItem): Boolean { - binding.pluginRecyclerView.isVisible = true - binding.repoRecyclerView.isVisible = false - return true - } - }) - - // Don't go back if active query - setNavigationOnClickListener { - if (searchView?.isIconified == false) { - searchView.isIconified = true - } else { - dispatchBackPressed() - } - } - - searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String?): Boolean { - pluginViewModel.search(query) - return true - } - - override fun onQueryTextChange(newText: String?): Boolean { - pluginViewModel.search(newText) - return true - } - }) - } - - val addRepositoryClick = View.OnClickListener { val ctx = context ?: return@OnClickListener val binding = AddRepoInputBinding.inflate(LayoutInflater.from(ctx), null, false) @@ -266,10 +199,7 @@ class ExtensionsFragment : BaseFragment( )?.text?.toString()?.let { copiedText -> if (copiedText.contains(RepoAdapter.SHAREABLE_REPO_SEPARATOR)) { // text is of format : - val (name, url) = copiedText.split( - RepoAdapter.SHAREABLE_REPO_SEPARATOR, - limit = 2 - ) + val (name, url) = copiedText.split(RepoAdapter.SHAREABLE_REPO_SEPARATOR, limit = 2) binding.repoUrlInput.setText(url.trim()) binding.repoNameInput.setText(name.trim()) } else { @@ -280,18 +210,13 @@ class ExtensionsFragment : BaseFragment( binding.applyBtt.setOnClickListener secondListener@{ val name = binding.repoNameInput.text?.toString() val urlInput = binding.repoUrlInput.text?.toString() - if (urlInput.isNullOrEmpty()) { - showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT) - return@secondListener - } - binding.applyBtt.showProgress() ioSafe { - try { - val url = RepositoryManager.parseRepoUrl(urlInput) - if (url.isNullOrBlank()) { + val url = urlInput?.let { it1 -> RepositoryManager.parseRepoUrl(it1) } + if (url.isNullOrBlank()) { + main { showToast(R.string.error_invalid_data, Toast.LENGTH_SHORT) - return@ioSafe } + } else { val repository = RepositoryManager.parseRepository(url) // Exit if wrong repository @@ -302,33 +227,29 @@ class ExtensionsFragment : BaseFragment( val fixedName = if (!name.isNullOrBlank()) name else repository.name - val newRepo = RepositoryData(repository.iconUrl, fixedName, url, repository.description) + val newRepo = RepositoryData(repository.iconUrl,fixedName, url) RepositoryManager.addRepository(newRepo) extensionViewModel.loadStats() extensionViewModel.loadRepositories() - dialog.dismissSafe(activity) // Only dismiss if the repo was added - - val plugins = RepositoryManager.getRepoPlugins(newRepo) + val plugins = RepositoryManager.getRepoPlugins(url) if (plugins.isNullOrEmpty()) { showToast(R.string.no_plugins_found_error, Toast.LENGTH_LONG) - return@ioSafe + } else { + this@ExtensionsFragment.activity?.addRepositoryDialog( + fixedName, + url, + ) } - - this@ExtensionsFragment.activity?.addRepositoryDialog( - newRepo - ) - } finally { - binding.applyBtt.hideProgress() } } + dialog.dismissSafe(activity) } binding.cancelBtt.setOnClickListener { dialog.dismissSafe(activity) } } - val isTv = isLayout(TV) binding.apply { addRepoButton.isGone = isTv diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment2.kt deleted file mode 100644 index 7041887df..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment2.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings.extensions - -import com.lagradost.cloudstream3.utils.BaseComposeFragment -import com.lagradost.cloudstream4.compose.Screen - -class ExtensionsFragment2 : BaseComposeFragment(), Screen by ExtensionsScreen \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsScreen.kt deleted file mode 100644 index 3725a99da..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsScreen.kt +++ /dev/null @@ -1,1922 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings.extensions - -import android.annotation.SuppressLint -import android.text.format.Formatter.formatShortFileSize -import android.util.Log -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.focusGroup -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.defaultMinSize -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.layout.sizeIn -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.input.clearText -import androidx.compose.foundation.text.input.rememberTextFieldState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.FloatingActionButtonDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ProvideTextStyle -import androidx.compose.material3.Text -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1 -import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2 -import androidx.compose.ui.focus.focusProperties -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.focusRestorer -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.Clipboard -import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringArrayResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.PreviewLightDark -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.findNavController -import coil3.compose.AsyncImage -import com.fasterxml.jackson.core.JacksonException -import com.lagradost.cloudstream3.BuildConfig -import com.lagradost.cloudstream3.CloudStreamApp.Companion.openBrowser -import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.ui.settings.SettingsFragmentScreen -import com.lagradost.cloudstream3.ui.settings.SettingsProvidersScreen.toStringRes -import com.lagradost.cloudstream3.ui.settings.extensions.RepoAdapter.Companion.SHAREABLE_REPO_SEPARATOR -import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToLanguageName -import com.lagradost.cloudstream3.utils.SubtitleHelper.getNameNextToFlagEmoji -import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.cloudstream4.compose.ActionDialog -import com.lagradost.cloudstream4.compose.BlackButton -import com.lagradost.cloudstream4.compose.BlackTextField -import com.lagradost.cloudstream4.compose.IsScrolling -import com.lagradost.cloudstream4.compose.LogBoxBlack -import com.lagradost.cloudstream4.compose.LogBoxWhite -import com.lagradost.cloudstream4.compose.LogText -import com.lagradost.cloudstream4.compose.MultiSelectDialog -import com.lagradost.cloudstream4.compose.PHONE -import com.lagradost.cloudstream4.compose.RoundedShape -import com.lagradost.cloudstream4.compose.Screen -import com.lagradost.cloudstream4.compose.TV -import com.lagradost.cloudstream4.compose.WhiteButton -import com.lagradost.cloudstream4.compose.WhiteFilterChip -import com.lagradost.cloudstream4.compose.WhiteIconButton -import com.lagradost.cloudstream4.compose.focusOutline -import com.lagradost.cloudstream4.compose.isLayout -import com.lagradost.cloudstream4.compose.rounded -import com.lagradost.cloudstream4.generated.resources.Res -import com.lagradost.cloudstream4.generated.resources.no_results_found -import com.lagradost.cloudstream4.getPlainText -import com.lagradost.cloudstream4.rememberAppSettings -import com.lagradost.cloudstream4.setPlainText -import com.lagradost.cloudstream4.state.LogLevel -import com.lagradost.cloudstream4.state.ObserveEffect -import com.lagradost.cloudstream4.state.SearchableData -import com.lagradost.cloudstream4.state.SortByName -import com.lagradost.cloudstream4.theme.CloudStreamPreviewTheme -import com.lagradost.cloudstream4.viewmodels.AddRepositoryDialog -import com.lagradost.cloudstream4.viewmodels.DerivedRepositoryState -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.CancelPluginDownloader -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.CancelPluginsUpdate -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.ClearPluginSearch -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.DeletePlugin -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.DeleteRepository -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.DownloadPlugin -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.DownloadRepositoryWithDialog -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.FilterByInverseLanguages -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.FilterByTypes -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.Init -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.ReloadRepository -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.Search -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.SelectPlugin -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.SelectRepository -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.SetRepositoryDialog -import com.lagradost.cloudstream4.viewmodels.ExtensionsAction.UpdateAllPlugins -import com.lagradost.cloudstream4.viewmodels.ExtensionsEffect -import com.lagradost.cloudstream4.viewmodels.ExtensionsState -import com.lagradost.cloudstream4.viewmodels.ExtensionsViewModel2 -import com.lagradost.cloudstream4.viewmodels.FilterPlugin -import com.lagradost.cloudstream4.viewmodels.Plugin -import com.lagradost.cloudstream4.viewmodels.PluginStatus -import com.lagradost.cloudstream4.viewmodels.PluginsUpdatingDialog -import com.lagradost.cloudstream4.viewmodels.Repository -import com.lagradost.cloudstream4.viewmodels.RepositoryInfo -import com.lagradost.cloudstream4.viewmodels.RepositoryStatus -import com.mihon.material.AppBar -import com.mihon.material.padding -import com.mihon.presentation.LocalBackPress -import com.mihon.presentation.secondaryItemAlpha -import com.mihon.presentation.settings.LocalPreferenceHighlighted -import com.mihon.presentation.settings.LocalPreferenceMinHeight -import com.mihon.presentation.settings.widget.BasePreferenceWidget -import com.mihon.presentation.settings.widget.PrefsHorizontalPadding -import com.mihon.presentation.settings.widget.TextPreferenceWidget -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.persistentMapOf -import kotlinx.collections.immutable.toPersistentSet -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.serialization.SerializationException -import kotlin.math.roundToInt -import kotlin.time.Duration.Companion.seconds -import kotlin.uuid.Uuid - -@Composable -@PreviewLightDark -fun Preview() { - CloudStreamPreviewTheme { - val uuid1 = Uuid.random() - val uuid2 = Uuid.random() - val uuid3 = Uuid.random() - ExtensionsScreen.Content( - state = ExtensionsState( - //addDialog = AddRepositoryDialog(error = Throwable()), - /*updateAllPluginsDialog = PluginsUpdatingDialog( - total = 5, - downloaded = 4, - failed = 0 - ),*/ - repositories = SearchableData.from( - data = persistentMapOf( - uuid1 to Repository( - uuid = uuid1, - info = RepositoryInfo( - name = "Cloudstream providers repository", - description = "hello", - url = "https://raw.githubusercontent.com/recloudstream/extensions/refs/heads/master/repo.json", - iconUrl = null - ), - status = RepositoryStatus.Loading, - sortingOrder = 0, - ), - uuid2 to Repository( - uuid = uuid2, - info = RepositoryInfo( - name = "codeberg test", - url = "https://codeberg.org/", - iconUrl = null - ), - status = RepositoryStatus.Loading, - sortingOrder = 1, - ), - uuid3 to Repository( - uuid = uuid3, - info = RepositoryInfo( - name = "parsebin test", - url = "https://pastebin.com/", - iconUrl = null - ), - status = RepositoryStatus.Loading, - sortingOrder = 2, - ) - ), - sortedBy = SortByName { it.info.name } - ) - ), - onAction = {} - ) - } -} - - -object ExtensionsScreen : Screen { - @Composable - override fun Content() { - val settings = rememberAppSettings() - val viewModel = - viewModel { - ExtensionsViewModel2( - manager = AndroidRepositoryManager, - settings = settings - ) - } - LaunchedEffect(Unit) { // TODO fix this to only do if necessary - viewModel.onAction(Init) - } - val state by viewModel.state.collectAsState() - val highLight = remember { mutableStateOf(null) } - ObserveEffect(viewModel.effect) { effect -> - when (effect) { - is ExtensionsEffect.RepositoryAdded -> { - highLight.value = effect.uuid - } - } - } - Content(state = state, onAction = viewModel::onAction, highLightState = highLight) - } - - @Composable - fun Content( - state: ExtensionsState, - onAction: (ExtensionsAction) -> Unit, - highLightState: MutableState = remember { mutableStateOf(null) }, - ) { - val (topFocusRequester, bottomFocusRequester) = remember { FocusRequester.createRefs() } - val fabExpanded = remember { mutableStateOf(true) } - - com.mihon.material.Scaffold( - // TODO @WindowInsets remove this when we have converted everything to compose - contentWindowInsets = WindowInsets(0, 0, 0, 0), - topBar = { - AppBar( - modifier = Modifier - .focusRequester(topFocusRequester) - .focusProperties { - down = bottomFocusRequester - }, - title = if (state.pluginFilter.repository != null) { - state.repositories[state.pluginFilter.repository]?.info?.name - /** Should never happen */ - ?: stringResource(R.string.extensions) - } else if (state.searchPlugins) { - stringResource(R.string.search) - } else { - stringResource(R.string.extensions) - }, - navigateUp = LocalBackPress.current, - actions = { - TopActionRow(state = state, onAction = onAction) - }, - scrollBehavior = it, - ) - }, - bottomBar = { - BottomBar(state = state, onAction = onAction) - }, - floatingActionButton = { - if (isLayout(PHONE) && !state.searchPlugins) { - FloatingActionButton(state = state, onAction = onAction, expanded = fabExpanded) - } - }, - content = { contentPadding -> - Content( - contentPadding = contentPadding, - state = state, - onAction = onAction, - highLightState = highLightState, - fabExpanded = fabExpanded, - focusRequester = bottomFocusRequester, - topBarFocusRequester = topFocusRequester - ) - }, - ) - } - - @Composable - fun TopActionRow(state: ExtensionsState, onAction: (ExtensionsAction) -> Unit) { - val scope = rememberCoroutineScope() - val clipboard = LocalClipboard.current - - var extraDialog by remember { mutableStateOf(false) } - if (!state.searchPlugins) { - if (isLayout(TV)) { - WhiteIconButton( - onClick = { - scope.launch { - openRepositoryDialog(clipboard = clipboard, onAction = onAction) - } - }) { - Icon( - tint = MaterialTheme.colorScheme.onBackground, - painter = painterResource(R.drawable.add_24px), - contentDescription = stringResource(R.string.add_repository) - ) - } - } - - WhiteIconButton( - onClick = { - extraDialog = true - }) { - Icon( - tint = MaterialTheme.colorScheme.onBackground, - painter = painterResource(R.drawable.ic_baseline_more_vert_24), - contentDescription = stringResource(R.string.extensions) - ) - } - } - - DropdownMenu(expanded = extraDialog, onDismissRequest = { - extraDialog = false - }, shape = RoundedShape()) { - DropdownMenuItem(text = { - Text(text = stringResource(R.string.add_repository)) - }, onClick = { - scope.launch { - openRepositoryDialog(clipboard = clipboard, onAction = onAction) - extraDialog = false - } - }) - DropdownMenuItem(text = { - Text(text = stringResource(R.string.category_provider_test)) - }, onClick = { - extraDialog = false - activity?.navigate(R.id.navigation_test_providers) - }) - DropdownMenuItem(text = { - Text(text = stringResource(R.string.installed_plugins)) - }, onClick = { - extraDialog = false - openInstalledPlugins() - }) - DropdownMenuItem(text = { - Text(text = stringResource(R.string.update_plugins_manually)) - }, onClick = { - extraDialog = false - onAction(UpdateAllPlugins) - }) - - /** - * Easier to debug the system if we have this. - * No need to translate this IMPO - * */ - if (BuildConfig.DEBUG) { - DropdownMenuItem(text = { - Text(text = "Delete all repositories") - }, onClick = { - extraDialog = false - onAction(ExtensionsAction.DeleteAllRepositories) - }) - } - } - - var languageDialog by remember { mutableStateOf(false) } - var deleteDialog by remember { mutableStateOf(false) } - val derivedRepositoryState = state.derivedRepositoryState - - val activeRepository = state.pluginFilter.repository - if (state.searchPlugins && activeRepository != null && derivedRepositoryState.notDownloaded != derivedRepositoryState.total) { - WhiteIconButton(onClick = { - deleteDialog = true - }) { - Icon( - tint = MaterialTheme.colorScheme.onBackground, - painter = painterResource(R.drawable.delete_sweep_24px), - contentDescription = stringResource(R.string.delete_repository) - ) - } - } - - /** We only show the langauge filter button if we are searching plugins and there is languages to pick from*/ - if (state.searchPlugins && derivedRepositoryState.languageCount.isNotEmpty()) { - WhiteIconButton(onClick = { - languageDialog = true - }) { - Icon( - tint = MaterialTheme.colorScheme.onBackground, - painter = painterResource(R.drawable.plugin_lang), - contentDescription = stringResource(R.string.extension_language) - ) - } - } - - if (deleteDialog && activeRepository != null) { - ActionDialog( - title = stringResource(R.string.delete_files), - text = stringResource(R.string.delete_repository_plugins), - icon = painterResource(R.drawable.delete_sweep_24px), - dismiss = { - deleteDialog = false - }, - confirm = { - onAction(ExtensionsAction.DeletePluginsInRepository(activeRepository)) - deleteDialog = false - }, - confirmText = stringResource(R.string.delete), - dismissText = stringResource(R.string.cancel), - ) - } - - if (languageDialog) { - val entries = - derivedRepositoryState.languageCount.keys.sortedBy { lang -> - fromTagToLanguageName(lang) ?: lang - } - - MultiSelectDialog( - title = stringResource(R.string.extension_language), - entries = entries.associateWith { lang -> - if (lang == "") { - stringResource(R.string.no_data) - } else { - (getNameNextToFlagEmoji( - lang - ) ?: lang) - } - }, - properties = DialogProperties( - usePlatformDefaultWidth = true, - ), - confirmText = stringResource(R.string.ok), - confirm = { selection -> - val removed = - derivedRepositoryState.languageCount.keys.subtract(selection) - onAction( - FilterByInverseLanguages( - removed.toPersistentSet() - ) - ) - languageDialog = false - }, - dismissText = stringResource(R.string.cancel), - dismiss = { - languageDialog = false - }, - selectedKeys = derivedRepositoryState.languageCount.keys.subtract( - state.pluginFilter.removeLanguages - ), - ) - } - } - - /** To make the UI even remotely usable, we batch the types */ - val contentTypes: PersistentList>> = persistentListOf( - R.string.movies_singular to persistentListOf(TvType.Movie), - R.string.tv_series_singular to persistentListOf(TvType.TvSeries), - R.string.anime_singular to persistentListOf(TvType.Anime, TvType.AnimeMovie, TvType.OVA), - R.string.cartoons_singular to persistentListOf(TvType.Cartoon), - R.string.asian_drama_singular to persistentListOf(TvType.AsianDrama), - R.string.live_singular to persistentListOf(TvType.Live), - R.string.documentaries_singular to persistentListOf(TvType.Documentary), - R.string.torrent_singular to persistentListOf(TvType.Torrent), - R.string.audio_singular to persistentListOf( - TvType.Audio, - TvType.AudioBook, - TvType.Podcast, - TvType.Music - ), - R.string.nsfw_singular to persistentListOf(TvType.NSFW), - R.string.others to persistentListOf( - TvType.CustomMedia, - TvType.Video, - TvType.Others - ), - ) - - fun openInstalledPlugins() { - val activity = activity ?: return - activity.findNavController(R.id.nav_host_fragment).navigate( - R.id.navigation_settings_extensions_to_navigation_settings_plugins, - PluginsFragment.newLocalInstance( - activity.getString(R.string.extensions), - ) - ) - } - - @SuppressLint("UnrememberedMutableState") - @Composable - fun Content( - contentPadding: PaddingValues, - state: ExtensionsState, - onAction: (ExtensionsAction) -> Unit, - highLightState: MutableState, - fabExpanded: MutableState, - topBarFocusRequester: FocusRequester, - focusRequester: FocusRequester, - ) { - AddDialog(dialog = state.addDialog, onAction = onAction) - UpdateDialog(dialog = state.updateAllPluginsDialog, onAction = onAction) - - val lazyRepoState = rememberLazyListState() - val textState = rememberTextFieldState() - val lazyState = if (state.searchPlugins) { - rememberLazyListState() - } else { - lazyRepoState - } - - BackHandler(enabled = state.searchPlugins) { - textState.clearText() - onAction(ClearPluginSearch) - } - - /** Fab scrolling, this works but is currently a bit too fast IMPO */ - lazyRepoState.IsScrolling(up = { - fabExpanded.value = true - }, down = { - fabExpanded.value = false - }) - - /** - * We are using mihons highlight here, meaning that we scroll for a few sec - * and then remove the highlight to prevent backpresses from rehighlighting it. - * The highlight is never in the viewmodel as it is a view issue. - * */ - var highLight by highLightState - LaunchedEffect(highLight) { - if (highLight == null) return@LaunchedEffect - val list = if (state.searchPlugins) { - state.plugins.sorted - } else { - state.repositories.sorted - } - val index = list.indexOfFirst { uuid -> uuid == highLight } - if (index != -1) { - lazyRepoState.animateScrollToItem( - /** We have some extra items here */ - index + 2 - ) - } - delay(5.0.seconds) - highLight = null - } - - /** - * This may look a bit weird, but it is very jarring to snap the UI like this - * (because we want to have 2 different scrolls) - * - * The user expects a transition, so we fade it using animateTo + remember(repository), - * NOT snapTo(0.0f) as snapTo is 1 recomposition too slow! - * */ - val itemAlpha = remember(state.pluginFilter.repository) { Animatable(0f) } - LaunchedEffect(state.pluginFilter.repository) { - itemAlpha.snapTo(0.0f) - itemAlpha.animateTo(1f) - } - - LaunchedEffect(textState.text) { - onAction(Search(textState.text.toString())) - } - - LazyColumn( - state = lazyState, - contentPadding = contentPadding, - modifier = Modifier - .fillMaxSize() - .focusGroup() - .focusRequester(focusRequester) - .focusProperties { - up = topBarFocusRequester - }, - ) { - item(key = "header") { - Spacer(modifier = Modifier.height(MaterialTheme.padding.medium)) - SettingsFragmentScreen.SettingsSearch(textState) - } - if (state.searchPlugins) { - item(key = "filter") { - Spacer(modifier = Modifier.height(MaterialTheme.padding.small)) - - /** - * In the case of a filter applied to the repository, - * we want to use the associated derived state to calculate what types this - * repository has - * */ - val derivedState = - state.pluginFilter.repository?.let(state.plugins.state.repositories::get) - ?: state.plugins.state.sumOfRepositories - - FilterRow( - filter = state.pluginFilter, - derivedState = derivedState, - onAction = onAction - ) - } - - items(items = state.plugins.sorted, key = { uuid -> uuid }) { uuid -> - val plugin = state.plugins[uuid]!! - val isHighlighted = uuid == highLight - CompositionLocalProvider( - LocalPreferenceHighlighted provides isHighlighted, - ) { - Plugin( - plugin = plugin, - onAction = onAction, - modifier = Modifier - .animateItem() - .graphicsLayer { alpha = itemAlpha.value }, - onClick = if (state.pluginFilter.repository != null) { - null - } else { - { - onAction(SelectPlugin(plugin.uuid)) - highLightState.value = plugin.uuid - } - } - ) - } - } - - val repository = state.pluginFilter.repository - val status = state.repositories[repository]?.status - if (state.plugins.sorted.isEmpty()) { - when (status) { - is RepositoryStatus.Failure -> { - item { - ErrorButton(status.cause) - } - } - - RepositoryStatus.Loading -> { - item { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.onBackground, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - ) - } - } - - RepositoryStatus.Success, null -> { - val derivedState = - repository?.let(state.plugins.state.repositories::get) - - item { - Text( - text = - if (repository != null && (derivedState == null || derivedState.total == 0)) { - /** - * If we have a repository filter on, - * but it does not contain anything then show this - * instead as we do not really filter it but - * the repo is simply empty. This should mostly be used - * for plugin authors - * */ - stringResource(R.string.no_plugins_found_error) - } else { - org.jetbrains.compose.resources.stringResource(Res.string.no_results_found) - }, - modifier = Modifier - .secondaryItemAlpha() - .fillMaxWidth() - .padding(20.dp), - textAlign = TextAlign.Center - ) - } - } - } - } - } else { - if (state.repositories.sorted.isEmpty()) { - item { - Text( - text = stringResource(R.string.blank_repo_message), - modifier = Modifier - .secondaryItemAlpha() - .fillMaxWidth() - .padding(20.dp), - textAlign = TextAlign.Center - ) - } - item { - val scope = rememberCoroutineScope() - val clipboard = LocalClipboard.current - val minHeight = LocalPreferenceMinHeight.current - Row( - modifier = Modifier - .focusOutline() - .sizeIn(minHeight = minHeight) - .combinedClickable(onClick = { - scope.launch { - openRepositoryDialog( - clipboard = clipboard, - onAction = onAction - ) - } - }) - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - ) { - Icon( - painter = painterResource(R.drawable.add_24px), - tint = MaterialTheme.colorScheme.onBackground, - contentDescription = stringResource(R.string.add_repository) - ) - Spacer(modifier = Modifier.width(MaterialTheme.padding.medium)) - Text( - text = stringResource(R.string.add_repository), - style = MaterialTheme.typography.bodyMedium - ) - } - } - } else { - item(key = "items top padding") { - Spacer(modifier = Modifier.height(MaterialTheme.padding.small)) - } - } - - items(items = state.repositories.sorted, key = { uuid -> uuid }) { uuid -> - val repository = state.repositories[uuid]!! - val isHighlighted = uuid == highLight - val derivedState = state.plugins.state.repositories[uuid] - - CompositionLocalProvider( - LocalPreferenceHighlighted provides isHighlighted, - ) { - Repository( - repository = repository, - onAction = onAction, - derivedState = derivedState, - modifier = Modifier - .animateItem() - .graphicsLayer { alpha = itemAlpha.value } - ) - } - } - } - } - } - - @Composable - fun FilterRow( - filter: FilterPlugin, - derivedState: DerivedRepositoryState, - onAction: (ExtensionsAction) -> Unit, - ) { - val defaultFocusRequester = remember { FocusRequester() } - - LazyRow( - modifier = Modifier - .fillMaxWidth() - .focusGroup() - .focusRestorer(defaultFocusRequester), - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), - contentPadding = PaddingValues(horizontal = MaterialTheme.padding.small) - ) { - var first = true - for ((text, types) in contentTypes) { - if (types.none(derivedState::containsType)) { - continue - } - val modifier = if (first) { - first = false - Modifier.focusRequester(defaultFocusRequester) - } else { - Modifier - } - item(key = text) { - FilterSelection( - modifier = modifier, - filter = filter, - text = text, - types = types, - onAction = onAction - ) - } - } - } - } - - @Composable - fun FilterSelection( - filter: FilterPlugin, - text: Int, - types: PersistentList, - onAction: (ExtensionsAction) -> Unit, - modifier: Modifier = Modifier, - ) { - val selected = - filter.tvTypes.contains(types[0]) - - WhiteFilterChip( - modifier = modifier, - selected = selected, - label = stringResource(text), - ) { - onAction( - FilterByTypes( - if (selected) { - filter.tvTypes.removingAll(types) - } else { - filter.tvTypes.addingAll(types) - } - ) - ) - } - } - - @Composable - fun ErrorDialog( - error: Throwable, - tag: String, - confirmButton: @Composable ((Modifier) -> Unit), - onDismissRequest: () -> Unit - ) { - val message = error.toString() - val stackTrace = error.stackTraceToString() - .lines() - .mapNotNull { line -> - val trimmed = line.trim() - if (!trimmed.startsWith("at ")) return@mapNotNull null - // pull "File.kt:42" out of "at com.pkg.Class.method(File.kt:42)" - Regex("""\(([^)]+)\)$""").find(trimmed)?.groupValues?.get(1) - ?.replace(":", " ") - } - .joinToString("\n") - - val (logFocusRequester, dismissFocusRequester) = remember { FocusRequester.createRefs() } - val isTv = isLayout(TV) - LaunchedEffect(Unit) { - if (isTv) { - dismissFocusRequester.requestFocus() - } - } - - AlertDialog( - containerColor = MaterialTheme.colorScheme.background, - onDismissRequest = onDismissRequest, - confirmButton = { - confirmButton(Modifier) - }, - dismissButton = { - BlackButton( - text = stringResource(R.string.dismiss), - onClick = onDismissRequest, - modifier = Modifier - .focusRequester(dismissFocusRequester) - ) - }, - text = { - val clipboard = LocalClipboard.current - val scope = rememberCoroutineScope() - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .focusRequester(logFocusRequester) - .focusProperties { - down = dismissFocusRequester - }) { - Row(modifier = Modifier.fillMaxWidth()) { - LogBoxWhite(LogLevel.Error.identifier) - LogBoxBlack(tag) - } - LogText( - level = LogLevel.Error, - message = message, - onClick = { - scope.launch { - clipboard.setPlainText("Error", message) - } - }, - ) - if (stackTrace.isNotBlank()) { - LogText( - level = LogLevel.Error, - message = stackTrace, - onClick = { - scope.launch { - clipboard.setPlainText("Stacktrace", stackTrace) - } - }) - } - } - }, - title = { - Text(text = stringResource(R.string.error)) - }) - } - - @Composable - fun Plugin( - plugin: Plugin, - onAction: (ExtensionsAction) -> Unit, - modifier: Modifier, - onClick: (() -> Unit)?, - ) { - val description = plugin.info.description - var extendedDialog by remember { mutableStateOf(false) } - val isTv = isLayout(TV) - val isPluginDisabledByAuthor = plugin.info.status == PROVIDER_STATUS_DOWN - - if (extendedDialog) { - val dismissFocusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { - if (isTv) { - dismissFocusRequester.requestFocus() - } - } - - AlertDialog( - containerColor = MaterialTheme.colorScheme.background, - onDismissRequest = { - extendedDialog = false - }, - confirmButton = { - val repository = plugin.info.repositoryUrl - if (repository != null) { - /** TODO filter this based on trusted urls? */ - WhiteButton(text = stringResource(R.string.result_open_in_browser)) { - openBrowser(repository) - } - } - }, - dismissButton = { - BlackButton( - modifier = Modifier.focusRequester(dismissFocusRequester), - text = stringResource(R.string.dismiss), - onClick = { extendedDialog = false } - ) - }, - text = { - Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) { - ProvideTextStyle( - LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.onBackground, - ) - ) { - val roundedBackground = - Modifier - .background( - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedShape() - ) - .padding(MaterialTheme.padding.extraSmall) - - val description = plugin.info.description - if (description.isNullOrBlank()) { - InfoRow( - modifier = roundedBackground, - text = R.string.extension_description, - content = stringResource(R.string.no_data) - ) - } else { - Text( - text = stringResource(R.string.extension_description), - modifier = roundedBackground - ) - Text(text = description) - } - - if (plugin.info.authors.isNotEmpty()) { - InfoRow( - modifier = roundedBackground, - text = R.string.extension_authors, - content = plugin.info.authors.joinToString() - ) - } - InfoRow( - modifier = roundedBackground, - text = R.string.extension_version, - content = plugin.info.version.toString() - ) - InfoRow( - modifier = roundedBackground, - text = R.string.extension_status, - content = stringArrayResource(R.array.extension_statuses).getOrNull( - plugin.info.status - ) ?: stringResource(R.string.no_data) - ) - val size = plugin.source?.size - if (size != null) { - InfoRow( - modifier = roundedBackground, - text = R.string.extension_size, - content = formatShortFileSize(LocalContext.current, size) - ) - } - if (plugin.info.tvTypes.isNotEmpty()) { - InfoRow( - modifier = roundedBackground, - text = R.string.extension_types, - content = - @Suppress("SimplifiableCallChain") - plugin.info.tvTypes.map { stringResource(it.toStringRes()) } - .joinToString()) - } - InfoRow( - modifier = roundedBackground, - text = R.string.extension_language, - content = plugin.info.language?.let { language -> - getNameNextToFlagEmoji(language) ?: language - } ?: stringResource(R.string.no_data)) - } - } - }, - title = { - Row( - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), - verticalAlignment = Alignment.CenterVertically - ) { - RepositoryIcon( - iconUrl = plugin.info.iconUrl, - defaultIcon = if (isPluginDisabledByAuthor) { - painterResource(R.drawable.extension_off_24px) - } else { - painterResource(R.drawable.extension_24px) - } - ) - Text(text = plugin.info.name) - } - }) - } - - val (backgroundFocusRequester, buttonsFocusRequester) = - remember { FocusRequester.createRefs() } - - - BasePreferenceWidget( - modifier = modifier - .focusRequester(backgroundFocusRequester) - .focusProperties { - end = buttonsFocusRequester - }, - title = - if (isPluginDisabledByAuthor) { - stringResource(R.string.single_plugin_disabled, plugin.info.name) - } else { - plugin.info.name - }, - subcomponent = - { - Column(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) { - FlowRow( - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), - verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small) - ) { - val language = plugin.info.language - if (!language.isNullOrBlank()) { - Text( - text = getNameNextToFlagEmoji(language) ?: language, - modifier = Modifier - .secondaryItemAlpha(), - style = MaterialTheme.typography.bodySmall, - ) - } - Text( - text = "v${plugin.info.version}", - modifier = Modifier - .secondaryItemAlpha(), - style = MaterialTheme.typography.bodySmall, - ) - val sizeBytes = plugin.source?.size - if (sizeBytes != null) { - Text( - text = formatShortFileSize(LocalContext.current, sizeBytes), - modifier = Modifier - .secondaryItemAlpha(), - style = MaterialTheme.typography.bodySmall, - ) - } - if (plugin.info.tvTypes.contains(TvType.NSFW)) { - Text( - text = "18+", - style = MaterialTheme.typography.bodySmall, - color = colorResource(R.color.adultColor) - ) - } - } - if (!description.isNullOrBlank()) { - Text( - text = description, - modifier = Modifier - .secondaryItemAlpha(), - style = MaterialTheme.typography.bodySmall, - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) - } - } - }, - icon = { - RepositoryIcon( - iconUrl = plugin.info.iconUrl, - defaultIcon = - if (isPluginDisabledByAuthor) { - painterResource(R.drawable.extension_off_24px) - } else { - painterResource(R.drawable.extension_24px) - } - ) - }, - onClick = onClick ?: { - extendedDialog = true - }, - onLongClick = { - extendedDialog = true - }, - widget = { - PluginWidget( - plugin = plugin, - onAction = onAction, - returnFocusRequester = backgroundFocusRequester, - modifier = Modifier - .focusRequester(buttonsFocusRequester) - .focusProperties { - start = backgroundFocusRequester - } - .focusGroup() - ) - }, - ) - } - - @Composable - fun PluginWidget( - plugin: Plugin, - onAction: (ExtensionsAction) -> Unit, - modifier: Modifier, - returnFocusRequester: FocusRequester, - ) { - var errorDialog by remember { mutableStateOf(null) } - val errorCause = errorDialog - if (errorCause != null) { - ErrorDialog( - error = errorCause, - tag = stringResource( - R.string.plugin_load_fail, - plugin.info.name - ), - confirmButton = { modifier -> - if (plugin.downloadLocation != null) { - WhiteButton( - modifier = modifier, - text = stringResource(R.string.delete_plugin), - onClick = { - errorDialog = null - onAction(DeletePlugin(plugin.uuid)) - }) - } else { - WhiteButton( - modifier = modifier, - text = stringResource(R.string.reload_error), - onClick = { - errorDialog = null - onAction(DownloadPlugin(plugin.uuid)) - }) - } - }, - onDismissRequest = { - errorDialog = null - } - ) - } - - val isPluginDisabledByAuthor = plugin.info.status == PROVIDER_STATUS_DOWN - val isDownloadDisabled = isPluginDisabledByAuthor || plugin.source == null - Row( - modifier = modifier - ) { - val cs3Plugin = plugin.downloadLocation?.let { location -> - (plugin.source?.pluginUrl?.let(PluginManager.urlPlugins::get) - ?: (PluginManager.plugins[location])) as? com.lagradost.cloudstream3.plugins.Plugin - } - val settings = cs3Plugin?.openSettings - - val (button1FocusRequester, button2FocusRequester) = remember { - FocusRequester.createRefs() - } - - if (settings != null) { - val context = LocalContext.current - WhiteIconButton( - modifier = Modifier - .focusRequester(button1FocusRequester) - .focusProperties { - start = returnFocusRequester - end = button2FocusRequester - }, - onClick = { - try { - settings.invoke(context) - } catch (e: Throwable) { - Log.e( - "PluginAdapter", - "Failed to open ${plugin.info.name} settings: ${ - Log.getStackTraceString(e) - }" - ) - } - }) { - Icon( - painter = painterResource(R.drawable.ic_baseline_tune_24), - contentDescription = null - ) - } - } - - WhiteIconButton( - modifier = Modifier - .focusRequester(button2FocusRequester) - .focusProperties { - start = if (settings == null) { - returnFocusRequester - } else { - button1FocusRequester - } - }, - onClick = { - when (val status = plugin.status) { - is PluginStatus.Downloading -> { - onAction(CancelPluginDownloader(plugin.uuid)) - } - - is PluginStatus.Failed -> { - errorDialog = status.cause - } - - PluginStatus.Idle -> { - if (plugin.downloadLocation != null) { - onAction(DeletePlugin(plugin.uuid)) - } else if (isDownloadDisabled) { - /** The author has disabled the plugin, so do nothing here */ - } else { - onAction(DownloadPlugin(plugin.uuid)) - } - } - } - }, - ) { - when (val status = plugin.status) { - is PluginStatus.Downloading -> { - val total = status.total - if (total != null) { - CircularProgressIndicator( - progress = { - status.progress.toFloat() / total.toFloat() - }, - modifier = Modifier - .size(IconButtonDefaults.smallContainerSize()) - .padding(MaterialTheme.padding.small), - color = MaterialTheme.colorScheme.onBackground, - ) - } else { - CircularProgressIndicator( - modifier = Modifier - .size(IconButtonDefaults.smallContainerSize()) - .padding(MaterialTheme.padding.small), - color = MaterialTheme.colorScheme.onBackground, - ) - } - } - - is PluginStatus.Failed -> { - Icon( - painter = painterResource(R.drawable.warning_24px), - contentDescription = stringResource(R.string.error) - ) - } - - PluginStatus.Idle -> { - if (plugin.downloadLocation != null) { - Icon( - painter = painterResource(R.drawable.delete_24px), - contentDescription = stringResource(R.string.delete_plugin) - ) - } else if (isDownloadDisabled) { - Icon( - painter = painterResource(R.drawable.visibility_off_24px), - contentDescription = stringResource( - R.string.single_plugin_disabled, - plugin.info.name - ) - ) - } else { - Icon( - painter = painterResource(R.drawable.arrow_circle_down_24px), - contentDescription = stringResource(R.string.download) - ) - } - } - } - } - } - } - - @Composable - fun InfoRow( - modifier: Modifier, - text: Int, - content: String, - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(text), - modifier = modifier - ) - Text(text = content) - } - } - - @Composable - fun UpdateDialog(dialog: PluginsUpdatingDialog?, onAction: (ExtensionsAction) -> Unit) { - if (dialog == null) { - return - } - val finished = dialog.progress >= dialog.total - - AlertDialog( - containerColor = MaterialTheme.colorScheme.background, - onDismissRequest = { - onAction(CancelPluginsUpdate) - }, - confirmButton = { - }, - dismissButton = { - BlackButton( - text = if (finished) { - stringResource(R.string.dismiss) - } else { - stringResource(R.string.cancel) - }, onClick = { - onAction(CancelPluginsUpdate) - }) - }, - text = { - Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) { - if (dialog.failed > 0) { - Text( - text = - stringResource(R.string.plugins_not_downloaded, dialog.failed) - ) - } - if (!finished) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.onBackground, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - progress = { - if (dialog.total == 0) { - 1.0f - } else { - dialog.progress.toFloat() / dialog.total.toFloat() - } - } - ) - } - } - }, - title = { - Text(text = stringResource(R.string.plugins_updated, dialog.downloaded)) - }) - } - - @Composable - fun AddDialog(dialog: AddRepositoryDialog?, onAction: (ExtensionsAction) -> Unit) { - if (dialog == null) { - return - } - - val nextFocusRequester = remember { FocusRequester() } - - AlertDialog( - containerColor = MaterialTheme.colorScheme.background, - onDismissRequest = { - onAction(SetRepositoryDialog(null)) - }, - confirmButton = { - WhiteButton(text = stringResource(R.string.add_repository), onClick = { - onAction(DownloadRepositoryWithDialog(url = dialog.url)) - }, modifier = Modifier.focusRequester(nextFocusRequester)) - }, - dismissButton = { - BlackButton(text = stringResource(R.string.cancel), onClick = { - onAction(SetRepositoryDialog(null)) - }) - }, - text = { - Column { - BlackTextField( - value = dialog.url, - placeHolder = stringResource(R.string.repository_url_hint), - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri), - keyboardActions = KeyboardActions( - onDone = { - onAction(DownloadRepositoryWithDialog(url = dialog.url)) - }, - onNext = { - nextFocusRequester.requestFocus() - } - ), - singleLine = true - ) { newValue -> - onAction( - SetRepositoryDialog( - dialog.copy( - url = newValue, - error = null - ) - ) - ) - } - Crossfade(dialog.loading) { loading -> - if (loading) { - Column { - Spacer(modifier = Modifier.height(MaterialTheme.padding.medium)) - LinearProgressIndicator( - modifier = Modifier.width(TextFieldDefaults.MinWidth), - color = MaterialTheme.colorScheme.onBackground, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - ) - } - } else { - ErrorButton(dialog.error) - } - } - } - }, - title = { - Text(text = stringResource(R.string.add_repository)) - }) - } - - @Composable - fun ErrorButton(error: Throwable?) { - var shownError by remember { mutableStateOf(false) } - - if (shownError && error != null) { - ErrorDialog( - error = error, - tag = stringResource(R.string.error_invalid_url), - confirmButton = {}, - onDismissRequest = { - shownError = false - }) - } - - AnimatedVisibility(error != null, enter = fadeIn(), exit = fadeOut()) { - if (error == null) { - return@AnimatedVisibility - } - - val message = error.message - TextPreferenceWidget( - modifier = Modifier - .rounded() - .width(TextFieldDefaults.MinWidth), - title = when { - error is JacksonException -> { - stringResource(R.string.no_repository_found_error) - } - - error is SerializationException -> { - stringResource(R.string.no_repository_found_error) - } - - message != null -> { - message - } - - else -> stringResource(R.string.error_invalid_data) - }, - icon = painterResource(R.drawable.warning_24px), - iconTint = LocalContentColor.current - ) { - shownError = true - } - } - } - - @Composable - fun RepositoryIcon(iconUrl: String?, defaultIcon: Painter) { - if (iconUrl != null) { - AsyncImage( - model = remapUrl(url = iconUrl, size = 24.dp), - contentDescription = null, - modifier = Modifier.size(24.dp), - error = defaultIcon - ) - } else { - Icon( - painter = defaultIcon, - tint = MaterialTheme.colorScheme.onBackground, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - } - } - - @Composable - fun FloatingActionButton( - state: ExtensionsState, - expanded: MutableState, - onAction: (ExtensionsAction) -> Unit - ) { - val clipboard = LocalClipboard.current - val scope = rememberCoroutineScope() - - ExtendedFloatingActionButton( - containerColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onBackground, - elevation = FloatingActionButtonDefaults.elevation( - defaultElevation = 0.dp, - pressedElevation = 0.dp, - focusedElevation = 0.dp, - hoveredElevation = 0.dp - ), - expanded = expanded.value, - text = { - Text(stringResource(R.string.add_repository)) - }, - icon = { - Icon( - painter = painterResource(R.drawable.add_24px), - contentDescription = stringResource(R.string.add_repository) - ) - }, - onClick = { - scope.launch { - openRepositoryDialog(clipboard, onAction) - } - }) - } - - suspend fun openRepositoryDialog( - clipboard: Clipboard, - onAction: (ExtensionsAction) -> Unit - ) { - val copiedText = clipboard.getPlainText()?.trim() ?: "" - - /** text is of format : */ - val url = if (copiedText.contains(SHAREABLE_REPO_SEPARATOR)) { - val (_, url) = copiedText.split( - SHAREABLE_REPO_SEPARATOR, - limit = 2 - ) - url.trim() - } - /** Can this realistically be an url? */ - else if (copiedText.length < 200 && !copiedText.contains("\n")) { - copiedText - } - /** Otherwise we just do a clear */ - else { - "" - } - onAction( - SetRepositoryDialog( - AddRepositoryDialog(url = url) - ) - ) - } - - @Composable - fun BottomBar( - state: ExtensionsState, - onAction: (ExtensionsAction) -> Unit - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surfaceVariant) - .clickable( - enabled = isLayout(PHONE), - onClick = { - openInstalledPlugins() - }) - .padding(MaterialTheme.padding.small) - ) { - Text(stringResource(R.string.extensions)) - Spacer(modifier = Modifier.height(MaterialTheme.padding.small)) - val shape = RoundedCornerShape(3.dp) - val size = 10.dp - Row(modifier = Modifier.clip(shape)) { - Box( - modifier = Modifier - .height(size) - .background(color = MaterialTheme.colorScheme.onBackground) - .weight( - maxOf( - state.plugins.state.downloadedAndEnabled.toFloat(), - 0.001f - ) - ) - ) - Box( - modifier = Modifier - .height(size) - .background(color = MaterialTheme.colorScheme.primary) - .weight( - maxOf( - state.plugins.state.downloadedAndDisabled.toFloat(), - 0.001f - ) - ) - ) - Box( - modifier = Modifier - .height(size) - .background(color = MaterialTheme.colorScheme.onSurfaceVariant) - .weight(maxOf(state.plugins.state.notDownloaded.toFloat(), 0.02f)) - ) - } - Spacer(modifier = Modifier.height(MaterialTheme.padding.small)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy( - MaterialTheme.padding.extraSmall - ) - ) { - DownloadBox( - text = stringResource( - R.string.plugins_downloaded, - state.plugins.state.downloadedAndEnabled - ), color = MaterialTheme.colorScheme.onBackground - ) - DownloadBox( - text = stringResource( - R.string.plugins_disabled, - state.plugins.state.downloadedAndDisabled - ), color = MaterialTheme.colorScheme.primary - ) - DownloadBox( - text = stringResource( - R.string.plugins_not_downloaded, - state.plugins.state.notDownloaded - ), color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - - @Composable - fun Repository( - modifier: Modifier, - repository: Repository, - derivedState: DerivedRepositoryState?, - onAction: (ExtensionsAction) -> Unit - ) { - var deleteDialog by remember { mutableStateOf(false) } - - if (deleteDialog) { - ActionDialog( - icon = painterResource(R.drawable.delete_24px), - title = stringResource(R.string.delete_repository), - text = stringResource(R.string.delete_repository_plugins), - dismiss = { - deleteDialog = false - }, - confirm = { - onAction(DeleteRepository(repository.uuid)) - deleteDialog = false - }, - confirmText = stringResource(R.string.delete), - dismissText = stringResource(R.string.cancel), - ) - } - - val clipboard = LocalClipboard.current - val scope = rememberCoroutineScope() - val copyTextLabel = stringResource(R.string.repo_copy_label) - - val (backgroundFocusRequester, buttonFocusRequester) = remember { FocusRequester.createRefs() } - - BasePreferenceWidget( - modifier = modifier - .focusRequester(backgroundFocusRequester) - .focusProperties { - end = buttonFocusRequester - }, - title = repository.info.name, - subcomponent = { - Column( - verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), - modifier = Modifier - .padding(horizontal = PrefsHorizontalPadding) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy( - MaterialTheme.padding.extraSmall - ), - modifier = Modifier.defaultMinSize(minHeight = 14.dp) - ) { - if (derivedState != null) { - Text( - text = stringResource( - R.string.plugins_downloaded, - derivedState.downloadedAndEnabled - ) - /** - Not very good concatenation translation here tbh, - but we do not want to add translation keys - */ - + "/${derivedState.total}", - style = MaterialTheme.typography.labelMedium - ) - if (derivedState.downloadedAndDisabled > 0) { - Text( - text = stringResource( - R.string.plugins_disabled, - derivedState.downloadedAndDisabled - ), - style = MaterialTheme.typography.labelMedium - ) - } - } else if (repository.status == RepositoryStatus.Loading) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.onBackground, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - ) - } else { - Text( - text = stringResource( - R.string.plugin_load_fail, - repository.info.name - ), - style = MaterialTheme.typography.labelMedium - ) - } - } - Text( - text = repository.info.description ?: repository.info.url, - modifier = Modifier.secondaryItemAlpha(), - style = MaterialTheme.typography.bodyMedium, - maxLines = 10, - ) - } - }, - icon = { - RepositoryIcon( - iconUrl = repository.info.iconUrl, - painterResource( - when { - repository.info.url.contains("codeberg") -> R.drawable.codeberg - repository.info.url.contains("pastebin") -> R.drawable.pastebin - else -> R.drawable.ic_github_logo - } - ) - ) - }, - onClick = { - onAction(SelectRepository(repository.uuid)) - }, - onLongClick = { - val shareableRepoData = - "${repository.info.name}$SHAREABLE_REPO_SEPARATOR\n ${repository.info.url}" - - scope.launch { - clipboard.setPlainText( - label = copyTextLabel, - text = shareableRepoData - ) - } - }, - widget = { - when (val reason = repository.status) { - is RepositoryStatus.Failure -> { - var errorDialog by remember { mutableStateOf(false) } - WhiteIconButton( - onClick = { - errorDialog = true - }, - modifier = Modifier - .focusRequester(buttonFocusRequester) - .focusProperties { - start = backgroundFocusRequester - }) - { - Icon( - painter = painterResource(R.drawable.warning_24px), - contentDescription = null - ) - } - if (errorDialog) { - ErrorDialog( - error = reason.cause, - tag = stringResource(R.string.error_invalid_data), - confirmButton = { modifier -> - WhiteButton( - text = stringResource(R.string.reload_error), - modifier = modifier - ) { - onAction(ReloadRepository(repository.uuid)) - errorDialog = false - } - WhiteButton( - text = stringResource(R.string.delete_repository), - modifier = modifier - ) { - deleteDialog = true - } - }, - onDismissRequest = { - errorDialog = false - }) - } - } - - RepositoryStatus.Loading, RepositoryStatus.Success -> { - WhiteIconButton( - onClick = { - deleteDialog = true - }, - modifier = Modifier - .focusRequester(buttonFocusRequester) - .focusProperties { - start = backgroundFocusRequester - } - ) { - Icon( - painter = painterResource(R.drawable.delete_24px), - contentDescription = null - ) - /*CircularProgressIndicator( - modifier = Modifier - .size(IconButtonDefaults.smallContainerSize()) - .padding(MaterialTheme.padding.small), - color = MaterialTheme.colorScheme.onBackground, - )*/ - } - } - } - }, - ) - } - - @Composable - fun DownloadBox( - text: String, - color: Color, - ) { - Box( - modifier = Modifier - .size(10.dp) - .background( - color = color, - shape = RoundedCornerShape(3.dp) - ) - ) - Text( - text = text, - style = MaterialTheme.typography.labelMedium - ) - } - - @Composable - fun remapUrl(url: String, size: Dp): String { - tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { - if (current >= max) return max - if (current >= target) return current - return findClosestBase2(target, current * 2, max) - } - - val pxValue = with(LocalDensity.current) { maxOf(1, size.toPx().roundToInt()) } - return url.replace( - "%size%", - "${findClosestBase2(pxValue, 16, 512)}" - ).replace( - "%exact_size%", - "$pxValue" - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt index 2fe63a59b..482251b78 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt @@ -15,29 +15,13 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIE import com.lagradost.cloudstream3.utils.UiText import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -@Serializable data class RepositoryData( - @JsonProperty("iconUrl") @SerialName("iconUrl") val iconUrl: String?, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("url") @SerialName("url") val url: String, - @JsonProperty("description") @SerialName("description") val description: String? = null, -) { - constructor(name: String, url: String) : this( - iconUrl = null, - name = name, - url = url, - description = null - ) - - constructor(iconUrl: String?, name: String, url: String) : this( - iconUrl = iconUrl, - name = name, - url = url, - description = null - ) + @JsonProperty("iconUrl") val iconUrl: String?, + @JsonProperty("name") val name: String, + @JsonProperty("url") val url: String +){ + constructor(name: String,url: String):this(null,name,url) } const val REPOSITORIES_KEY = "REPOSITORIES_KEY" @@ -68,16 +52,16 @@ class ExtensionsViewModel : ViewModel() { ?: emptyArray()) + PREBUILT_REPOSITORIES val onlinePlugins = urls.toList().amap { - RepositoryManager.getRepoPlugins(it)?.toList() ?: emptyList() - }.flatten().distinctBy { it.plugin.url } + RepositoryManager.getRepoPlugins(it.url)?.toList() ?: emptyList() + }.flatten().distinctBy { it.second.url } // Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated - val outdatedPlugins = getPluginsOnline().flatMap { savedData -> - onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.plugin.internalName } + val outdatedPlugins = getPluginsOnline().map { savedData -> + onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.second.internalName } .map { onlineData -> PluginManager.OnlinePluginData(savedData, onlineData) } - }.distinctBy { it.onlineData.plugin.url } + }.flatten().distinctBy { it.onlineData.second.url } val total = onlinePlugins.count() val disabled = outdatedPlugins.count { it.isDisabled } @@ -106,4 +90,4 @@ class ExtensionsViewModel : ViewModel() { val urls = repos() _repositories.postValue(urls) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index 2ff06cd6a..d0f9ff565 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.plugins.PluginWrapper import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.NoStateAdapter import com.lagradost.cloudstream3.ui.ViewHolderState @@ -35,7 +34,7 @@ import kotlin.math.log10 import kotlin.math.pow data class PluginViewData( - val pluginWrapper: PluginWrapper, + val plugin: Plugin, val isDownloaded: Boolean, ) @@ -45,9 +44,9 @@ class RepositoryViewHolderState(view: ViewBinding) : ViewHolderState(view) } class PluginAdapter( - val showRepositoryNames: Boolean = false, val iconClickCallback: (PluginWrapper) -> Unit, + val iconClickCallback: (Plugin) -> Unit ) : NoStateAdapter(diffCallback = BaseDiffCallback(itemSame = { a, b -> - a.pluginWrapper.plugin.internalName == b.pluginWrapper.plugin.internalName && a.pluginWrapper.repositoryData.url == b.pluginWrapper.repositoryData.url + a.plugin.second.internalName == b.plugin.second.internalName && a.plugin.first == b.plugin.first })) { override fun onCreateContent(parent: ViewGroup): ViewHolderState { val layout = if (isLayout(TV)) R.layout.repository_item_tv else R.layout.repository_item @@ -74,22 +73,14 @@ class PluginAdapter( val binding = holder.view as? RepositoryItemBinding ?: return val itemView = holder.itemView - val metadata = item.pluginWrapper.plugin + val metadata = item.plugin.second val disabled = metadata.status == PROVIDER_STATUS_DOWN val name = metadata.name.removeSuffix("Provider") val alpha = if (disabled) 0.6f else 1f - val isLocal = !item.pluginWrapper.plugin.url.startsWith("http") + val isLocal = !item.plugin.second.url.startsWith("http") binding.mainText.alpha = alpha binding.subText.alpha = alpha - binding.repositoryNameText.isVisible = showRepositoryNames - if (showRepositoryNames) { - val name = item.pluginWrapper.repositoryData.name - binding.repositoryNameText.text = name - } else { - binding.repositoryNameText.text = "" - } - val drawableInt = if (item.isDownloaded) R.drawable.ic_baseline_delete_outline_24 else R.drawable.netflix_download @@ -98,7 +89,7 @@ class PluginAdapter( binding.actionButton.setImageResource(drawableInt) binding.actionButton.setOnClickListener { - iconClickCallback.invoke(item.pluginWrapper) + iconClickCallback.invoke(item.plugin) } itemView.setOnClickListener { if (isLocal) return@setOnClickListener diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt index 06a95d527..0dcbece6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt @@ -54,7 +54,7 @@ class PluginDetailsFragment(val data: PluginViewData) : BaseBottomSheetDialogFra } override fun onBindingCreated(binding: FragmentPluginDetailsBinding) { - val metadata = data.pluginWrapper.plugin + val metadata = data.plugin.second binding.apply { pluginIcon.loadImage(metadata.iconUrl?.replace("%size%", "$iconSize") ?.replace("%exact_size%", "$iconSizeExact")) { @@ -135,7 +135,7 @@ class PluginDetailsFragment(val data: PluginViewData) : BaseBottomSheetDialogFra } private fun updateVoting(value: Int) { - val metadata = data.pluginWrapper.plugin + val metadata = data.plugin.second binding?.apply { pluginVotes.text = value.toString() if (metadata.hasVoted()) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt index a7b24d3ae..534ffa62a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -4,13 +4,13 @@ import android.os.Bundle import android.view.View import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.activityViewModels import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.BuildConfig -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentPluginsBinding import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.BaseFragment import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips import com.lagradost.cloudstream3.ui.result.FOCUS_SELF @@ -23,19 +23,19 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setSyst import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings -import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper.getNameNextToFlagEmoji import com.lagradost.cloudstream3.utils.UIHelper.toPx -const val PLUGINS_BUNDLE_DATA = "data" +const val PLUGINS_BUNDLE_NAME = "name" +const val PLUGINS_BUNDLE_URL = "url" const val PLUGINS_BUNDLE_LOCAL = "isLocal" class PluginsFragment : BaseFragment( BaseFragment.BindingCreator.Inflate(FragmentPluginsBinding::inflate) ) { - private lateinit var pluginViewModel: PluginsViewModel + + private val pluginViewModel: PluginsViewModel by activityViewModels() override fun onDestroyView() { pluginViewModel.clear() // clear for the next observe @@ -47,8 +47,6 @@ class PluginsFragment : BaseFragment( } override fun onBindingCreated(binding: FragmentPluginsBinding) { - pluginViewModel = ViewModelProvider(this)[PluginsViewModel::class.java] - // Since the ViewModel is getting reused the tvTypes must be cleared between uses pluginViewModel.tvTypes.clear() pluginViewModel.selectedLanguages = listOf() @@ -62,25 +60,24 @@ class PluginsFragment : BaseFragment( } } - val repositoryData = arguments?.getString(PLUGINS_BUNDLE_DATA)?.let { data -> - tryParseJson(data) - } + val name = arguments?.getString(PLUGINS_BUNDLE_NAME) + val url = arguments?.getString(PLUGINS_BUNDLE_URL) val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true // download all extensions button val downloadAllButton = binding.settingsToolbar.menu?.findItem(R.id.download_all) - if (repositoryData == null) { + if (url == null || name == null) { dispatchBackPressed() return } setToolBarScrollFlags() - setUpToolbar(repositoryData.name) + setUpToolbar(name) binding.settingsToolbar.apply { setOnMenuItemClickListener { menuItem -> when (menuItem?.itemId) { R.id.download_all -> { - PluginsViewModel.downloadAll(activity, repositoryData, pluginViewModel) + PluginsViewModel.downloadAll(activity, url, pluginViewModel) } R.id.lang_filter -> { @@ -133,6 +130,9 @@ class PluginsFragment : BaseFragment( dispatchBackPressed() } } + searchView?.setOnQueryTextFocusChangeListener { _, hasFocus -> + if (!hasFocus) pluginViewModel.search(null) + } searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { @@ -161,7 +161,7 @@ class PluginsFragment : BaseFragment( setRecycledViewPool(PluginAdapter.sharedPool) adapter = PluginAdapter { - pluginViewModel.handlePluginAction(activity, listOf(repositoryData), it, isLocal) + pluginViewModel.handlePluginAction(activity, url, it, isLocal) } } @@ -185,7 +185,7 @@ class PluginsFragment : BaseFragment( binding.tvtypesChipsScroll.root.isVisible = false } else { - pluginViewModel.updatePluginList(context, listOf(repositoryData)) + pluginViewModel.updatePluginList(context, url) binding.tvtypesChipsScroll.root.isVisible = true // not needed for users but may be useful for devs downloadAllButton?.isVisible = BuildConfig.DEBUG @@ -206,17 +206,21 @@ class PluginsFragment : BaseFragment( } companion object { - fun newInstance(repositoryData: RepositoryData): Bundle { + fun newInstance(name: String, url: String, isLocal: Boolean): Bundle { return Bundle().apply { - putString(PLUGINS_BUNDLE_DATA, repositoryData.toJson()) - putBoolean(PLUGINS_BUNDLE_LOCAL, false) - } - } - fun newLocalInstance(name: String): Bundle { - return Bundle().apply { - putString(PLUGINS_BUNDLE_DATA, RepositoryData("", name, "").toJson()) - putBoolean(PLUGINS_BUNDLE_LOCAL, true) + putString(PLUGINS_BUNDLE_NAME, name) + putString(PLUGINS_BUNDLE_URL, url) + putBoolean(PLUGINS_BUNDLE_LOCAL, isLocal) } } + +// class RepoSearchView(context: Context) : android.widget.SearchView(context) { +// var onActionViewCollapsed = {} +// +// override fun onActionViewCollapsed() { +// onActionViewCollapsed() +// } +// } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt index 09c202d43..dfc61eba5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt @@ -17,15 +17,17 @@ import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager.getPluginPath -import com.lagradost.cloudstream3.plugins.PluginWrapper import com.lagradost.cloudstream3.plugins.RepositoryManager +import com.lagradost.cloudstream3.plugins.SitePlugin import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread -import com.lagradost.cloudstream3.utils.Levenshtein +import me.xdrop.fuzzywuzzy.FuzzySearch import java.io.File +// String => repository url +typealias Plugin = Pair /** * The boolean signifies if the plugin list should be scrolled to the top, used for searching. * */ @@ -38,7 +40,7 @@ class PluginsViewModel : ViewModel() { set(value) { // Also set all the plugin languages for easier filtering value.map { pluginViewData -> - val language = pluginViewData.pluginWrapper.plugin.language?.lowercase() + val language = pluginViewData.plugin.second.language?.lowercase() pluginLanguages.add( when { language.isNullOrBlank() -> "none" @@ -60,7 +62,7 @@ class PluginsViewModel : ViewModel() { private var currentQuery: String? = null companion object { - private val repositoryCache: MutableMap> = mutableMapOf() + private val repositoryCache: MutableMap> = mutableMapOf() const val TAG = "PLG" private fun isDownloaded( @@ -72,33 +74,32 @@ class PluginsViewModel : ViewModel() { } private suspend fun getPlugins( - repository: RepositoryData, + repositoryUrl: String, canUseCache: Boolean = true - ): List { - Log.i(TAG, "getPlugins = $repository") - if (canUseCache && repositoryCache.containsKey(repository.url)) { - repositoryCache[repository.url]?.let { + ): List { + Log.i(TAG, "getPlugins = $repositoryUrl") + if (canUseCache && repositoryCache.containsKey(repositoryUrl)) { + repositoryCache[repositoryUrl]?.let { return it } } - - return RepositoryManager.getRepoPlugins(repository) - ?.also { repositoryCache[repository.url] = it } ?: emptyList() + return RepositoryManager.getRepoPlugins(repositoryUrl) + ?.also { repositoryCache[repositoryUrl] = it } ?: emptyList() } /** * @param viewModel optional, updates the plugins livedata for that viewModel if included * */ - fun downloadAll(activity: Activity?, repository: RepositoryData, viewModel: PluginsViewModel?) = + fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) = ioSafe { if (activity == null) return@ioSafe - val plugins = getPlugins(repository) + val plugins = getPlugins(repositoryUrl) - plugins.filter { pluginWrapper -> + plugins.filter { plugin -> !isDownloaded( activity, - pluginWrapper.plugin.internalName, - repository.url + plugin.second.internalName, + repositoryUrl ) }.also { list -> main { @@ -123,13 +124,13 @@ class PluginsViewModel : ViewModel() { Toast.LENGTH_SHORT ) } - }.amap { (_, repo, metadata) -> + }.amap { (repo, metadata) -> PluginManager.downloadPlugin( activity, metadata.url, metadata.fileHash, metadata.internalName, - repo.url, + repo, metadata.status != PROVIDER_STATUS_DOWN ) }.main { list -> @@ -142,7 +143,7 @@ class PluginsViewModel : ViewModel() { ), Toast.LENGTH_SHORT ) - viewModel?.updatePluginListPrivate(activity, listOf(repository)) + viewModel?.updatePluginListPrivate(activity, repositoryUrl) } else if (list.isNotEmpty()) { showToast(R.string.download_failed, Toast.LENGTH_SHORT) } @@ -156,32 +157,32 @@ class PluginsViewModel : ViewModel() { * */ fun handlePluginAction( activity: Activity?, - repositoryUrls: List, - pluginWrapper: PluginWrapper, + repositoryUrl: String, + plugin: Plugin, isLocal: Boolean ) = ioSafe { - Log.i(TAG, "handlePluginAction = ${repositoryUrls}, $pluginWrapper, $isLocal") + Log.i(TAG, "handlePluginAction = $repositoryUrl, $plugin, $isLocal") if (activity == null) return@ioSafe - val (_, repositoryData, metadata) = pluginWrapper + val (repo, metadata) = plugin - val file = if (isLocal) File(pluginWrapper.plugin.url) else getPluginPath( + val file = if (isLocal) File(plugin.second.url) else getPluginPath( activity, - pluginWrapper.plugin.internalName, - pluginWrapper.repositoryData.url + plugin.second.internalName, + plugin.first ) val (success, message) = if (file.exists()) { PluginManager.deletePlugin(file) to R.string.plugin_deleted } else { - val isEnabled = pluginWrapper.plugin.status != PROVIDER_STATUS_DOWN + val isEnabled = plugin.second.status != PROVIDER_STATUS_DOWN val message = if (isEnabled) R.string.plugin_loaded else R.string.plugin_downloaded PluginManager.downloadPlugin( activity, metadata.url, metadata.fileHash, metadata.internalName, - repositoryData.url, + repo, isEnabled ) to message } @@ -197,23 +198,20 @@ class PluginsViewModel : ViewModel() { if (isLocal) updatePluginListLocal() else - updatePluginListPrivate(activity, repositoryUrls) + updatePluginListPrivate(activity, repositoryUrl) } - private suspend fun updatePluginListPrivate(context: Context, repositories: List) { + private suspend fun updatePluginListPrivate(context: Context, repositoryUrl: String) { val isAdult = PreferenceManager.getDefaultSharedPreferences(context) .getStringSet(context.getString(R.string.prefer_media_type_key), emptySet()) ?.contains(TvType.NSFW.ordinal.toString()) == true - val plugins = repositories.flatMap { repositoryUrl -> - getPlugins(repositoryUrl) - } - + val plugins = getPlugins(repositoryUrl) val list = plugins.filter { // Show all non-nsfw plugins or all if nsfw is enabled - it.plugin.tvTypes?.contains(TvType.NSFW.name) != true || isAdult + it.second.tvTypes?.contains(TvType.NSFW.name) != true || isAdult }.map { plugin -> - PluginViewData(plugin, isDownloaded(context, plugin.plugin.internalName, plugin.repositoryData.url)) + PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first)) } this.plugins = list @@ -226,8 +224,8 @@ class PluginsViewModel : ViewModel() { private fun List.filterTvTypes(): List { if (tvTypes.isEmpty()) return this return this.filter { - (it.pluginWrapper.plugin.tvTypes?.any { type -> tvTypes.contains(type) } == true) || - (tvTypes.contains(TvType.Others.name) && (it.pluginWrapper.plugin.tvTypes + (it.plugin.second.tvTypes?.any { type -> tvTypes.contains(type) } == true) || + (tvTypes.contains(TvType.Others.name) && (it.plugin.second.tvTypes ?: emptyList()).isEmpty()) } } @@ -235,36 +233,24 @@ class PluginsViewModel : ViewModel() { private fun List.filterLang(): List { if (selectedLanguages.isEmpty()) return this // do not filter return this.filter { - if (it.pluginWrapper.plugin.language == null) { + if (it.plugin.second.language == null) { return@filter selectedLanguages.contains("none") } - selectedLanguages.contains(it.pluginWrapper.plugin.language.lowercase()) + selectedLanguages.contains(it.plugin.second.language?.lowercase()) } } private fun List.sortByQuery(query: String?): List { - return if (query.isNullOrBlank()) { + return if (query == null) { // Return list to base state if no query - this.sortedBy { it.pluginWrapper.plugin.name } + this.sortedBy { it.plugin.second.name } } else { - this.mapNotNull { - // Try matching name - val score = Levenshtein.partialRatio( - it.pluginWrapper.plugin.name.lowercase(), + this.sortedBy { + -FuzzySearch.partialRatio( + it.plugin.second.name.lowercase(), query.lowercase() - ).takeIf { score -> score > 80 } ?: - // Fallback to description, but limit characters to reduce lag - it.pluginWrapper.plugin.description?.lowercase()?.take(64) - ?.let { description -> - Levenshtein.partialRatio( - description, - query.lowercase() - ) - }?.takeIf { score -> score > 80 } ?: return@mapNotNull null - it to score - }.sortedBy { - -it.second - }.map { it.first } + ) + } } } @@ -281,17 +267,16 @@ class PluginsViewModel : ViewModel() { ) } - fun updatePluginList(context: Context?, repositories: List) = - viewModelScope.launchSafe { - if (context == null) return@launchSafe - Log.i(TAG, "updatePluginList = $repositories") - updatePluginListPrivate(context, repositories) - } + fun updatePluginList(context: Context?, repositoryUrl: String) = viewModelScope.launchSafe { + if (context == null) return@launchSafe + Log.i(TAG, "updatePluginList = $repositoryUrl") + updatePluginListPrivate(context, repositoryUrl) + } fun search(query: String?) { currentQuery = query _filteredPlugins.postValue( - true to plugins.filterTvTypes().filterLang().sortByQuery(query) + true to (filteredPlugins.value?.second?.sortByQuery(query) ?: emptyList()) ) } @@ -304,7 +289,7 @@ class PluginsViewModel : ViewModel() { val downloadedPlugins = (PluginManager.getPluginsOnline() + PluginManager.getPluginsLocal()) .distinctBy { it.filePath } .map { - PluginViewData(PluginWrapper.getLocalPluginWrapper(it.toSitePlugin()), true) + PluginViewData("" to it.toSitePlugin(), true) } plugins = downloadedPlugins diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/logcat/LogcatDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/logcat/LogcatDialog.kt deleted file mode 100644 index 649d2f009..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/logcat/LogcatDialog.kt +++ /dev/null @@ -1,181 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings.logcat - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1 -import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2 -import androidx.compose.ui.focus.focusProperties -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.window.DialogProperties -import com.lagradost.cloudstream3.CommonActivity.showToast -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper -import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager -import com.lagradost.cloudstream3.utils.txt -import com.lagradost.cloudstream4.compose.BlackButton -import com.lagradost.cloudstream4.compose.LogBoxBlack -import com.lagradost.cloudstream4.compose.LogBoxWhite -import com.lagradost.cloudstream4.compose.LogText -import com.lagradost.cloudstream4.compose.WhiteButton -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.OutputStream -import java.lang.System.currentTimeMillis -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - - -@Composable -fun LogcatDialog(dismiss: () -> Unit) { - val list = remember { mutableStateOf(persistentListOf()) } - var isLoading by remember { mutableStateOf(true) } - LaunchedEffect(dismiss) { - try { - isLoading = true - - // https://developer.android.com/studio/command-line/logcat - val process = Runtime.getRuntime().exec("logcat --binary -d") - val items = arrayListOf() - LogcatBinaryParser(process.inputStream).use { parser -> - while (true) { - val item = parser.parseItem() ?: break - items.add(item) - } - } - - list.value = items.toPersistentList() - } catch (e: Exception) { - logError(e) // kinda ironic - } finally { - isLoading = false - } - } - val (dismissFocus, confirmFocus) = remember { FocusRequester.createRefs() } - - val context = LocalContext.current - val scope = rememberCoroutineScope() - AlertDialog( - containerColor = MaterialTheme.colorScheme.background, - onDismissRequest = dismiss, - title = { - Text(text = stringResource(R.string.log_cat)) - }, - text = { - if (isLoading) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.onBackground, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - ) - } - LazyColumn( - modifier = Modifier.focusProperties { - start = dismissFocus - end = confirmFocus - } - ) { - items(items = list.value, key = { item -> item.uuid }) { item -> - LogcatItem(item, modifier = Modifier.focusProperties { - start = dismissFocus - end = confirmFocus - }) - } - } - }, - confirmButton = { - WhiteButton( - text = stringResource(R.string.sort_save), - modifier = Modifier.focusRequester(confirmFocus) - ) { - scope.launch { - withContext(Dispatchers.IO) { - val date = SimpleDateFormat("yyyy_MM_dd_HH_mm", Locale.getDefault()).format( - Date(currentTimeMillis()) - ) - var fileStream: OutputStream? - try { - fileStream = VideoDownloadManager.setupStream( - context, - "logcat_${date}", - null, - "txt", - false - ).openNew() - fileStream.bufferedWriter() - .use { writer -> - list.value.forEach { - writer.write(it.toString()) - writer.write("\n\n") - } - } - dismiss() - } catch (t: Throwable) { - logError(t) - showToast(t.message) - } - } - } - } - - WhiteButton(text = stringResource(R.string.sort_copy)) { - clipboardHelper( - txt("Logcat"), - list.value.joinToString(separator = "\n\n") { it.toString() } - ) - } - WhiteButton(text = stringResource(R.string.sort_clear)) { - try { - Runtime.getRuntime().exec("logcat -c") - } catch (t: Throwable) { - logError(t) - } - dismiss() - } - }, - dismissButton = { - BlackButton( - text = stringResource(R.string.sort_close), - onClick = dismiss, - modifier = Modifier.focusRequester(dismissFocus) - ) - }, - properties = DialogProperties(usePlatformDefaultWidth = false) - ) -} - -@Composable -fun LogcatItem(item: LogcatItem, modifier: Modifier = Modifier) { - Row(modifier = Modifier.fillMaxWidth()) { - item.level?.identifier?.let { value -> - LogBoxWhite(value) - } - LogBoxBlack(item.date.toHumanReadable()) - LogBoxBlack(item.tag) - } - - LogText(level = item.level, message = item.message, modifier = modifier, onClick = { - clipboardHelper(txt("Logcat"), item.toString()) - }) -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/logcat/LogcatParser.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/logcat/LogcatParser.kt deleted file mode 100644 index 4f2cfba40..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/logcat/LogcatParser.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings.logcat - -import androidx.compose.runtime.Immutable -import com.lagradost.cloudstream4.state.LogLevel -import com.lagradost.cloudstream4.state.UniqueItem -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.io.Buffer -import kotlinx.io.asSource -import kotlinx.io.readByteArray -import kotlinx.io.readIntLe -import kotlinx.io.readString -import kotlinx.io.readUShortLe -import java.io.Closeable -import java.io.InputStream -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.TimeZone -import kotlin.time.Instant -import kotlin.uuid.Uuid - -fun Instant.toHumanReadable(): String { - val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).apply { - timeZone = TimeZone.getDefault() - } - return formatter.format(Date(this.toEpochMilliseconds())) -} - -@Immutable -data class LogcatItem( - val date: Instant, - val pid: Int, - val tid: Int, - val level: LogLevel?, - val tag: String, - val message: String, - override val uuid : Uuid = Uuid.random(), -) : UniqueItem { - override fun toString(): String { - return "${date.toHumanReadable()} $pid-$tid $tag ${level?.identifier ?: "?"} $message" - } -} - -/**https://github.com/brudaswen/android-logcat/blob/main/library/logcat-core/src/main/kotlin/de/brudaswen/android/logcat/core/parser/LogcatBinaryParser.kt */ -class LogcatBinaryParser( - private val input: InputStream, -) : Closeable by input { - val source = input.asSource() - private val buffer = Buffer() - - /** - * Parse one [LogcatItem] from the current [input] stream. - * - * @return The parsed [LogcatItem] or `null` if stream reached EOF. - */ - suspend fun parseItem(): LogcatItem? = withContext(Dispatchers.IO) { - val firstByte = input.read() - if (firstByte == -1) return@withContext null - - // Read v1 header - buffer.writeByte(firstByte.toByte()) - buffer.write(source = source, byteCount = 19) - - val len = buffer.readUShortLe().toInt() - val headerSize = buffer.readUShortLe().toInt() - val pid = buffer.readIntLe() - val tid = buffer.readIntLe() - val sec = buffer.readIntLe() - val nsec = buffer.readIntLe() - - // Read additional header fields - buffer.write(source = source, byteCount = headerSize - 20L) - - val additionalHeaderBytes = (headerSize - 20).coerceAtLeast(0) - buffer.readByteArray(byteCount = additionalHeaderBytes) - - // Read payload - buffer.write(source = source, byteCount = len.toLong()) - - val priority = buffer.readByte() - - val payload = buffer.readString() - val texts = payload.split('\u0000', limit = 2) - val tag = texts.getOrNull(0).orEmpty() - val message = texts.getOrNull(1).orEmpty().removeSuffix("\u0000").trim() - - // Clear buffer - buffer.clear() - - // Convert raw values to item - LogcatItem( - sec = sec, - nsec = nsec, - priority = priority, - pid = pid, - tid = tid, - tag = tag, - message = message, - ) - } - - private fun LogcatItem( - sec: Int, - nsec: Int, - priority: Byte, - pid: Int, - tid: Int, - tag: String, - message: String, - ): LogcatItem { - val date = Instant.fromEpochSeconds( - epochSeconds = sec.toLong(), - nanosecondAdjustment = nsec, - ) - - val level = when (priority) { - 2.toByte() -> LogLevel.Verbose - 3.toByte() -> LogLevel.Debug - 4.toByte() -> LogLevel.Info - 5.toByte() -> LogLevel.Warning - 6.toByte() -> LogLevel.Error - 7.toByte() -> LogLevel.Fatal - else -> null - } - - return LogcatItem( - date = date, - pid = pid, - tid = tid, - level = level, - tag = tag, - message = message, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment2.kt deleted file mode 100644 index d1f1d1a96..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment2.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings.testing - -import com.lagradost.cloudstream3.utils.BaseComposeFragment -import com.lagradost.cloudstream4.compose.Screen - -/** Empty glue code to connect the old navigation graph to Compose */ -class TestFragment2 : BaseComposeFragment(), Screen by TestScreen diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt index 109268b07..c53ff1fcf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt @@ -21,12 +21,10 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.TestingUtils -import com.lagradost.cloudstream3.utils.TestingUtils.Logger -import com.lagradost.cloudstream4.state.LogLevel import java.io.File class TestResultAdapter() : - NoStateAdapter>( + NoStateAdapter>( diffCallback = BaseDiffCallback( itemSame = { a, b -> a.first.name == b.first.name && a.first.mainUrl == b.first.mainUrl @@ -58,11 +56,11 @@ class TestResultAdapter() : override fun onBindContent( holder: ViewHolderState, - item: Triple, + item: Pair, position: Int ) { val binding = holder.view as? ProviderTestItemBinding ?: return - val (api, result, log) = item + val (api, result) = item val itemView = holder.itemView @@ -76,7 +74,7 @@ class TestResultAdapter() : providerTitle.text = api.name val (resultText, resultColor) = if (result.success) { - if (log.messageLog.any { it.level == LogLevel.Warning }) { + if (result.log.any { it.level == TestingUtils.Logger.LogLevel.Warning }) { R.string.test_warning to R.color.colorTestWarning } else { R.string.test_passed to R.color.colorTestPass @@ -90,7 +88,7 @@ class TestResultAdapter() : val stackTrace = result.exception?.getStackTracePretty(false)?.ifBlank { null } val messages = result.exception?.getAllMessages()?.ifBlank { null } - val resultLog = log.messageLog.joinToString("\n") + val resultLog = result.log.joinToString("\n") val fullLog = resultLog + (messages?.let { "\n\nError: $it" } ?: "") + diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestScreen.kt deleted file mode 100644 index 21842aee0..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestScreen.kt +++ /dev/null @@ -1,470 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings.testing - -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.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -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.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.input.rememberTextFieldState -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1 -import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2 -import androidx.compose.ui.focus.focusProperties -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.PreviewLightDark -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.viewmodel.compose.viewModel -import com.lagradost.cloudstream3.APIHolder.allProviders -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.ui.settings.SettingsFragmentScreen.SettingsSearch -import com.lagradost.cloudstream3.ui.settings.logcat.toHumanReadable -import com.lagradost.cloudstream3.utils.TestingUtils -import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper -import com.lagradost.cloudstream3.utils.txt -import com.lagradost.cloudstream4.compose.BlackButton -import com.lagradost.cloudstream4.compose.LogBoxBlack -import com.lagradost.cloudstream4.compose.LogBoxWhite -import com.lagradost.cloudstream4.compose.LogText -import com.lagradost.cloudstream4.compose.PHONE -import com.lagradost.cloudstream4.compose.Screen -import com.lagradost.cloudstream4.compose.WhiteButton -import com.lagradost.cloudstream4.compose.WhiteFilterChip -import com.lagradost.cloudstream4.compose.isLayout -import com.lagradost.cloudstream4.state.LogItem -import com.lagradost.cloudstream4.state.LogLevel -import com.lagradost.cloudstream4.state.SearchableDataState -import com.lagradost.cloudstream4.state.SortByName -import com.lagradost.cloudstream4.theme.CloudStreamPreviewTheme -import com.mihon.material.AppBar -import com.mihon.material.Scaffold -import com.mihon.material.padding -import com.mihon.presentation.LocalBackPress -import kotlinx.collections.immutable.PersistentMap -import kotlinx.collections.immutable.PersistentSet -import kotlinx.collections.immutable.persistentMapOf -import kotlinx.collections.immutable.toPersistentList -import kotlin.uuid.Uuid - - -@PreviewLightDark -@Composable -fun Preview() { - val uuid1 = Uuid.random() - val uuid2 = Uuid.random() - CloudStreamPreviewTheme { - TestScreen.Content( - state = TestState( - items = SearchableDataState.from( - state = DerivedTestState(), - data = persistentMapOf( - uuid1 to ImmutableTestResult( - name = "test", - language = "en", - uuid = uuid1, - plugin = "test plugin" - ), - uuid2 to ImmutableTestResult( - name = "aaa", - language = "se", - uuid = uuid2, - plugin = "test plugin", - highestLogLevel = LogLevel.Error, - result = TestingUtils.TestResultProvider(false, Throwable()) - ) - ), - sortedBy = SortByName { it.name }, - filteredBy = null, - ) - ), onAction = {}) - } -} - - -object TestScreen : Screen { - @Composable - override fun Content() { - val viewModel = viewModel { TestViewModel2() } - - LaunchedEffect(Unit) { - viewModel.onAction(TestAction.Init(allProviders.withLock { allProviders.toPersistentList() })) - } - - val state by viewModel.state.collectAsState() - Content(state, viewModel::onAction) - } - - @Composable - fun Content( - state: TestState, - onAction: (TestAction) -> Unit, - ) { - val (top, bottom) = remember { FocusRequester.createRefs() } - val handleBack = LocalBackPress.current - Scaffold( - // TODO @WindowInsets remove this when we have converted everything to compose - contentWindowInsets = WindowInsets(0, 0, 0, 0), - topBar = { - AppBar( - modifier = Modifier - .focusRequester(top) - .focusProperties { - down = bottom - }, - title = stringResource(R.string.category_provider_test), - navigateUp = handleBack, - actions = { }, - scrollBehavior = it, - ) - }, - floatingActionButton = { - if (isLayout(PHONE)) { - val loading = state.activeQueries > 0 - FloatingActionButton( - containerColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onBackground, - onClick = { - if (loading) { - onAction(TestAction.Cancel) - } else { - onAction(TestAction.Start) - } - }) { - if (loading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onBackground, - ) - } else { - Icon( - painter = painterResource(R.drawable.play_arrow_24px), - contentDescription = stringResource(R.string.test_extensions) - ) - } - } - } - }, - content = { contentPadding -> - TestContent( - state = state, - onAction = onAction, - contentPadding = contentPadding, - modifier = Modifier - .focusRequester(bottom) - .focusProperties { - up = top - }) - }, - ) - } - - @Composable - fun SelectSingleButton( - name: Int, - filtering: PersistentSet, - level: LogLevel, - highestLogLevelCount: PersistentMap, - onAction: (TestAction) -> Unit, - ) { - val selected = filtering.contains(level) - WhiteFilterChip( - selected = selected, - label = "${stringResource(name)} ${highestLogLevelCount[level] ?: 0}" - ) { - if (selected) { - onAction(TestAction.SetLevel(filtering.removing(level))) - } else { - onAction(TestAction.SetLevel(filtering.adding(level))) - } - } - } - - @Composable - fun TestContent( - state: TestState, - onAction: (TestAction) -> Unit, - contentPadding: PaddingValues, - modifier: Modifier = Modifier, - ) { - LazyColumn( - modifier = modifier, - contentPadding = contentPadding, - ) { - item(key = "header") { - ProviderHeader(onAction = onAction) - } - - item(key = "filter") { - FilterRow(state = state, onAction = onAction) - } - - items( - items = state.items.sorted, - key = { api -> api } - ) { uuid -> - val result = state.items.data[uuid]!! - - TestResult( - result = result, - onAction = onAction, - modifier = Modifier.animateItem(), - ) - } - } - } - - /** This is not a .toString() because toHumanReadable is jvm */ - fun LogItem.format() = "${date.toHumanReadable()} $tag ${level.identifier} $message" - - @Composable - fun LogItem(item: LogItem, modifier: Modifier = Modifier) { - Row(modifier = Modifier.fillMaxWidth()) { - LogBoxWhite(item.level.identifier) - LogBoxBlack(item.date.toHumanReadable()) - LogBoxBlack(item.tag) - } - - LogText(level = item.level, message = item.message, modifier = modifier, onClick = { - clipboardHelper(txt("Log"), item.format()) - }) - } - - @Composable - fun FilterRow(state: TestState, onAction: (TestAction) -> Unit) { - Row( - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(state = rememberScrollState()) - ) { - Spacer(modifier = Modifier.width(MaterialTheme.padding.small)) - val highestLogLevelCount = state.items.state.highestLogLevelCount - SelectSingleButton( - name = R.string.no_data, - filtering = state.filtering.levelFilter, - level = LogLevel.Verbose, - highestLogLevelCount = highestLogLevelCount, - onAction = onAction, - ) - SelectSingleButton( - name = R.string.test_passed, - filtering = state.filtering.levelFilter, - level = LogLevel.Info, - highestLogLevelCount = highestLogLevelCount, - onAction = onAction, - ) - SelectSingleButton( - name = R.string.test_warning, - filtering = state.filtering.levelFilter, - level = LogLevel.Warning, - highestLogLevelCount = highestLogLevelCount, - onAction = onAction, - ) - SelectSingleButton( - name = R.string.test_failed, - filtering = state.filtering.levelFilter, - level = LogLevel.Error, - highestLogLevelCount = highestLogLevelCount, - onAction = onAction, - ) - Spacer(modifier = Modifier.width(MaterialTheme.padding.small)) - } - } - - @Composable - fun TestResultDialog( - result: ImmutableTestResult, - onAction: (TestAction) -> Unit, - dismiss: () -> Unit, - ) { - val (dismissFocus, confirmFocus) = remember { FocusRequester.createRefs() } - AlertDialog( - containerColor = MaterialTheme.colorScheme.background, - onDismissRequest = dismiss, - title = { - Text(text = result.name) - }, - text = { - LazyColumn( - modifier = Modifier.focusProperties { - start = dismissFocus - end = confirmFocus - } - ) { - item(key = "loading header") { - if (result.isLoading) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.onBackground, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - ) - Spacer(Modifier.height(height = MaterialTheme.padding.small)) - } else if (result.log.isEmpty()) { - Text( - text = stringResource(R.string.no_data), - style = MaterialTheme.typography.titleMedium, - textAlign = TextAlign.Center, - ) - } - } - items(items = result.log, key = { item -> item.uuid }) { item -> - LogItem(item, modifier = Modifier.focusProperties { - start = dismissFocus - end = confirmFocus - }) - } - } - }, - confirmButton = { - WhiteButton( - text = if (result.isLoading) stringResource(R.string.stop) else stringResource(R.string.start), - modifier = Modifier.focusRequester(confirmFocus) - ) { - if (result.isLoading) { - onAction(TestAction.CancelSingle(result.uuid)) - } else { - onAction(TestAction.StartSingle(result.uuid)) - } - } - - WhiteButton(text = stringResource(R.string.sort_copy)) { - clipboardHelper( - txt("Log"), - result.log.joinToString(separator = "\n\n") { it.format() } - ) - } - WhiteButton(text = stringResource(R.string.sort_clear)) { - onAction(TestAction.ClearSingle(result.uuid)) - } - }, - dismissButton = { - BlackButton( - text = stringResource(R.string.sort_close), - onClick = dismiss, - modifier = Modifier.focusRequester(dismissFocus) - ) - }, - properties = DialogProperties(usePlatformDefaultWidth = false) - ) - } - - @Composable - fun TestResult( - result: ImmutableTestResult, - onAction: (TestAction) -> Unit, - modifier: Modifier = Modifier, - ) { - var dialogShown by remember { mutableStateOf(false) } - - if (dialogShown) { - TestResultDialog(result = result, onAction = onAction, dismiss = { - dialogShown = false - }) - } - - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .clickable(onClick = { - dialogShown = !dialogShown - }) - .padding(horizontal = MaterialTheme.padding.medium) - ) { - Column(modifier = Modifier.weight(1.0f)) { - Text(text = result.name) - - Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) { - if (result.result != null) { - val (text, color) = when (result.highestLogLevel) { - LogLevel.Fatal, LogLevel.Error -> R.string.test_failed to R.color.colorTestFail - LogLevel.Warning -> R.string.test_warning to R.color.colorTestWarning - LogLevel.Verbose, LogLevel.Debug, LogLevel.Info -> R.string.test_passed to R.color.colorTestPass - } - Text(text = stringResource(text), color = colorResource(color)) - } - if (result.plugin != null) { - Text( - text = result.plugin, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - - Spacer(modifier = Modifier.width(MaterialTheme.padding.small)) - - IconButton( - onClick = { - if (result.isLoading) { - onAction(TestAction.CancelSingle(result.uuid)) - } else { - onAction(TestAction.StartSingle(result.uuid)) - } - }, - colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.onBackground) - ) { - if (result.isLoading) { - CircularProgressIndicator( - modifier = Modifier.padding(MaterialTheme.padding.small), - color = MaterialTheme.colorScheme.onBackground, - ) - } else { - Icon( - painter = painterResource(R.drawable.play_arrow_24px), - contentDescription = null, - ) - } - } - } - } - - @Composable - fun ProviderHeader( - onAction: (TestAction) -> Unit, - ) { - val textFieldState = rememberTextFieldState() - - Box(modifier = Modifier.padding(vertical = MaterialTheme.padding.small)) { - SettingsSearch(textFieldState) - } - - LaunchedEffect(textFieldState.text) { - onAction(TestAction.Search(textFieldState.text.toString())) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt index 55ad51187..818f1fd79 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.MainAPI -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.TestingUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -28,11 +28,11 @@ class TestViewModel : ViewModel() { val providerProgress: LiveData = _providerProgress private val _providerResults = - MutableLiveData>>( + MutableLiveData>>( emptyList() ) - val providerResults: LiveData>> = + val providerResults: LiveData>> = _providerResults private var scope: CoroutineScope? = null @@ -40,8 +40,7 @@ class TestViewModel : ViewModel() { get() = scope != null private var filter = ProviderFilter.All - private val providers = - atomicListOf>() + private val providers = threadSafeListOf>() private var passed = 0 private var failed = 0 private var total = 0 @@ -52,9 +51,9 @@ class TestViewModel : ViewModel() { } private fun postProviders() { - providers.withLock { + synchronized(providers) { val filtered = when (filter) { - ProviderFilter.All -> providers.toList() + ProviderFilter.All -> providers ProviderFilter.Passed -> providers.filter { it.second.success } ProviderFilter.Failed -> providers.filter { !it.second.success } } @@ -68,41 +67,36 @@ class TestViewModel : ViewModel() { postProviders() } - private fun addProvider( - api: MainAPI, - results: TestingUtils.TestResultProvider, - logger: TestingUtils.Logger - ) { - providers.withLock { + private fun addProvider(api: MainAPI, results: TestingUtils.TestResultProvider) { + synchronized(providers) { val index = providers.indexOfFirst { it.first == api } - val triple = Triple(api, results, logger) if (index == -1) { - providers.add(triple) + providers.add(api to results) if (results.success) passed++ else failed++ } else { - providers[index] = triple + providers[index] = api to results } updateProgress() } } fun init() { - total = APIHolder.allProviders.withLock { APIHolder.allProviders.size } + total = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size } updateProgress() } fun startTest() { scope = CoroutineScope(Dispatchers.Default) - val apis = APIHolder.allProviders.withLock { APIHolder.allProviders.toTypedArray() } + val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() } total = apis.size failed = 0 passed = 0 providers.clear() updateProgress() - TestingUtils.getDeferredProviderTests(scope ?: return, apis) { api, result, log -> - addProvider(api, result, log) + TestingUtils.getDeferredProviderTests(scope ?: return, apis) { api, result -> + addProvider(api, result) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel2.kt deleted file mode 100644 index 1050f72a2..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel2.kt +++ /dev/null @@ -1,291 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings.testing - -import androidx.compose.runtime.Immutable -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.lagradost.cloudstream3.MainAPI -import com.lagradost.cloudstream3.utils.TestingUtils.TestResultProvider -import com.lagradost.cloudstream3.utils.TestingUtils.runSingleProviderTest -import com.lagradost.cloudstream4.state.ActionHandler -import com.lagradost.cloudstream4.state.DefaultStateContainer -import com.lagradost.cloudstream4.state.DerivedState -import com.lagradost.cloudstream4.state.FilterByQuery -import com.lagradost.cloudstream4.state.Log -import com.lagradost.cloudstream4.state.LogItem -import com.lagradost.cloudstream4.state.LogLevel -import com.lagradost.cloudstream4.state.MultiActiveQuery -import com.lagradost.cloudstream4.state.SearchableDataState -import com.lagradost.cloudstream4.state.SortByName -import com.lagradost.cloudstream4.state.StateContainer -import com.lagradost.cloudstream4.state.UniqueItem -import com.lagradost.cloudstream4.state.edit -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.PersistentMap -import kotlinx.collections.immutable.PersistentSet -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.persistentMapOf -import kotlinx.collections.immutable.toPersistentMap -import kotlinx.collections.immutable.toPersistentSet -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlin.time.Duration.Companion.milliseconds -import kotlin.uuid.Uuid - -@Immutable -data class ImmutableTestResult( - val name: String, - val language: String, - val plugin: String?, - override val uuid: Uuid, - val log: PersistentList = persistentListOf(), - val highestLogLevel: LogLevel = LogLevel.Verbose, - val isLoading: Boolean = false, - val result: TestResultProvider? = null, -) : UniqueItem { - fun addMessage(message: LogItem): ImmutableTestResult { - val lastMessage = log.lastOrNull() - - /** If we have a very similar message at almost the same time, - * then append it instead of adding a new message*/ - if (lastMessage != null - && lastMessage.level == message.level - && lastMessage.tag == message.tag - && (message.date - lastMessage.date) < 100.milliseconds - ) { - return copy( - log = log.replacingAt(log.lastIndex, lastMessage.copy( - message = lastMessage.message + "\n" + message.message, - date = message.date - )) - ) - } - - return copy(log = log.adding(message)) - } -} - -@Immutable -data class FilterTestResults( - val searchFilter: FilterByQuery = FilterByQuery(query = "") { it.name }, - val levelFilter: PersistentSet = LogLevel.entries.toPersistentSet(), -) : Function1 { - override fun invoke(result: ImmutableTestResult): Boolean = - levelFilter.contains(result.highestLogLevel) && searchFilter(result) - - fun setQuery(newQuery: String): FilterTestResults { - return copy(searchFilter = searchFilter.update(newQuery)) - } - - fun setLevel(newLevel: PersistentSet): FilterTestResults { - return copy(levelFilter = newLevel) - } -} - -@Immutable -data class DerivedTestState( - val highestLogLevelCount: PersistentMap = zero -) : DerivedState { - companion object { - val zero = LogLevel.entries.associateWith { 0 }.toPersistentMap() - } - - override fun clearing(): DerivedTestState = DerivedTestState() - override fun adding(value: ImmutableTestResult): DerivedTestState = - copy(highestLogLevelCount = highestLogLevelCount.edit(value.highestLogLevel) { plus(1) }) - - override fun removing(value: ImmutableTestResult): DerivedTestState = - copy(highestLogLevelCount = highestLogLevelCount.edit(value.highestLogLevel) { minus(1) }) -} - - -/** - * To avoid issues with the uniqueness of a MainAPI, we use an Uuid to refer to each MainAPI - * */ -@Immutable -data class TestState( - val sorting: SortByName = SortByName { it.name }, - val filtering: FilterTestResults = FilterTestResults(), - val items: SearchableDataState = SearchableDataState.from( - state = DerivedTestState(), - data = persistentMapOf(), - sortedBy = sorting, - filteredBy = filtering - ), - val activeQueries: Int = 0, - val internalApis: PersistentMap = persistentMapOf(), -) { - val total: Int = internalApis.size -} - - -@Immutable -sealed class TestAction { - object Start : TestAction() - object Cancel : TestAction() - data class Init(val apis: List) : TestAction() - data class StartSingle(val uuid: Uuid) : TestAction() - data class ClearSingle(val uuid: Uuid) : TestAction() - data class CancelSingle(val uuid: Uuid) : TestAction() - data class Search(val query: String) : TestAction() - data class SetLevel(val level: PersistentSet) : TestAction() -} - -class TestViewModel2 : ViewModel(), StateContainer by DefaultStateContainer(TestState()), - ActionHandler { - private val dispatcher = MultiActiveQuery(Dispatchers.IO) - - override fun onAction(action: TestAction) { - when (action) { - TestAction.Cancel -> { - viewModelScope.launch { - dispatcher.cancelAll() - } - } - - TestAction.Start -> { - viewModelScope.launch { - runAll() - } - } - - is TestAction.StartSingle -> { - viewModelScope.launch { - runSingleTest(action.uuid) - } - } - - is TestAction.CancelSingle -> { - viewModelScope.launch { - dispatcher.cancelOne(action.uuid) - } - } - - is TestAction.ClearSingle -> { - viewModelScope.launch { - /** Replace the existing work by cancelling and then clearing */ - dispatcher.launch(action.uuid) { - updateState { - copy(items = items.updating(action.uuid) { - copy( - log = log.cleared(), - highestLogLevel = LogLevel.Verbose, - result = null - ) - }) - } - } - } - } - - is TestAction.Init -> { - updateState { - /** We do a reverse map lookup here to keep the old data and insert new elements */ - val inverseMap = internalApis.map { entry -> entry.value to entry.key }.toMap() - - val newInternalApis = - /** Make sure it is distinct to avoid possible runtime crashes */ - action.apis.distinct() - /** Keep the old uuid if it exists */ - .associateBy { api -> inverseMap[api] ?: Uuid.random() } - .toPersistentMap() - - /** By default, they are idle, but we also keep the old data */ - val newData = (newInternalApis.mapValues { entry -> - // val pluginInstance = (entry.value.sourcePlugin?.let { PluginManager.plugins[it] } as? Plugin)?.filename - // There is no way to get the icon without a get request... - ImmutableTestResult( - language = entry.value.lang, - name = entry.value.name, - plugin = null, // The readable plugin name is wonky af to get - uuid = entry.key, - ) - } + items.data).toPersistentMap() - - val map = LogLevel.entries.associateWith { 0 }.toMutableMap() - for (entry in newData) { - map[entry.value.highestLogLevel] = map[entry.value.highestLogLevel]!! + 1 - } - - copy( - internalApis = newInternalApis, - items = items.setTo(newData), - ) - } - } - - is TestAction.Search -> { - updateState { - val newFiltering = filtering.setQuery(action.query) - copy(filtering = newFiltering, items = items.filteringBy(newFiltering)) - } - } - - is TestAction.SetLevel -> { - updateState { - val newFiltering = filtering.setLevel(action.level) - copy(filtering = newFiltering, items = items.filteringBy(newFiltering)) - } - } - } - } - - private suspend fun runAll() { - val apis = state.value.items.sorted - - for (api in apis) { - runSingleTest(api) - } - } - - private suspend fun runSingleTest(uuid: Uuid) { - val internalApi = state.value.internalApis[uuid] ?: return - - dispatcher.launch(uuid) { - updateState { - val newItems = items.updating(uuid) { - copy( - isLoading = true, - log = persistentListOf(), - highestLogLevel = LogLevel.Verbose, - result = null, - ) - } - copy( - /** - * We assume that runSingleProviderTest will not crash, so the final updateState - * will always be called and decrement this - * */ - activeQueries = activeQueries + 1, - items = newItems - ) - } - - val logger = object : Log { - override fun log(item: LogItem) { - updateState { - copy(items = items.updating(uuid) { - addMessage(item) - }) - } - } - } - - val result = runSingleProviderTest(internalApi, logger) - - updateState { - copy( - activeQueries = activeQueries - 1, - items = items.updating(uuid) { - copy( - isLoading = false, - result = result, - /** Only set the log level when done to prevent items from sorting directly */ - highestLogLevel = log.fold( - highestLogLevel - ) { acc, item -> acc.highestLogLevel(item.level) } - ) - }) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/utils/DirectoryPicker.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/utils/DirectoryPicker.kt index 9b380d2d6..dfc931174 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/utils/DirectoryPicker.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/utils/DirectoryPicker.kt @@ -5,13 +5,12 @@ import android.net.Uri import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import com.lagradost.cloudstream3.CloudStreamApp -import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.safefile.SafeFile fun Fragment.getChooseFolderLauncher(dirSelected: (uri: Uri?, path: String?) -> Unit) = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> // It lies, it can be null if file manager quits. - if (uri == null) { + if(uri == null) { dirSelected(null, null) return@registerForActivityResult } @@ -20,14 +19,7 @@ fun Fragment.getChooseFolderLauncher(dirSelected: (uri: Uri?, path: String?) -> val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - try { - context.contentResolver.takePersistableUriPermission(uri, flags) - } catch (e: SecurityException) { - // Some non-system SAF providers grant temporary access but do not expose a - // persistable grant. Keep the returned URI usable for this session instead - // of crashing while delivering the picker result. - logError(e) - } + context.contentResolver.takePersistableUriPermission(uri, flags) val filePath = SafeFile.fromUri(context, uri)?.filePath() println("Selected URI path: $uri - Full path: $filePath") @@ -35,4 +27,4 @@ fun Fragment.getChooseFolderLauncher(dirSelected: (uri: Uri?, path: String?) -> // store the actual URI instead of the path due to permissions. // filePath should only be used for cosmetic purposes. dirSelected(uri, filePath) - } + } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt index 544dd9335..501ee0eef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt @@ -56,7 +56,7 @@ class SetupFragmentExtensions : BaseFragment( if (hasRepos) { binding?.repoRecyclerView?.adapter = RepoAdapter(true, {}, { - PluginsViewModel.downloadAll(activity, it, null) + PluginsViewModel.downloadAll(activity, it.url, null) }).apply { submitList(repositories.toList()) } } // else { @@ -84,7 +84,7 @@ class SetupFragmentExtensions : BaseFragment( if (isSetup) if ( // If any available languages - apis.distinctBy { it.lang }.size > 1 + synchronized(apis) { apis.distinctBy { it.lang }.size > 1 } ) { findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt index 42a62ed52..e96a662c3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt @@ -50,18 +50,18 @@ class SetupFragmentLanguage : BaseFragment( appIconImage.setImageDrawable(ContextCompat.getDrawable(ctx, drawable)) } - val tags = appLanguages.map { it.second } - val current = getCurrentLocale(ctx).replace('_', '-') - val currentIndex = tags.indexOf(current).takeIf { it >= 0 } - ?: tags.indexOf(current.substringBefore('-')) + val current = getCurrentLocale(ctx) + val languageTagsIETF = appLanguages.map { it.second } + val languageNames = appLanguages.map { it.nameNextToFlagEmoji() } + val currentIndex = languageTagsIETF.indexOf(current) - arrayAdapter.addAll(appLanguages.map { it.nameNextToFlagEmoji() }) + arrayAdapter.addAll(languageNames) listview1.adapter = arrayAdapter listview1.choiceMode = AbsListView.CHOICE_MODE_SINGLE - if (currentIndex >= 0) listview1.setItemChecked(currentIndex, true) + listview1.setItemChecked(currentIndex, true) listview1.setOnItemClickListener { _, _, selectedLangIndex, _ -> - val langTagIETF = tags[selectedLangIndex] + val langTagIETF = languageTagsIETF[selectedLangIndex] CommonActivity.setLocale(activity, langTagIETF) settingsManager.edit { putString(getString(R.string.locale_key), langTagIETF) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index 45755b947..8da121daa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -4,6 +4,7 @@ import android.view.View import android.widget.AbsListView import android.widget.ArrayAdapter import androidx.core.content.edit +import androidx.core.util.forEach import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.R @@ -26,41 +27,40 @@ class SetupFragmentMedia : BaseFragment( safe { val ctx = context ?: return@safe val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) - val prefKey = getString(R.string.prefer_media_type_key) val arrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) - val sortedTypes = enumValues().sorted() + val names = enumValues().sorted().map { it.name } + val selected = mutableListOf() - arrayAdapter.addAll(sortedTypes.map { it.name }) + arrayAdapter.addAll(names) binding.apply { listview1.let { it.adapter = arrayAdapter it.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE - val stored = settingsManager.getStringSet(prefKey, null) - sortedTypes.forEachIndexed { index, type -> - // All media types except NSFW are checked by default - val isChecked = stored?.contains(type.ordinal.toString()) - ?: (type != TvType.NSFW) - it.setItemChecked(index, isChecked) - } - - val saveSelection = { - val values = sortedTypes - .filterIndexed { index, _ -> it.isItemChecked(index) } - .map { type -> type.ordinal.toString() } - .toSet() - settingsManager.edit { putStringSet(prefKey, values) } + it.setOnItemClickListener { _, _, _, _ -> + it.checkedItemPositions?.forEach { key, value -> + if (value) { + selected.add(key) + } else { + selected.remove(key) + } + } + val prefValues = selected.mapNotNull { pos -> + val item = + it.getItemAtPosition(pos)?.toString() ?: return@mapNotNull null + val itemVal = TvType.valueOf(item) + itemVal.ordinal.toString() + }.toSet() + settingsManager.edit { + putStringSet(getString(R.string.prefer_media_type_key), prefValues) + } // Regenerate set homepage DataStoreHelper.currentHomePage = null } - - if (stored == null) saveSelection() - - it.setOnItemClickListener { _, _, _, _ -> saveSelection() } } nextBtt.setOnClickListener { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index c18be8a2f..3c4a09ade 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -36,10 +36,10 @@ class SetupFragmentProviderLanguage : BaseFragment diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt index 0e203989f..f9b1cb1fe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt @@ -11,8 +11,6 @@ import android.view.View import android.widget.TextView import android.widget.Toast import androidx.annotation.OptIn -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.mutableStateOf import androidx.media3.common.text.Cue import androidx.media3.common.util.UnstableApi import com.fasterxml.jackson.annotation.JsonProperty @@ -40,22 +38,18 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable const val CHROME_SUBTITLE_KEY = "chome_subtitle_settings" -@Serializable -@Immutable data class SaveChromeCaptionStyle( - @JsonProperty("fontFamily") @SerialName("fontFamily") val fontFamily: String? = null, - @JsonProperty("fontGenericFamily") @SerialName("fontGenericFamily") val fontGenericFamily: Int? = null, - @JsonProperty("backgroundColor") @SerialName("backgroundColor") val backgroundColor: Int = 0x00FFFFFF, // transparent - @JsonProperty("edgeColor") @SerialName("edgeColor") val edgeColor: Int = Color.BLACK, // BLACK - @JsonProperty("edgeType") @SerialName("edgeType") val edgeType: Int = EDGE_TYPE_OUTLINE, - @JsonProperty("foregroundColor") @SerialName("foregroundColor") val foregroundColor: Int = Color.WHITE, - @JsonProperty("fontScale") @SerialName("fontScale") val fontScale: Float = 1.05f, - @JsonProperty("windowColor") @SerialName("windowColor") val windowColor: Int = Color.TRANSPARENT, + @JsonProperty("fontFamily") var fontFamily: String? = null, + @JsonProperty("fontGenericFamily") var fontGenericFamily: Int? = null, + @JsonProperty("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent + @JsonProperty("edgeColor") var edgeColor: Int = Color.BLACK, // BLACK + @JsonProperty("edgeType") var edgeType: Int = EDGE_TYPE_OUTLINE, + @JsonProperty("foregroundColor") var foregroundColor: Int = Color.WHITE, + @JsonProperty("fontScale") var fontScale: Float = 1.05f, + @JsonProperty("windowColor") var windowColor: Int = Color.TRANSPARENT, ) class ChromecastSubtitlesFragment : BaseFragment( @@ -95,9 +89,6 @@ class ChromecastSubtitlesFragment : BaseFragment(CHROME_SUBTITLE_KEY) ?: defaultChromeCastSubtitleState - ) + private val defaultState = SaveChromeCaptionStyle() } private fun onColorSelected(stuff: Pair) { @@ -143,10 +131,10 @@ class ChromecastSubtitlesFragment : BaseFragment state = state.copy(foregroundColor = realColor) - 1 -> state = state.copy(edgeColor = realColor) - 2 -> state = state.copy(backgroundColor = realColor) - 3 -> state = state.copy(windowColor = realColor) + 0 -> state.foregroundColor = realColor + 1 -> state.edgeColor = realColor + 2 -> state.backgroundColor = realColor + 3 -> state.windowColor = realColor else -> Unit } @@ -249,13 +237,13 @@ class ChromecastSubtitlesFragment : BaseFragment - state = state.copy(edgeType = edgeTypes.map { it.first }[index]) + state.edgeType = edgeTypes.map { it.first }[index] updateState() } } binding.subsEdgeType.setOnLongClickListener { - state = state.copy(edgeType = defaultChromeCastSubtitleState.edgeType) + state.edgeType = defaultState.edgeType updateState() showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true @@ -290,13 +278,13 @@ class ChromecastSubtitlesFragment : BaseFragment - state = state.copy(fontScale = fontSizes.map { it.first }[index]) + state.fontScale = fontSizes.map { it.first }[index] //textView.context.updateState() // font size not changed } } binding.subsFontSize.setOnLongClickListener { _ -> - state = state.copy(fontScale = defaultChromeCastSubtitleState.fontScale) + state.fontScale = defaultState.fontScale //textView.context.updateState() // font size not changed showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true @@ -323,12 +311,12 @@ class ChromecastSubtitlesFragment : BaseFragment - state = state.copy(fontFamily = fontTypes.map { it.first }[index]) + state.fontFamily = fontTypes.map { it.first }[index] updateState() } } binding.subsFont.setOnLongClickListener { _ -> - state = state.copy(fontFamily = defaultChromeCastSubtitleState.fontFamily) + state.fontFamily = defaultState.fontFamily updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment2.kt deleted file mode 100644 index a25a8aa88..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment2.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.lagradost.cloudstream3.ui.subtitles - -import com.lagradost.cloudstream3.utils.BaseComposeFragment -import com.lagradost.cloudstream4.compose.Screen - -class ChromecastSubtitlesFragment2 : BaseComposeFragment(), Screen by ChromecastSubtitlesScreen \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesScreen.kt deleted file mode 100644 index 63c3209b6..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesScreen.kt +++ /dev/null @@ -1,159 +0,0 @@ -package com.lagradost.cloudstream3.ui.subtitles - -import androidx.compose.foundation.layout.RowScope -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DEPRESSED -import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DROP_SHADOW -import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_NONE -import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_OUTLINE -import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_RAISED -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment.Companion.applyStyleEvent -import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment.Companion.chromeCastSubtitleState -import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment.Companion.defaultChromeCastSubtitleState -import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment.Companion.saveStyle -import com.lagradost.cloudstream3.ui.subtitles.SubtitlesScreen.SetUpUndoBar -import com.mihon.common.preference.StatePreferenceStore -import com.mihon.presentation.settings.Preference -import com.mihon.presentation.settings.SearchableSettings -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.persistentMapOf -import kotlin.math.roundToInt - -object ChromecastSubtitlesScreen : SearchableSettings { - @Composable - override fun getTitleRes(): String = stringResource(R.string.chromecast_subtitles_settings) - - // For ux purposes we also want to show a diff of what will be reset - private val diff = arrayOf Any?>>( - R.string.subs_font to { style -> style.fontFamily }, - R.string.subs_text_color to { state -> state.foregroundColor }, - R.string.subs_font_size to { state -> state.fontScale }, - R.string.subs_edge_type to { state -> state.edgeType }, - R.string.subs_outline_color to { state -> state.edgeColor }, - R.string.subs_window_color to { state -> state.windowColor }, - R.string.subs_background_color to { state -> state.backgroundColor }, - ) - - @Composable - override fun RowScope.AppBarAction() { - val initialState = remember { chromeCastSubtitleState.value } - var state by chromeCastSubtitleState - val context = LocalContext.current - - SideEffect(state) { - // When we modify the state, we modify the stored data - context.saveStyle(state) - } - - DisposableEffect(Unit) { - onDispose { - if (initialState != state) { - applyStyleEvent.invoke(state) - } - } - } - SetUpUndoBar( - diff = diff, - initialState = initialState, - currentState = chromeCastSubtitleState, - default = defaultChromeCastSubtitleState - ) - } - - @Composable - override fun getPreferences(): List { - val store = StatePreferenceStore(chromeCastSubtitleState) - - return persistentListOf( - Preference.PreferenceGroup( - title = stringResource(R.string.subs_font), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - preference = store.field(SaveChromeCaptionStyle::fontFamily) { newValue -> - copy(fontFamily = newValue) - }, - icon = painterResource(R.drawable.font_download_24px), - title = stringResource(R.string.subs_font), - entries = persistentMapOf( - null to stringResource(R.string.normal), - "Droid Sans" to "Droid Sans", - "Droid Sans Mono" to "Droid Sans Mono", - "Droid Serif Regular" to "Droid Serif Regular", - "Cutive Mono" to "Cutive Mono", - "Short Stack" to "Short Stack", - "Quintessential" to "Quintessential", - "Alegreya Sans SC" to "Alegreya Sans SC", - ) - ), - Preference.PreferenceItem.ColorPreference( - preference = store.field(SaveChromeCaptionStyle::foregroundColor) { newValue -> - copy(foregroundColor = newValue) - }, - title = stringResource(R.string.subs_text_color), - icon = painterResource(R.drawable.format_color_text_24px), - ), - Preference.PreferenceItem.SliderPreference( - preference = store.field( - get = { fontScale.times(100.0f).roundToInt() }, - set = { newValue -> copy(fontScale = newValue.toFloat() * 0.01f) }), - icon = painterResource(R.drawable.text_fields_24px), - title = stringResource(R.string.subs_font_size), - valueRange = 75..150, - steps = 75 / 5 - 1, - ) - ) - ), Preference.PreferenceGroup( - title = stringResource(R.string.subs_edge), preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - preference = store.field(SaveChromeCaptionStyle::edgeType) { newValue -> - copy(edgeType = newValue) - }, - icon = painterResource(R.drawable.shadow_24px), - title = stringResource(R.string.subs_edge_type), - entries = persistentMapOf( - EDGE_TYPE_NONE to stringResource(R.string.subtitles_none), - EDGE_TYPE_OUTLINE to stringResource(R.string.subtitles_outline), - EDGE_TYPE_DEPRESSED to stringResource(R.string.subtitles_depressed), - EDGE_TYPE_DROP_SHADOW to stringResource(R.string.subtitles_shadow), - EDGE_TYPE_RAISED to stringResource(R.string.subtitles_raised), - ) - ), - Preference.PreferenceItem.ColorPreference( - preference = store.field(SaveChromeCaptionStyle::edgeColor) { newValue -> - copy(edgeColor = newValue) - }, - //enabled = state.edgeType != EDGE_TYPE_NONE, - title = stringResource(R.string.subs_outline_color), - icon = painterResource(R.drawable.border_color_24px), - ), - ) - ), Preference.PreferenceGroup( - title = stringResource(R.string.background), preferenceItems = persistentListOf( - Preference.PreferenceItem.ColorPreference( - preference = store.field(SaveChromeCaptionStyle::windowColor) { newValue -> - copy(windowColor = newValue) - }, - title = stringResource(R.string.subs_window_color), - icon = painterResource(R.drawable.imagesearch_roller_24px), - ), - Preference.PreferenceItem.ColorPreference( - preference = store.field(SaveChromeCaptionStyle::backgroundColor) { newValue -> - copy(backgroundColor = newValue) - }, - title = stringResource(R.string.subs_background_color), - icon = painterResource(R.drawable.format_color_fill_24px), - ), - ) - ) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index dd363fc10..5f716cca3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -9,7 +9,6 @@ import android.os.Bundle import android.text.Layout import android.text.Spannable import android.text.SpannableString -import android.text.SpannableStringBuilder import android.text.style.StyleSpan import android.util.DisplayMetrics import android.util.TypedValue @@ -19,8 +18,6 @@ import android.widget.Toast import androidx.annotation.FontRes import androidx.annotation.OptIn import androidx.annotation.Px -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.mutableStateOf import androidx.core.content.edit import androidx.core.content.res.ResourcesCompat import androidx.media3.common.text.Cue @@ -57,92 +54,40 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.io.File const val SUBTITLE_KEY = "subtitle_settings" const val SUBTITLE_AUTO_SELECT_KEY = "subs_auto_select" const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download" -@Serializable -enum class SubtitleFont( - @FontRes val resource: Int, - val label: String -) { - @JsonProperty("Trebuchet") - @SerialName("Trebuchet") - Trebuchet(R.font.trebuchet_ms, "Trebuchet MS"), - @JsonProperty("Netflix") - @SerialName("Netflix") - Netflix(R.font.netflix_sans, "Netflix Sans"), - @JsonProperty("Google") - @SerialName("Google") - Google(R.font.google_sans, "Google Sans"), - @JsonProperty("Open") - @SerialName("Open") - Open(R.font.open_sans, "Open Sans"), - @JsonProperty("Futura") - @SerialName("Futura") - Futura(R.font.futura, "Futura"), - @JsonProperty("Consola") - @SerialName("Consola") - Consola(R.font.consola, "Consola"), - @JsonProperty("Gotham") - @SerialName("Gotham") - Gotham(R.font.gotham, "Gotham"), - @JsonProperty("Lucida") - @SerialName("Lucida") - Lucida(R.font.lucida_grande, "Lucida Grande"), - @JsonProperty("STIX") - @SerialName("STIX") - STIX(R.font.stix_general, "STIX General"), - @JsonProperty("TimesNewRoman") - @SerialName("TimesNewRoman") - TimesNewRoman(R.font.times_new_roman, "Times New Roman"), - @JsonProperty("Verdana") - @SerialName("Verdana") - Verdana(R.font.verdana, "Verdana"), - @JsonProperty("Ubuntu") - @SerialName("Ubuntu") - Ubuntu(R.font.ubuntu_regular, "Ubuntu"), - @JsonProperty("Comic") - @SerialName("Comic") - Comic(R.font.comic_sans, "Comic Sans"), - @JsonProperty("Poppins") - @SerialName("Poppins") - Poppins(R.font.poppins_regular, "Poppins"), - @JsonProperty("Custom") - @SerialName("Custom") - Custom(0, "Custom"), -} - -@Serializable -@Immutable data class SaveCaptionStyle( - @JsonProperty("foregroundColor") @SerialName("foregroundColor") val foregroundColor: Int, - @JsonProperty("backgroundColor") @SerialName("backgroundColor") val backgroundColor: Int, - @JsonProperty("windowColor") @SerialName("windowColor") val windowColor: Int, + @JsonProperty("foregroundColor") var foregroundColor: Int, + @JsonProperty("backgroundColor") var backgroundColor: Int, + @JsonProperty("windowColor") var windowColor: Int, @OptIn(UnstableApi::class) - @JsonProperty("edgeType") @SerialName("edgeType") val edgeType: @CaptionStyleCompat.EdgeType Int, - @JsonProperty("edgeColor") @SerialName("edgeColor") val edgeColor: Int, - @JsonProperty("font") @SerialName("font") val font: SubtitleFont? = null, - @JsonProperty("typefaceFilePath") @SerialName("typefaceFilePath") val typefaceFilePath: String?, - @JsonProperty("elevation") @SerialName("elevation") val elevation: Int, // in dp - @JsonProperty("fixedTextSize") @SerialName("fixedTextSize") val fixedTextSize: Float?, // in sp - @Px @JsonProperty("edgeSize") @SerialName("edgeSize") val edgeSize: Float? = null, - @JsonProperty("removeCaptions") @SerialName("removeCaptions") val removeCaptions: Boolean = false, - @JsonProperty("removeBloat") @SerialName("removeBloat") val removeBloat: Boolean = true, - /** Apply caps lock to the text */ - @JsonProperty("upperCase") @SerialName("upperCase") val upperCase: Boolean = false, - /** Apply bold to the text */ - @JsonProperty("bold") @SerialName("bold") val bold: Boolean = false, - /** Apply italic to the text */ - @JsonProperty("italic") @SerialName("italic") val italic: Boolean = false, - /** in px, background radius, aka how round the background (backgroundColor) on each row is */ - @JsonProperty("backgroundRadius") @SerialName("backgroundRadius") val backgroundRadius: Float? = null, + @JsonProperty("edgeType") var edgeType: @CaptionStyleCompat.EdgeType Int, + @JsonProperty("edgeColor") var edgeColor: Int, + @FontRes + @JsonProperty("typeface") var typeface: Int?, + @JsonProperty("typefaceFilePath") var typefaceFilePath: String?, + /**in dp**/ + @JsonProperty("elevation") var elevation: Int, + /**in sp**/ + @JsonProperty("fixedTextSize") var fixedTextSize: Float?, + @Px + @JsonProperty("edgeSize") var edgeSize: Float? = null, + @JsonProperty("removeCaptions") var removeCaptions: Boolean = false, + @JsonProperty("removeBloat") var removeBloat: Boolean = true, + /** Apply caps lock to the text **/ + @JsonProperty("upperCase") var upperCase: Boolean = false, + /** Apply bold to the text **/ + @JsonProperty("bold") var bold: Boolean = false, + /** Apply italic to the text **/ + @JsonProperty("italic") var italic: Boolean = false, + /** in px, background radius, aka how round the background (backgroundColor) on each row is **/ + @JsonProperty("backgroundRadius") var backgroundRadius: Float? = null, /** The SSA_ALIGNMENT */ - @JsonProperty("alignment") @SerialName("alignment") val alignment: Int? = null, + @JsonProperty("alignment") var alignment: Int? = null, ) const val DEF_SUBS_ELEVATION = 20 @@ -171,9 +116,6 @@ class SubtitlesFragment : BaseDialogFragment( ) } - view.clipToPadding = false - view.clipChildren = false - // we default to 25sp, this is needed as RoundedBackgroundColorSpan breaks on override sizes val size = data.fixedTextSize ?: 25.0f view.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, size) @@ -186,7 +128,7 @@ class SubtitlesFragment : BaseDialogFragment( } fun Cue.Builder.applyStyle(style: SaveCaptionStyle): Cue.Builder { - val edgeSize = style.edgeSize ?: 0.0f + val edgeSize = style.edgeSize /* This is old code for only applying on non null @@ -204,18 +146,22 @@ class SubtitlesFragment : BaseDialogFragment( // instead we use the inherit size of the subtitle view setTextSize(Cue.DIMEN_UNSET, Cue.TYPE_UNSET) + // 2. apply edge text?.let { text -> - val textSpanBuilder = SpannableStringBuilder(text) - - // 2. apply edge - if (edgeSize > 0.0f) { - textSpanBuilder.setSpan( - OutlineSpan(edgeSize), 0, textSpanBuilder.length, + val customSpan = SpannableString.valueOf(text) + if (edgeSize != null) { + customSpan.setSpan( + OutlineSpan(edgeSize), 0, customSpan.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } + setText(customSpan) + } + + // 3. apply bold + italic + text?.let { text -> + val customSpan = SpannableString.valueOf(text) - // 3. apply bold + italic val typeface = when (style.bold to style.italic) { (true to true) -> Typeface.BOLD_ITALIC (true to false) -> Typeface.BOLD @@ -227,36 +173,39 @@ class SubtitlesFragment : BaseDialogFragment( } if (typeface != Typeface.NORMAL) { val styleSpan = StyleSpan(typeface) - textSpanBuilder.setSpan( - styleSpan, 0, textSpanBuilder.length, + customSpan.setSpan( + styleSpan, 0, customSpan.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } + setText(customSpan) + } - // 4. apply radius - val radius = style.backgroundRadius ?: 0.0f - if (radius > 0.0f && style.backgroundColor != Color.TRANSPARENT) { + // 4. apply radius + text?.let { text -> + val customSpan = SpannableString.valueOf(text) + val radius = style.backgroundRadius + + if (radius != null && style.backgroundColor != Color.TRANSPARENT) { val styleSpan = RoundedBackgroundColorSpan( style.backgroundColor, this.textAlignment ?: Layout.Alignment.ALIGN_CENTER, 2.0F + radius * 0.5f, radius ) - textSpanBuilder.setSpan( - styleSpan, 0, textSpanBuilder.length, + customSpan.setSpan( + styleSpan, 0, customSpan.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } + setText(customSpan) + } - // 5. remove captions but ensure that styling is kept - // Just doing .replace removes all styling! + // 5. remove captions + text?.let { text -> if (style.removeCaptions) { - for (match in captionRegex.findAll(text).toList().asReversed()) { - textSpanBuilder.delete(match.range.first, match.range.last + 1) - } + setText(text.replace(captionRegex, "")) } - - this.setText(textSpanBuilder) } // 6. set alignment @@ -266,31 +215,23 @@ class SubtitlesFragment : BaseDialogFragment( private fun Context.fromSaveToStyle(data: SaveCaptionStyle): CaptionStyleCompat { return CaptionStyleCompat( data.foregroundColor, - data.backgroundColor, + // we actually override with a custom span when backgroundRadius != null + if (data.backgroundRadius == null) data.backgroundColor else Color.TRANSPARENT, data.windowColor, data.edgeType, data.edgeColor, data.typefaceFilePath?.let { try { // RuntimeException: Font asset not found - // Fuck android, they have no good way to load a font from a uri without a copy Typeface.createFromFile(File(it)) - } catch (_: Exception) { + } catch (e: Exception) { null } - } ?: data.font?.let { font -> - if(font.resource != 0) { - try { - ResourcesCompat.getFont( - this, - font.resource - ) - } catch (_ : Throwable) { - Typeface.SANS_SERIF - } - } else { - Typeface.SANS_SERIF - } + } ?: data.typeface?.let { + ResourcesCompat.getFont( + this, + it + ) } ?: Typeface.SANS_SERIF ) @@ -313,29 +254,25 @@ class SubtitlesFragment : BaseDialogFragment( } } + private var cachedSubtitleStyle: SaveCaptionStyle? = null + fun Context.saveStyle(style: SaveCaptionStyle) { - if (subtitleStyleState.value !== style) { - subtitleStyleState.value = style - } + cachedSubtitleStyle = style this.setKey(SUBTITLE_KEY, style) } - val defaultSubtitleStyle = SaveCaptionStyle( - foregroundColor = getDefColor(0), - backgroundColor = getDefColor(2), - windowColor = getDefColor(3), - edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE, - edgeColor = getDefColor(1), - font = null, - typefaceFilePath = null, - elevation = DEF_SUBS_ELEVATION, - fixedTextSize = null, - ) - val subtitleStyleState = - mutableStateOf((getKey(SUBTITLE_KEY) ?: defaultSubtitleStyle)) - fun getCurrentSavedStyle(): SaveCaptionStyle { - return subtitleStyleState.value + return cachedSubtitleStyle ?: (getKey(SUBTITLE_KEY) ?: SaveCaptionStyle( + foregroundColor = getDefColor(0), + backgroundColor = getDefColor(2), + windowColor = getDefColor(3), + edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE, + edgeColor = getDefColor(1), + typeface = null, + typefaceFilePath = null, + elevation = DEF_SUBS_ELEVATION, + fixedTextSize = null, + )).also { cachedSubtitleStyle = it } } private fun Context.getSavedFonts(): List { @@ -357,11 +294,11 @@ class SubtitlesFragment : BaseDialogFragment( } fun getDownloadSubsLanguageTagIETF(): List { - return getKey>(SUBTITLE_DOWNLOAD_KEY) ?: listOf("en") + return getKey(SUBTITLE_DOWNLOAD_KEY) ?: listOf("en") } fun getAutoSelectLanguageTagIETF(): String { - return getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en" + return getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en" } } @@ -379,10 +316,10 @@ class SubtitlesFragment : BaseDialogFragment( private fun Context.setColor(id: Int, color: Int?) { val realColor = color ?: getDefColor(id) when (id) { - 0 -> state = state.copy(foregroundColor = realColor) - 1 -> state = state.copy(edgeColor = realColor) - 2 -> state = state.copy(backgroundColor = realColor) - 3 -> state = state.copy(windowColor = realColor) + 0 -> state.foregroundColor = realColor + 1 -> state.edgeColor = realColor + 2 -> state.backgroundColor = realColor + 3 -> state.windowColor = realColor else -> Unit } @@ -508,7 +445,7 @@ class SubtitlesFragment : BaseDialogFragment( false, dismissCallback ) { index -> - state = state.copy(elevation = elevationTypes.map { it.first }[index]) + state.elevation = elevationTypes.map { it.first }[index] textView.context.updateState() if (hide) activity?.hideSystemUI() @@ -516,7 +453,7 @@ class SubtitlesFragment : BaseDialogFragment( } subsSubtitleElevation.setOnLongClickListener { - state = state.copy(elevation = DEF_SUBS_ELEVATION) + state.elevation = DEF_SUBS_ELEVATION it.context.updateState() showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true @@ -539,14 +476,13 @@ class SubtitlesFragment : BaseDialogFragment( false, dismissCallback ) { index -> - state = - state.copy(backgroundRadius = radiusTypes.map { it.first }[index]?.toFloat()) + state.backgroundRadius = radiusTypes.map { it.first }[index]?.toFloat() textView.context.updateState() } } subsBackgroundRadius.setOnLongClickListener { - state = state.copy(backgroundRadius = null) + state.backgroundRadius = null it.context.updateState() showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true @@ -574,7 +510,7 @@ class SubtitlesFragment : BaseDialogFragment( false, dismissCallback ) { index -> - state = state.copy(alignment = alignmentTypes.map { it.first }[index]) + state.alignment = alignmentTypes.map { it.first }[index] textView.context.updateState() } } @@ -602,13 +538,13 @@ class SubtitlesFragment : BaseDialogFragment( false, dismissCallback ) { index -> - state = state.copy(edgeType = edgeTypes.map { it.first }[index]) + state.edgeType = edgeTypes.map { it.first }[index] textView.context.updateState() } } subsEdgeType.setOnLongClickListener { - state = state.copy(edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE) + state.edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE it.context.updateState() showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true @@ -628,7 +564,7 @@ class SubtitlesFragment : BaseDialogFragment( false, dismissCallback ) { index -> - state = state.copy(fixedTextSize = fontSizes.map { it.first }[index]) + state.fixedTextSize = fontSizes.map { it.first }[index] textView.context.updateState() } } @@ -647,47 +583,47 @@ class SubtitlesFragment : BaseDialogFragment( false, dismissCallback ) { index -> - state = state.copy(edgeSize = fontSizes.map { it.first }[index]) + state.edgeSize = fontSizes.map { it.first }[index] textView.context.updateState() } } subtitlesRemoveBloat.isChecked = state.removeBloat subtitlesRemoveBloat.setOnCheckedChangeListener { _, b -> - state = state.copy(removeBloat = b) + state.removeBloat = b } subtitlesUppercase.isChecked = state.upperCase subtitlesUppercase.setOnCheckedChangeListener { _, b -> - state = state.copy(upperCase = b) + state.upperCase = b context?.updateState() } subtitlesRemoveCaptions.isChecked = state.removeCaptions subtitlesRemoveCaptions.setOnCheckedChangeListener { _, b -> - state = state.copy(removeCaptions = b) + state.removeCaptions = b } subtitlesBold.isChecked = state.bold subtitlesBold.setOnCheckedChangeListener { _, b -> - state = state.copy(bold = b) + state.bold = b context?.updateState() } subtitlesItalic.isChecked = state.italic subtitlesItalic.setOnCheckedChangeListener { _, b -> - state = state.copy(italic = b) + state.italic = b context?.updateState() } subsFontSize.setOnLongClickListener { _ -> - state = state.copy(fixedTextSize = null) + state.fixedTextSize = null context?.updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } subsEdgeSize.setOnLongClickListener { _ -> - state = state.copy(edgeSize = null) + state.edgeSize = null context?.updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true @@ -733,7 +669,7 @@ class SubtitlesFragment : BaseDialogFragment( savedFontTypes.indexOfFirst { it.absolutePath == state.typefaceFilePath } .let { index -> if (index == -1) - fontTypes.indexOfFirst { it.first == state.font?.resource } + fontTypes.indexOfFirst { it.first == state.typeface } else index + fontTypes.size } @@ -745,23 +681,20 @@ class SubtitlesFragment : BaseDialogFragment( false, dismissCallback ) { index -> - state = if (index < fontTypes.size) { - state.copy( - font = SubtitleFont.entries.firstOrNull { it.resource == fontTypes[index].first }, - typefaceFilePath = null - ) + if (index < fontTypes.size) { + state.typeface = fontTypes[index].first + state.typefaceFilePath = null } else { - state.copy( - typefaceFilePath = savedFontTypes[index - fontTypes.size].absolutePath, - font = null - ) + state.typefaceFilePath = savedFontTypes[index - fontTypes.size].absolutePath + state.typeface = null } textView.context.updateState() } } subsFont.setOnLongClickListener { textView -> - state = state.copy(font = null, typefaceFilePath = null) + state.typeface = null + state.typefaceFilePath = null textView.context.updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment2.kt deleted file mode 100644 index b9fd40556..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment2.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.lagradost.cloudstream3.ui.subtitles - -import com.lagradost.cloudstream3.utils.BaseComposeFragment -import com.lagradost.cloudstream3.utils.BaseDialogComposeFragment -import com.lagradost.cloudstream4.compose.Screen - -class SubtitlesFragment2 : BaseComposeFragment(), Screen by SubtitlesScreen -class SubtitlesFragmentDialog2 : BaseDialogComposeFragment(), Screen by SubtitlesScreen \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt deleted file mode 100644 index cba651c76..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt +++ /dev/null @@ -1,453 +0,0 @@ -package com.lagradost.cloudstream3.ui.subtitles - -import android.text.SpannableString -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.OptIn -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.media3.common.text.Cue -import androidx.media3.common.util.UnstableApi -import androidx.media3.ui.CaptionStyleCompat -import androidx.media3.ui.SubtitleView -import coil3.compose.AsyncImage -import com.lagradost.cloudstream3.CloudStreamApp -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.ui.player.CustomDecoder -import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyle -import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyleEvent -import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.defaultSubtitleStyle -import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.saveStyle -import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.setSubtitleViewStyle -import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.subtitleStyleState -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJsonLiteral -import com.lagradost.cloudstream3.utils.SubtitleHelper.languages -import com.lagradost.cloudstream4.compose.ActionDialog -import com.mihon.common.preference.AndroidPreferenceStore -import com.mihon.common.preference.DataPreferenceStore -import com.mihon.common.preference.StatePreferenceStore -import com.mihon.presentation.settings.Preference -import com.mihon.presentation.settings.SearchableSettings -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.persistentMapOf -import java.io.File - -object SubtitlesScreen : SearchableSettings { - @Composable - override fun getTitleRes(): String = stringResource(R.string.subtitles_settings) - - // For ux purposes we also want to show a diff of what will be reset - private val diff = arrayOf Any?>>( - R.string.subs_font to { style -> style.font }, - R.string.subs_text_color to { state -> state.foregroundColor }, - R.string.subs_font_size to { state -> state.fixedTextSize }, - R.string.uppercase_all_subtitles to { state -> state.upperCase }, - R.string.all_subtitles_bold to { state -> state.bold }, - R.string.all_subtitles_italic to { state -> state.italic }, - R.string.subs_edge_type to { state -> state.edgeType }, - R.string.subs_outline_color to { state -> state.edgeColor }, - R.string.subs_edge_size to { state -> state.edgeSize }, - R.string.subs_window_color to { state -> state.windowColor }, - R.string.subs_background_color to { state -> state.backgroundColor }, - R.string.background_radius to { state -> state.backgroundRadius }, - R.string.subs_subtitle_alignment to { state -> state.alignment }, - R.string.subs_subtitle_elevation to { state -> state.elevation }, - R.string.subtitles_remove_captions to { state -> state.removeCaptions }, - R.string.subtitles_remove_bloat to { state -> state.removeBloat }, - ) - - @Composable - fun SetUpUndoBar( - diff: Array Any?>>, - initialState: T, - currentState: MutableState, - default: T - ) { - var currentState by currentState - var dialogShown by remember { mutableStateOf(false) } - if (dialogShown) { - ActionDialog( - title = stringResource(R.string.subs_default_reset_toast), - // This can not be converted to joinToString due to stringResource being composable - text = - @Suppress("SimplifiableCallChain") - diff.filter { (_, f) -> f(default) != f(currentState) } - .map { (name, _) -> stringResource(name) }.joinToString(separator = "\n"), - confirmText = stringResource(R.string.reset_btn), - dismissText = stringResource(R.string.dismiss), - confirm = { - currentState = default - dialogShown = false - }, - dismiss = { - dialogShown = false - }) - } - - // Undo, current -> initial last saved value - AnimatedVisibility(currentState != initialState) { - IconButton(onClick = { - currentState = initialState - }) { - Icon( - painter = painterResource(R.drawable.undo_24px), - contentDescription = stringResource(R.string.reset_btn), - tint = MaterialTheme.colorScheme.onBackground - ) - } - } - - // Reset, current -> default - AnimatedVisibility(currentState != default) { - IconButton(onClick = { - dialogShown = true - }) { - Icon( - painter = painterResource(R.drawable.reset_colors_24px), - contentDescription = stringResource(R.string.subs_default_reset_toast), - tint = MaterialTheme.colorScheme.onBackground - ) - } - } - } - - @Composable - override fun RowScope.AppBarAction() { - val initialState = remember { subtitleStyleState.value } - var state by subtitleStyleState - val context = LocalContext.current - - SideEffect(state) { - // When we modify the state, we modify the stored data - context.saveStyle(state) - } - - DisposableEffect(Unit) { - onDispose { - if (initialState != state) { - applyStyleEvent.invoke(state) - } - } - } - - SetUpUndoBar( - diff = diff, - initialState = initialState, - currentState = subtitleStyleState, - default = defaultSubtitleStyle - ) - } - - private val sortedLanguages by lazy { - languages.map { it.IETF_tag to it.nameNextToFlagEmoji() }.sortedBy { - it.second.substringAfter("\u00a0").lowercase() - }.toMap() - } - - @OptIn(UnstableApi::class) - @Composable - override fun getPreferences(): List { - val store = StatePreferenceStore(subtitleStyleState) - val dataStore = DataPreferenceStore(LocalContext.current) - val androidStore = AndroidPreferenceStore(LocalContext.current) - - val subtitlesFilterSubLang = androidStore.getBoolean("filter_sub_lang_key", false) - val autoSelectSubtitles = dataStore.getString(SUBTITLE_AUTO_SELECT_KEY, "en") - val downloadSubsLanguage = - dataStore.getObjectFromString(SUBTITLE_DOWNLOAD_KEY, setOf("en"), serializer = { obj -> - obj.toList().toJsonLiteral() - }, deserializer = { json -> - parseJson>(json).toSet() - }) - - val subtitleFileSelector = - rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> - // It lies, it can be null if file manager quits. - if (uri == null) return@rememberLauncherForActivityResult - val context = CloudStreamApp.context ?: return@rememberLauncherForActivityResult - - try { - val localFile = File(context.filesDir, "customfont.ttf") - localFile.delete() - localFile.createNewFile() - context.contentResolver?.openInputStream(uri)?.use { stream -> - stream.copyTo(localFile.outputStream()) - } - store.state.value = store.state.value.copy( - font = SubtitleFont.Custom, - typefaceFilePath = localFile.absolutePath - ) - } catch (t: Throwable) { - logError(t) - } - } - - return persistentListOf( - Preference.PreferenceItem.CustomPreference( - title = stringResource(R.string.skip_type_preview), content = { - val state by store.state - val text = stringResource(R.string.subtitles_example_text) - val fixedText = - SpannableString.valueOf(if (state.upperCase) text.uppercase() else text) - - Box( - modifier = Modifier - .fillMaxWidth() - .height(75.dp) - ) { - AsyncImage( - contentDescription = stringResource(R.string.preview_background_img_des), - model = R.drawable.subtitles_preview_background, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - AndroidView(modifier = Modifier.fillMaxSize(), factory = { context -> - SubtitleView(context).apply { - setSubtitleViewStyle(this, state, false) - this.setCues( - listOf( - Cue.Builder().setText(fixedText).applyStyle(state).build() - ) - ) - } - }, update = { view -> - setSubtitleViewStyle(view, state, false) - view.setCues( - listOf( - Cue.Builder().setText(fixedText).applyStyle(state).build() - ) - ) - }) - } - }), - Preference.PreferenceGroup( - title = stringResource(R.string.subs_font), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - preference = store.field(SaveCaptionStyle::font) { newValue -> copy(font = newValue, typefaceFilePath = null) }, - icon = painterResource(R.drawable.font_download_24px), - title = stringResource(R.string.subs_font), - entries = mapOf(null to stringResource(R.string.normal)) + SubtitleFont.entries.associateWith { it.label }, - onValueChanged = { value -> - if (value?.resource == 0) { - subtitleFileSelector.launch( - arrayOf( - "font/*", - ) - ) - false - } else { - true - } - } - ), - Preference.PreferenceItem.ColorPreference( - preference = store.field(SaveCaptionStyle::foregroundColor) { newValue -> - copy(foregroundColor = newValue) - }, - title = stringResource(R.string.subs_text_color), - icon = painterResource(R.drawable.format_color_text_24px), - ), - Preference.PreferenceItem.SliderPreference( - preference = store.field( - get = { fixedTextSize?.toInt() ?: 25 }, - set = { newValue -> copy(fixedTextSize = newValue.toFloat()) } - ), - icon = painterResource(R.drawable.text_fields_24px), - title = stringResource(R.string.subs_font_size), - valueRange = 5..60 - ), - Preference.PreferenceItem.SwitchPreference( - preference = store.field(SaveCaptionStyle::upperCase) { newValue -> - copy(upperCase = newValue) - }, - icon = painterResource(R.drawable.uppercase_24px), - title = stringResource(R.string.uppercase_all_subtitles), - ), - Preference.PreferenceItem.SwitchPreference( - preference = store.field(SaveCaptionStyle::bold) { newValue -> copy(bold = newValue) }, - icon = painterResource(R.drawable.format_bold_24px), - title = stringResource(R.string.all_subtitles_bold), - ), - Preference.PreferenceItem.SwitchPreference( - preference = store.field(SaveCaptionStyle::italic) { newValue -> copy(italic = newValue) }, - icon = painterResource(R.drawable.format_italic_24px), - title = stringResource(R.string.all_subtitles_italic), - ), - ) - ), - Preference.PreferenceGroup( - title = stringResource(R.string.subs_edge), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - icon = painterResource(R.drawable.shadow_24px), - title = stringResource(R.string.subs_edge_type), - preference = store.field(SaveCaptionStyle::edgeType) { newValue -> - copy(edgeType = newValue) - }, - entries = persistentMapOf( - CaptionStyleCompat.EDGE_TYPE_NONE to stringResource(R.string.subtitles_none), - CaptionStyleCompat.EDGE_TYPE_OUTLINE to stringResource(R.string.subtitles_outline), - CaptionStyleCompat.EDGE_TYPE_DEPRESSED to stringResource(R.string.subtitles_depressed), - CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW to stringResource(R.string.subtitles_shadow), - CaptionStyleCompat.EDGE_TYPE_RAISED to stringResource(R.string.subtitles_raised), - ) - ), - Preference.PreferenceItem.ColorPreference( - //enabled = state.edgeType != CaptionStyleCompat.EDGE_TYPE_NONE, - preference = store.field(SaveCaptionStyle::edgeColor) { newValue -> - copy(edgeColor = newValue) - }, - title = stringResource(R.string.subs_outline_color), - icon = painterResource(R.drawable.border_color_24px), - ), - Preference.PreferenceItem.SliderPreference( - preference = store.field( - get = { edgeSize?.toInt() ?: 0 }, - set = { newValue -> copy(edgeSize = newValue.toFloat()) } - ), - //enabled = state.edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE, - icon = painterResource(R.drawable.shadow_add_24px), - title = stringResource(R.string.subs_edge_size), - valueRange = 0..60, - ) - ) - ), - - Preference.PreferenceGroup( - title = stringResource(R.string.background), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ColorPreference( - preference = store.field(SaveCaptionStyle::windowColor) { newValue -> - copy(windowColor = newValue) - }, - title = stringResource(R.string.subs_window_color), - icon = painterResource(R.drawable.imagesearch_roller_24px), - ), - Preference.PreferenceItem.ColorPreference( - preference = store.field(SaveCaptionStyle::backgroundColor) { newValue -> - copy(backgroundColor = newValue) - }, - title = stringResource(R.string.subs_background_color), - icon = painterResource(R.drawable.format_color_fill_24px), - ), - Preference.PreferenceItem.SliderPreference( - preference = store.field( - get = { backgroundRadius?.toInt() ?: 0 }, - set = { newValue -> copy(backgroundRadius = newValue.toFloat()) } - ), - icon = painterResource(R.drawable.rounded_corner_24px), - title = stringResource(R.string.background_radius), - valueRange = 0..50, - steps = 9, - ) - ) - ), - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_player_layout), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - preference = store.field(SaveCaptionStyle::alignment) { newValue -> - copy(alignment = newValue) - }, - - icon = painterResource(R.drawable.format_align_center_24px), - title = stringResource(R.string.subs_subtitle_alignment), - entries = persistentMapOf( - null to stringResource(R.string.automatic), - CustomDecoder.SSA_ALIGNMENT_BOTTOM_LEFT to stringResource(R.string.bottom_left), - CustomDecoder.SSA_ALIGNMENT_BOTTOM_CENTER to stringResource(R.string.bottom_center), - CustomDecoder.SSA_ALIGNMENT_BOTTOM_RIGHT to stringResource(R.string.bottom_right), - CustomDecoder.SSA_ALIGNMENT_MIDDLE_LEFT to stringResource(R.string.middle_left), - CustomDecoder.SSA_ALIGNMENT_MIDDLE_CENTER to stringResource(R.string.middle_center), - CustomDecoder.SSA_ALIGNMENT_MIDDLE_RIGHT to stringResource(R.string.middle_right), - CustomDecoder.SSA_ALIGNMENT_TOP_LEFT to stringResource(R.string.top_left), - CustomDecoder.SSA_ALIGNMENT_TOP_CENTER to stringResource(R.string.top_center), - CustomDecoder.SSA_ALIGNMENT_TOP_RIGHT to stringResource(R.string.top_right), - ) - ), - Preference.PreferenceItem.SliderPreference( - preference = store.field(SaveCaptionStyle::elevation) { newValue -> - copy(elevation = newValue) - }, - title = stringResource(R.string.subs_subtitle_elevation), - icon = painterResource(R.drawable.text_select_move_up_24px), - valueRange = 0..400, - steps = 39 - ), - ) - ), - - Preference.PreferenceGroup( - title = stringResource(R.string.extension_language), - preferenceItems = persistentListOf( - Preference.PreferenceItem.ListPreference( - icon = painterResource(R.drawable.language_korean_latin_24px), - title = stringResource(R.string.subs_auto_select_language), - preference = autoSelectSubtitles, - entries = mapOf( - "None" to stringResource(R.string.none) - ) + sortedLanguages, - ), - Preference.PreferenceItem.MultiSelectListPreference( - icon = painterResource(R.drawable.language_download2), - title = stringResource(R.string.subs_download_languages), - preference = downloadSubsLanguage, - entries = mapOf( - "None" to stringResource(R.string.none) - ) + sortedLanguages, - ), - Preference.PreferenceItem.SwitchPreference( - preference = subtitlesFilterSubLang, - icon = painterResource(R.drawable.filter_alt_24px), - title = stringResource(R.string.subtitles_filter_lang), - ), - ) - ), - Preference.PreferenceGroup( - title = stringResource(R.string.pref_category_ui_features), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - preference = store.field(SaveCaptionStyle::removeCaptions) { newValue -> - copy(removeCaptions = newValue) - }, - icon = painterResource(R.drawable.closed_caption_24px), - title = stringResource(R.string.subtitles_remove_captions), - subtitle = "[Knocking on door] Hello → Hello", - ), - Preference.PreferenceItem.SwitchPreference( - preference = store.field(SaveCaptionStyle::removeBloat) { newValue -> - copy(removeBloat = newValue) - }, - icon = painterResource(R.drawable.text_ad_off_24px), - title = stringResource(R.string.subtitles_remove_bloat), - ), - ) - ), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index 2e3829120..1377ccd08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -86,14 +86,16 @@ import com.lagradost.cloudstream3.utils.FillerEpisodeCheck.toClassDir import com.lagradost.cloudstream3.utils.JsUnpacker.Companion.load import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.downloader.DownloadObjects -import com.lagradost.cloudstream4.AppSettings import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import okhttp3.Cache import java.io.File +import java.net.URL +import java.net.URLDecoder import java.util.concurrent.Executor import java.util.concurrent.Executors + object AppContextUtils { fun RecyclerView.isRecyclerScrollable(): Boolean { val layoutManager = @@ -145,7 +147,6 @@ object AppContextUtils { text.toSpanned() } } - /** Get channel ID by name */ @SuppressLint("RestrictedApi") private fun buildWatchNextProgramUri( @@ -363,22 +364,33 @@ object AppContextUtils { } } - /** Sort subtitles by names */ fun sortSubs(subs: Set): List { - // Be aware, sorting by "$originalName $nameSuffix" causes "a (b) 1" < "a 1", - // where "originalName then nameSuffix" preserves "a 1" < "a (b) 1", because we do not compare '(' and '1'. - return subs - .sortedWith( - compareBy { subtitle: SubtitleData -> subtitle.originalName } - .thenBy { subtitle: SubtitleData -> subtitle.nameSuffix }) + return subs.sortedBy { it.name } } fun Context.getApiSettings(): HashSet { + //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = HashSet() val activeLangs = getApiProviderLangSettings() val hasUniversal = activeLangs.contains(AllLanguagesName) - hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) } + hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } .map { it.name }) + + /*val set = settingsManager.getStringSet( + this.getString(R.string.search_providers_list_key), + hashSet + )?.toHashSet() ?: hashSet + + val list = HashSet() + for (name in set) { + val api = getApiFromNameNull(name) ?: continue + if (activeLangs.contains(api.lang)) { + list.add(name) + } + }*/ + //if (list.isEmpty()) return hashSet + //return list return hashSet } @@ -469,8 +481,9 @@ object AppContextUtils { } ?: default val langs = this.getApiProviderLangSettings() val hasUniversal = langs.contains(AllLanguagesName) - val allApis = + val allApis = synchronized(apis) { apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } + } return if (currentPrefMedia.isEmpty()) { allApis } else { @@ -482,14 +495,14 @@ object AppContextUtils { fun Context.filterSearchResultByFilmQuality(data: List): List { // Filter results omitting entries with certain quality if (data.isNotEmpty()) { - val filteredSearchQuality = AppSettings(this).ui.filterQuality.get() /*PreferenceManager.getDefaultSharedPreferences(this) + val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) ?.mapNotNull { entry -> entry.toIntOrNull() ?: return@mapNotNull null - } ?: listOf()*/ + } ?: listOf() if (filteredSearchQuality.isNotEmpty()) { return data.filter { item -> - val searchQualVal = item.quality //?.ordinal ?: -1 + val searchQualVal = item.quality?.ordinal ?: -1 //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") !filteredSearchQuality.contains(searchQualVal) } @@ -501,17 +514,17 @@ object AppContextUtils { fun Context.filterHomePageListByFilmQuality(data: HomePageList): HomePageList { // Filter results omitting entries with certain quality if (data.list.isNotEmpty()) { - val filteredSearchQuality = AppSettings(this).ui.filterQuality.get() /*PreferenceManager.getDefaultSharedPreferences(this) + val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) ?.mapNotNull { entry -> entry.toIntOrNull() ?: return@mapNotNull null - } ?: listOf()*/ + } ?: listOf() if (filteredSearchQuality.isNotEmpty()) { return HomePageList( name = data.name, isHorizontalImages = data.isHorizontalImages, list = data.list.filter { item -> - val searchQualVal = item.quality //?.ordinal ?: -1 + val searchQualVal = item.quality?.ordinal ?: -1 //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") !filteredSearchQuality.contains(searchQualVal) } @@ -524,13 +537,13 @@ object AppContextUtils { fun Activity.loadRepository(url: String) { ioSafe { val repo = RepositoryManager.parseRepository(url) ?: return@ioSafe - val data = RepositoryData( - repo.iconUrl ?: "", - repo.name, - url, - repo.description + RepositoryManager.addRepository( + RepositoryData( + repo.iconUrl ?: "", + repo.name, + url + ) ) - RepositoryManager.addRepository(data) main { showToast( getString(R.string.player_loaded_subtitles, repo.name), @@ -538,12 +551,13 @@ object AppContextUtils { ) } afterRepositoryLoadedEvent.invoke(true) - addRepositoryDialog(data) + addRepositoryDialog(repo.name, url) } } fun Activity.addRepositoryDialog( - repositoryData: RepositoryData + repositoryName: String, + repositoryURL: String, ) { val repos = RepositoryManager.getRepositories() @@ -553,7 +567,9 @@ object AppContextUtils { navigate( R.id.global_to_navigation_settings_plugins, PluginsFragment.newInstance( - repositoryData, + repositoryName, + repositoryURL, + false, ) ) } @@ -561,7 +577,7 @@ object AppContextUtils { runOnUiThread { AlertDialog.Builder(this).apply { - setTitle(repositoryData.name) + setTitle(repositoryName) setMessage(R.string.download_all_plugins_from_repo) setPositiveButton(R.string.open_downloaded_repo) { _, _ -> openAddedRepo() @@ -634,17 +650,16 @@ object AppContextUtils { } } - // Deprecate after next stable - /* @Deprecated( - message = "Use splitUrlParameters instead.", - replaceWith = ReplaceWith( - expression = "splitUrlParameters(url.toString())", - imports = ["com.lagradost.cloudstream3.splitUrlParameters"], - ), - level = DeprecationLevel.WARNING, - ) */ - fun splitQuery(url: java.net.URL): Map { - return com.lagradost.cloudstream3.splitUrlParameters(url.toString()) + fun splitQuery(url: URL): Map { + val queryPairs: MutableMap = LinkedHashMap() + val query: String = url.query + val pairs = query.split("&").toTypedArray() + for (pair in pairs) { + val idx = pair.indexOf("=") + queryPairs[URLDecoder.decode(pair.substring(0, idx), "UTF-8")] = + URLDecoder.decode(pair.substring(idx + 1), "UTF-8") + } + return queryPairs } /**| S1:E2 Hello World @@ -689,7 +704,7 @@ object AppContextUtils { "$seasonNameShort${rSeason}:$episodeNameShort${rEpisode}" } else if (rEpisode != null) { "$episodeNameShort$rEpisode" - } else null + }else null } fun Activity?.loadCache() { @@ -712,7 +727,7 @@ object AppContextUtils { fun loadResult( url: String, apiName: String, - name: String, + name : String, startAction: Int = 0, startValue: Int = 0 ) { @@ -722,7 +737,7 @@ object AppContextUtils { fun FragmentActivity.loadResult( url: String, apiName: String, - name: String, + name : String, startAction: Int = 0, startValue: Int = 0 ) { @@ -848,8 +863,7 @@ object AppContextUtils { } fun Context.isUsingMobileData(): Boolean { - val connectionManager = - getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val connectionManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val activeNetwork: Network? = connectionManager.activeNetwork val networkCapabilities = connectionManager.getNetworkCapabilities(activeNetwork) @@ -902,4 +916,4 @@ object AppContextUtils { } else null return currentAudioFocusRequest } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index d0c79c43c..88cb7481c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.utils -import android.app.Activity import android.content.Context import android.net.Uri import android.widget.Toast @@ -11,6 +10,7 @@ import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.readValue import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R @@ -21,12 +21,11 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_CACHED_LIST import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_CACHED_LIST import com.lagradost.cloudstream3.syncproviders.providers.KitsuApi.Companion.KITSU_CACHED_LIST -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs +import com.lagradost.cloudstream3.utils.DataStore.mapper import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.setupStream @@ -37,8 +36,6 @@ import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.KEY_RESU import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.KEY_RESUME_PACKAGES import com.lagradost.safefile.MediaFileContentType import com.lagradost.safefile.SafeFile -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import okhttp3.internal.closeQuietly import java.io.IOException import java.io.OutputStream @@ -52,7 +49,7 @@ object BackupUtils { /** * No sensitive or breaking data in the backup - */ + * */ private val nonTransferableKeys = listOf( ANILIST_CACHED_LIST, MAL_CACHED_LIST, @@ -97,8 +94,8 @@ object BackupUtils { // Download headers are unintuitively used in the resume watching system. // We can therefore not prune download headers in backups. - // DOWNLOAD_HEADER_CACHE_BACKUP, - // DOWNLOAD_HEADER_CACHE, + //DOWNLOAD_HEADER_CACHE_BACKUP, + //DOWNLOAD_HEADER_CACHE, // This may overwrite valid local data with invalid data @@ -121,24 +118,24 @@ object BackupUtils { private var restoreFileSelector: ActivityResultLauncher>? = null // Kinda hack, but I couldn't think of a better way - @Serializable data class BackupVars( - @JsonProperty("_Bool") @SerialName("_Bool") val bool: Map?, - @JsonProperty("_Int") @SerialName("_Int") val int: Map?, - @JsonProperty("_String") @SerialName("_String") val string: Map?, - @JsonProperty("_Float") @SerialName("_Float") val float: Map?, - @JsonProperty("_Long") @SerialName("_Long") val long: Map?, - @JsonProperty("_StringSet") @SerialName("_StringSet") val stringSet: Map?>?, + @JsonProperty("_Bool") val bool: Map?, + @JsonProperty("_Int") val int: Map?, + @JsonProperty("_String") val string: Map?, + @JsonProperty("_Float") val float: Map?, + @JsonProperty("_Long") val long: Map?, + @JsonProperty("_StringSet") val stringSet: Map?>?, ) - @Serializable data class BackupFile( - @JsonProperty("datastore") @SerialName("datastore") val datastore: BackupVars, - @JsonProperty("settings") @SerialName("settings") val settings: BackupVars, + @JsonProperty("datastore") val datastore: BackupVars, + @JsonProperty("settings") val settings: BackupVars ) @Suppress("UNCHECKED_CAST") - private fun getBackup(context: Context): BackupFile { + private fun getBackup(context: Context?): BackupFile? { + if (context == null) return null + val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() } val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() } @@ -148,7 +145,7 @@ object BackupUtils { allData.filter { it.value is String } as? Map, allData.filter { it.value is Float } as? Map, allData.filter { it.value is Long } as? Map, - allData.filter { it.value as? Set != null } as? Map>, + allData.filter { it.value as? Set != null } as? Map> ) val allSettingsSorted = BackupVars( @@ -157,12 +154,12 @@ object BackupUtils { allSettings.filter { it.value is String } as? Map, allSettings.filter { it.value is Float } as? Map, allSettings.filter { it.value is Long } as? Map, - allSettings.filter { it.value as? Set != null } as? Map>, + allSettings.filter { it.value as? Set != null } as? Map> ) return BackupFile( allDataSorted, - allSettingsSorted, + allSettingsSorted ) } @@ -171,7 +168,7 @@ object BackupUtils { context: Context?, backupFile: BackupFile, restoreSettings: Boolean, - restoreDataStore: Boolean, + restoreDataStore: Boolean ) { if (context == null) return if (restoreSettings) { @@ -200,9 +197,9 @@ object BackupUtils { fun backup(context: Context?) = ioSafe { if (context == null) return@ioSafe + var fileStream: OutputStream? = null var printStream: PrintWriter? = null - try { if (!context.checkWrite()) { showToast(R.string.backup_failed, Toast.LENGTH_LONG) @@ -217,14 +214,18 @@ object BackupUtils { fileStream = stream.openNew() printStream = PrintWriter(fileStream) - printStream.print(backupFile.toJson()) - showToast(R.string.backup_success, Toast.LENGTH_LONG) + printStream.print(mapper.writeValueAsString(backupFile)) + + showToast( + R.string.backup_success, + Toast.LENGTH_LONG + ) } catch (e: Exception) { logError(e) try { showToast( txt(R.string.backup_failed_error_format, e.toString()), - Toast.LENGTH_LONG, + Toast.LENGTH_LONG ) } catch (e: Exception) { logError(e) @@ -243,7 +244,7 @@ object BackupUtils { name, folder = null, extension = ext, - tryResume = false, + tryResume = false ) } @@ -258,14 +259,14 @@ object BackupUtils { val input = activity.contentResolver.openInputStream(uri) ?: return@ioSafe - val text = input.bufferedReader().readText() - val restoredValue = parseJson(text) + val restoredValue = + mapper.readValue(input) restore( activity, restoredValue, restoreSettings = true, - restoreDataStore = true, + restoreDataStore = true ) activity.runOnUiThread { activity.recreate() } } catch (e: Exception) { @@ -283,7 +284,7 @@ object BackupUtils { } } - fun Activity.restorePrompt() { + fun FragmentActivity.restorePrompt() { runOnUiThread { try { restoreFileSelector?.launch( @@ -306,7 +307,7 @@ object BackupUtils { private fun Context.restoreMap( map: Map?, - isEditingAppSettings: Boolean = false, + isEditingAppSettings: Boolean = false ) { val editor = DataStore.editor(this, isEditingAppSettings) map?.forEach { @@ -318,27 +319,21 @@ object BackupUtils { } /** - * Copy of [com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.getDefaultDir], - * modified for backup-specific paths. - */ + * Copy of [com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.basePathToFile], [com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.getDefaultDir] and [com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.getBasePath] + * modded for backup specific paths + * */ + fun getDefaultBackupDir(context: Context): SafeFile? { return SafeFile.fromMedia(context, MediaFileContentType.Downloads) } - /** - * Copy of [com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.getBasePath], - * modified for backup-specific paths. - */ fun getCurrentBackupDir(context: Context): Pair { val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) - val basePathSetting = settingsManager.getString(context.getString(R.string.backup_path_key), null) + val basePathSetting = + settingsManager.getString(context.getString(R.string.backup_path_key), null) return baseBackupPathToFile(context, basePathSetting) to basePathSetting } - /** - * Copy of [com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.basePathToFile], - * modified for backup-specific paths. - */ private fun baseBackupPathToFile(context: Context, path: String?): SafeFile? { return when { path.isNullOrBlank() -> getDefaultBackupDir(context) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BaseComposeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BaseComposeFragment.kt deleted file mode 100644 index b11b03754..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BaseComposeFragment.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.lagradost.cloudstream3.utils - -import android.content.res.Configuration -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.Fragment -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR -import com.lagradost.cloudstream3.ui.settings.Globals.TV -import com.lagradost.cloudstream3.ui.settings.Globals.isLandscape -import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding -import com.lagradost.cloudstream4.compose.Screen -import com.lagradost.cloudstream4.compose.createComposeView - -abstract class BaseComposeFragment : Fragment(), Screen { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View = createComposeView(inflater, container, savedInstanceState) - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - this.view?.let { view -> - fixSystemBarsPadding( - view, - padLeft = isLayout(TV or EMULATOR), - padBottom = isLandscape() - ) - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - fixSystemBarsPadding( - view, - padLeft = isLayout(TV or EMULATOR), - padBottom = isLandscape() - ) - } -} - -abstract class BaseDialogComposeFragment : DialogFragment(), Screen { - var systemBarsAddPadding = isLayout(TV or EMULATOR) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View = createComposeView(inflater, container, savedInstanceState) - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - dialog?.window?.decorView?.let { view -> - fixSystemBarsPadding( - view, - padLeft = systemBarsAddPadding, - padBottom = systemBarsAddPadding || isLandscape() - ) - } - } - - override fun onStart() { - super.onStart() - dialog?.window?.setWindowAnimations(R.style.DialogFullscreenPlayer) - } - - override fun getTheme(): Int { - return R.style.DialogFullscreenPlayer - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - dialog?.window?.decorView?.let { view -> - fixSystemBarsPadding( - view, - padLeft = systemBarsAddPadding, - padBottom = systemBarsAddPadding || isLandscape() - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index 68f262d8d..0a1db85fa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -2,16 +2,17 @@ package com.lagradost.cloudstream3.utils import android.content.Context import android.content.SharedPreferences -import androidx.core.content.edit import androidx.preference.PreferenceManager +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.module.kotlin.kotlinModule import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKeyClass import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKeyClass import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJsonLiteral import kotlin.reflect.KClass import kotlin.reflect.KProperty +import androidx.core.content.edit /** Used to display metadata about downloads and resume watching */ const val DOWNLOAD_HEADER_CACHE = "download_header_cache" @@ -87,18 +88,8 @@ data class Editor( } object DataStore { - // Extensions shouldn't have really been using this version of it, but it seems - // some have. Since there has always been a very easy alternative, we won't - // need to deprecate it that long, and should be able to fully remove it - // once extensions at least use the other version. - @Deprecated( - "Please do not use the mapper version from DataStore. Preferably use methods from AppUtils " + - "to parse JSON. However, you can use the stable-API version of the mapper at " + - "com.lagradost.cloudstream3.mapper to access the mapper directly if necessary.", - level = DeprecationLevel.ERROR, - replaceWith = ReplaceWith("com.lagradost.cloudstream3.mapper"), - ) - val mapper = com.lagradost.cloudstream3.mapper + val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() private fun getPreferences(context: Context): SharedPreferences { return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) @@ -108,6 +99,7 @@ object DataStore { return getPreferences(this) } + fun getFolderName(folder: String, path: String): String { return "${folder}/${path}" } @@ -173,19 +165,19 @@ object DataStore { fun Context.setKey(path: String, value: T) { try { getSharedPrefs().edit { - putString(path, value?.toJsonLiteral()) + putString(path, mapper.writeValueAsString(value)) } } catch (e: Exception) { logError(e) } } - fun Context.getKey(path: String, valueType: Class): T? { - return try { + fun Context.getKey(path: String, valueType: Class): T? { + try { val json: String = getSharedPrefs().getString(path, null) ?: return null - parseJson(json, valueType.kotlin) - } catch (_: Exception) { - null + return json.toKotlinObject(valueType) + } catch (e: Exception) { + return null } } @@ -193,37 +185,21 @@ object DataStore { setKey(getFolderName(folder, path), value) } - @Deprecated( - message = "Use parseJson(this) directly instead.", - level = DeprecationLevel.WARNING, - replaceWith = ReplaceWith( - expression = "parseJson(this)", - imports = ["com.lagradost.cloudstream3.utils.AppUtils.parseJson"], - ), - ) inline fun String.toKotlinObject(): T { - return parseJson(this) + return mapper.readValue(this, T::class.java) } - @Deprecated( - message = "Use parseJson(this) directly instead.", - level = DeprecationLevel.WARNING, - replaceWith = ReplaceWith( - expression = "parseJson(this)", - imports = ["com.lagradost.cloudstream3.utils.AppUtils.parseJson"], - ), - ) - fun String.toKotlinObject(valueType: Class): T { - return parseJson(this, valueType.kotlin) + fun String.toKotlinObject(valueType: Class): T { + return mapper.readValue(this, valueType) } // GET KEY GIVEN PATH AND DEFAULT VALUE, NULL IF ERROR inline fun Context.getKey(path: String, defVal: T?): T? { - return try { + try { val json: String = getSharedPrefs().getString(path, null) ?: return defVal - parseJson(json) - } catch (_: Exception) { - null + return json.toKotlinObject() + } catch (e: Exception) { + return null } } @@ -238,4 +214,4 @@ object DataStore { inline fun Context.getKey(folder: String, path: String, defVal: T?): T? { return getKey(getFolderName(folder, path), defVal) ?: defVal } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index d15ea791b..19caead21 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -1,8 +1,6 @@ package com.lagradost.cloudstream3.utils import android.content.Context -import androidx.compose.runtime.Immutable -import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.CloudStreamApp.Companion.context @@ -33,14 +31,6 @@ import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.result.VideoWatchState import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia import com.lagradost.cloudstream3.utils.downloader.DownloadObjects -import com.lagradost.cloudstream3.utils.serializers.WriteOnlySerializer -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KeepGeneratedSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient import java.util.Calendar import java.util.Date import java.util.GregorianCalendar @@ -53,18 +43,17 @@ const val RESULT_WATCH_STATE = "result_watch_state" const val RESULT_WATCH_STATE_DATA = "result_watch_state_data" const val RESULT_SUBSCRIBED_STATE_DATA = "result_subscribed_state_data" const val RESULT_FAVORITES_STATE_DATA = "result_favorites_state_data" -const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // Changed due to id changes +const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // changed due to id changes const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching" const val RESULT_RESUME_WATCHING_HAS_MIGRATED = "result_resume_watching_migrated" const val RESULT_EPISODE = "result_episode" const val RESULT_SEASON = "result_season" const val RESULT_DUB = "result_dub" const val KEY_RESULT_SORT = "result_sort" -const val USER_PINNED_PROVIDERS = "user_pinned_providers" // Key for pinned user set +const val USER_PINNED_PROVIDERS = "user_pinned_providers" //key for pinned user set class UserPreferenceDelegate( - private val key: String, - private val default: T, + private val key: String, private val default: T //, private val klass: KClass ) { private val klass: KClass = default::class private val realKey get() = "${DataStoreHelper.currentAccount}/$key" @@ -74,7 +63,7 @@ class UserPreferenceDelegate( operator fun setValue( self: Any?, property: KProperty<*>, - t: T?, + t: T? ) { if (t == null) { removeKey(realKey) @@ -93,7 +82,7 @@ object DataStoreHelper { R.drawable.profile_bg_pink, R.drawable.profile_bg_purple, R.drawable.profile_bg_red, - R.drawable.profile_bg_teal, + R.drawable.profile_bg_teal ) private var searchPreferenceProvidersStrings: List by UserPreferenceDelegate( @@ -123,17 +112,16 @@ object DataStoreHelper { private var searchPreferenceTagsStrings: List by UserPreferenceDelegate( "search_pref_tags", listOf(TvType.Movie, TvType.TvSeries).map { it.name }) - var searchPreferenceTags: List get() = deserializeTv(searchPreferenceTagsStrings) set(value) { searchPreferenceTagsStrings = serializeTv(value) } + private var homePreferenceStrings: List by UserPreferenceDelegate( "home_pref_homepage", listOf(TvType.Movie, TvType.TvSeries).map { it.name }) - var homePreference: List get() = deserializeTv(homePreferenceStrings) set(value) { @@ -144,39 +132,38 @@ object DataStoreHelper { "home_bookmarked_last_list", IntArray(0) ) - var playBackSpeed: Float by UserPreferenceDelegate("playback_speed", 1.0f) var resizeMode: Int by UserPreferenceDelegate("resize_mode", 0) var librarySortingMode: Int by UserPreferenceDelegate( "library_sorting_mode", ListSorting.AlphabeticalA.ordinal ) - private var _resultsSortingMode: Int by UserPreferenceDelegate( "results_sorting_mode", EpisodeSortType.NUMBER_ASC.ordinal ) - var resultsSortingMode: EpisodeSortType get() = EpisodeSortType.entries.getOrNull(_resultsSortingMode) ?: EpisodeSortType.NUMBER_ASC set(value) { _resultsSortingMode = value.ordinal } - @Serializable - @Immutable data class Account( - @JsonProperty("keyIndex") @SerialName("keyIndex") val keyIndex: Int, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("customImage") @SerialName("customImage") val customImage: String? = null, - @JsonProperty("defaultImageIndex") @SerialName("defaultImageIndex") val defaultImageIndex: Int, - @JsonProperty("lockPin") @SerialName("lockPin") val lockPin: String? = null, + @JsonProperty("keyIndex") + val keyIndex: Int, + @JsonProperty("name") + val name: String, + @JsonProperty("customImage") + val customImage: String? = null, + @JsonProperty("defaultImageIndex") + val defaultImageIndex: Int, + @JsonProperty("lockPin") + val lockPin: String? = null, ) { - @get:JsonIgnore - val image get() = customImage?.let { UiImage.Image(it) } ?: - profileImages.getOrNull(defaultImageIndex)?.let { - UiImage.Drawable(it) - } ?: UiImage.Drawable(profileImages.first()) + val image + get() = customImage?.let { UiImage.Image(it) } ?: profileImages.getOrNull( + defaultImageIndex + )?.let { UiImage.Drawable(it) } ?: UiImage.Drawable(profileImages.first()) } const val TAG = "data_store_helper" @@ -184,16 +171,12 @@ object DataStoreHelper { var selectedKeyIndex by PreferenceDelegate("$TAG/account_key_index", 0) val currentAccount: String get() = selectedKeyIndex.toString() - private val _selectedAccountNumberFlow = MutableStateFlow(0) - /** What account instance we are on, this number changes whenever anything about local accounts changes */ - val selectedAccountNumberFlow : StateFlow = _selectedAccountNumberFlow - /** * Get or set the current account homepage. * Setting this does not automatically reload the homepage. */ var currentHomePage: String? - get() = getKey("$currentAccount/$USER_SELECTED_HOMEPAGE_API") + get() = getKey("$currentAccount/$USER_SELECTED_HOMEPAGE_API") set(value) { val key = "$currentAccount/$USER_SELECTED_HOMEPAGE_API" if (value == null) { @@ -203,13 +186,12 @@ object DataStoreHelper { } } - fun setAccount(account: Account, showToast : Boolean = true) { + fun setAccount(account: Account) { val homepage = currentHomePage + selectedKeyIndex = account.keyIndex AccountManager.updateAccountIds() - if(showToast) { - showToast(context?.getString(R.string.logged_account, account.name) ?: account.name) - } + showToast(context?.getString(R.string.logged_account, account.name) ?: account.name) MainActivity.bookmarksUpdatedEvent(true) MainActivity.reloadLibraryEvent(true) val oldAccount = accounts.find { it.keyIndex == account.keyIndex } @@ -217,7 +199,6 @@ object DataStoreHelper { // This is not a new account, and the homepage has changed, reload it MainActivity.reloadHomeEvent(true) } - _selectedAccountNumberFlow.value += 1 } fun getDefaultAccount(context: Context): Account { @@ -225,7 +206,7 @@ object DataStoreHelper { currentAccounts.getOrNull(currentAccounts.indexOfFirst { it.keyIndex == 0 }) ?: Account( keyIndex = 0, name = context.getString(R.string.default_account), - defaultImageIndex = 0, + defaultImageIndex = 0 ) } } @@ -251,21 +232,18 @@ object DataStoreHelper { } } - @Serializable data class PosDur( - @JsonProperty("position") @SerialName("position") val position: Long, - @JsonProperty("duration") @SerialName("duration") val duration: Long, + @JsonProperty("position") val position: Long, + @JsonProperty("duration") val duration: Long ) fun PosDur.fixVisual(): PosDur { if (duration <= 0) return PosDur(0, duration) val percentage = position * 100 / duration - return when { - percentage <= 1 -> PosDur(0, duration) - percentage <= 5 -> PosDur(5 * duration / 100, duration) - percentage >= 95 -> PosDur(duration, duration) - else -> this - } + if (percentage <= 1) return PosDur(0, duration) + if (percentage <= 5) return PosDur(5 * duration / 100, duration) + if (percentage >= 95) return PosDur(duration, duration) + return this } fun Int.toYear(): Date = @@ -273,38 +251,28 @@ object DataStoreHelper { /** * Used to display notifications on new episodes and posters in library. - */ - @Serializable + **/ abstract class LibrarySearchResponse( - /** - * These fields are marked @Transient because this class is only ever serialized through - * through its subclasses, which redeclare each property with their own @SerialName - * annotations. Without @Transient here, kotlinx.serialization would try to - * generate a serializer for the abstract base class itself (or double-serialize - * these fields), which fails/conflicts since these are meant to be overridden, - * not serialized directly from the parent. - */ - @Transient override var id: Int? = null, - @Transient open val latestUpdatedTime: Long = 0L, - @Transient override val name: String = "", - @Transient override val url: String = "", - @Transient override val apiName: String = "", - @Transient override var type: TvType? = null, - @Transient override var posterUrl: String? = null, - @Transient open val year: Int? = null, - @Transient open val syncData: Map? = null, - @Transient override var quality: SearchQuality? = null, - @Transient override var posterHeaders: Map? = null, - @Transient open val plot: String? = null, - @Transient override var score: Score? = null, - @Transient open val tags: List? = null, + @JsonProperty("id") override var id: Int?, + @JsonProperty("latestUpdatedTime") open val latestUpdatedTime: Long, + @JsonProperty("name") override val name: String, + @JsonProperty("url") override val url: String, + @JsonProperty("apiName") override val apiName: String, + @JsonProperty("type") override var type: TvType?, + @JsonProperty("posterUrl") override var posterUrl: String?, + @JsonProperty("year") open val year: Int?, + @JsonProperty("syncData") open val syncData: Map?, + @JsonProperty("quality") override var quality: SearchQuality?, + @JsonProperty("posterHeaders") override var posterHeaders: Map?, + @JsonProperty("plot") open val plot: String? = null, + @JsonProperty("score") override var score: Score? = null, + @JsonProperty("tags") open val tags: List? = null, ) : SearchResponse { @JsonProperty("rating", access = JsonProperty.Access.WRITE_ONLY) - @SerialName("rating") @Deprecated( "`rating` is the old scoring system, use score instead", replaceWith = ReplaceWith("score"), - level = DeprecationLevel.ERROR, + level = DeprecationLevel.ERROR ) var rating: Int? = null set(value) { @@ -315,26 +283,23 @@ object DataStoreHelper { } } - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = SubscribedData.Serializer::class) data class SubscribedData( - @JsonProperty("subscribedTime") @SerialName("subscribedTime") val subscribedTime: Long, - @JsonProperty("lastSeenEpisodeCount") @SerialName("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map, - @JsonProperty("id") @SerialName("id") override var id: Int?, - @JsonProperty("latestUpdatedTime") @SerialName("latestUpdatedTime") override val latestUpdatedTime: Long, - @JsonProperty("name") @SerialName("name") override val name: String, - @JsonProperty("url") @SerialName("url") override val url: String, - @JsonProperty("apiName") @SerialName("apiName") override val apiName: String, - @JsonProperty("type") @SerialName("type") override var type: TvType?, - @JsonProperty("posterUrl") @SerialName("posterUrl") override var posterUrl: String?, - @JsonProperty("year") @SerialName("year") override val year: Int?, - @JsonProperty("syncData") @SerialName("syncData") override val syncData: Map? = null, - @JsonProperty("quality") @SerialName("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") @SerialName("posterHeaders") override var posterHeaders: Map? = null, - @JsonProperty("plot") @SerialName("plot") override val plot: String? = null, - @JsonProperty("score") @SerialName("score") override var score: Score? = null, - @JsonProperty("tags") @SerialName("tags") override val tags: List? = null, + @JsonProperty("subscribedTime") val subscribedTime: Long, + @JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map, + override var id: Int?, + override val latestUpdatedTime: Long, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override val year: Int?, + override val syncData: Map? = null, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null, + override val plot: String? = null, + override var score: Score? = null, + override val tags: List? = null, ) : LibrarySearchResponse( id, latestUpdatedTime, @@ -349,13 +314,8 @@ object DataStoreHelper { posterHeaders, plot, score, - tags, + tags ) { - object Serializer : WriteOnlySerializer( - SubscribedData.generatedSerializer(), - setOf("rating"), - ) - fun toLibraryItem(): SyncAPI.LibraryItem? { return SyncAPI.LibraryItem( name, @@ -374,30 +334,27 @@ object DataStoreHelper { this.id, plot = this.plot, score = this.score, - tags = this.tags, + tags = this.tags ) } } - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = BookmarkedData.Serializer::class) data class BookmarkedData( - @JsonProperty("bookmarkedTime") @SerialName("bookmarkedTime") val bookmarkedTime: Long, - @JsonProperty("id") @SerialName("id") override var id: Int?, - @JsonProperty("latestUpdatedTime") @SerialName("latestUpdatedTime") override val latestUpdatedTime: Long, - @JsonProperty("name") @SerialName("name") override val name: String, - @JsonProperty("url") @SerialName("url") override val url: String, - @JsonProperty("apiName") @SerialName("apiName") override val apiName: String, - @JsonProperty("type") @SerialName("type") override var type: TvType?, - @JsonProperty("posterUrl") @SerialName("posterUrl") override var posterUrl: String?, - @JsonProperty("year") @SerialName("year") override val year: Int?, - @JsonProperty("syncData") @SerialName("syncData") override val syncData: Map? = null, - @JsonProperty("quality") @SerialName("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") @SerialName("posterHeaders") override var posterHeaders: Map? = null, - @JsonProperty("plot") @SerialName("plot") override val plot: String? = null, - @JsonProperty("score") @SerialName("score") override var score: Score? = null, - @JsonProperty("tags") @SerialName("tags") override val tags: List? = null, + @JsonProperty("bookmarkedTime") val bookmarkedTime: Long, + override var id: Int?, + override val latestUpdatedTime: Long, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override val year: Int?, + override val syncData: Map? = null, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null, + override val plot: String? = null, + override var score: Score? = null, + override val tags: List? = null, ) : LibrarySearchResponse( id, latestUpdatedTime, @@ -410,13 +367,8 @@ object DataStoreHelper { syncData, quality, posterHeaders, - plot, + plot ) { - object Serializer : WriteOnlySerializer( - BookmarkedData.generatedSerializer(), - setOf("rating"), - ) - fun toLibraryItem(id: String): SyncAPI.LibraryItem { return SyncAPI.LibraryItem( name, @@ -435,30 +387,27 @@ object DataStoreHelper { this.id, plot = this.plot, score = this.score, - tags = this.tags, + tags = this.tags ) } } - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = FavoritesData.Serializer::class) data class FavoritesData( - @JsonProperty("favoritesTime") @SerialName("favoritesTime") val favoritesTime: Long, - @JsonProperty("id") @SerialName("id") override var id: Int?, - @JsonProperty("latestUpdatedTime") @SerialName("latestUpdatedTime") override val latestUpdatedTime: Long, - @JsonProperty("name") @SerialName("name") override val name: String, - @JsonProperty("url") @SerialName("url") override val url: String, - @JsonProperty("apiName") @SerialName("apiName") override val apiName: String, - @JsonProperty("type") @SerialName("type") override var type: TvType?, - @JsonProperty("posterUrl") @SerialName("posterUrl") override var posterUrl: String?, - @JsonProperty("year") @SerialName("year") override val year: Int?, - @JsonProperty("syncData") @SerialName("syncData") override val syncData: Map? = null, - @JsonProperty("quality") @SerialName("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") @SerialName("posterHeaders") override var posterHeaders: Map? = null, - @JsonProperty("plot") @SerialName("plot") override val plot: String? = null, - @JsonProperty("score") @SerialName("score") override var score: Score? = null, - @JsonProperty("tags") @SerialName("tags") override val tags: List? = null, + @JsonProperty("favoritesTime") val favoritesTime: Long, + override var id: Int?, + override val latestUpdatedTime: Long, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override val year: Int?, + override val syncData: Map? = null, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null, + override val plot: String? = null, + override var score: Score? = null, + override val tags: List? = null, ) : LibrarySearchResponse( id, latestUpdatedTime, @@ -471,13 +420,8 @@ object DataStoreHelper { syncData, quality, posterHeaders, - plot, + plot ) { - object Serializer : WriteOnlySerializer( - FavoritesData.generatedSerializer(), - setOf("rating"), - ) - fun toLibraryItem(): SyncAPI.LibraryItem? { return SyncAPI.LibraryItem( name, @@ -496,32 +440,31 @@ object DataStoreHelper { this.id, plot = this.plot, score = this.score, - tags = this.tags, + tags = this.tags ) } } - @Serializable data class ResumeWatchingResult( - @JsonProperty("name") @SerialName("name") override val name: String, - @JsonProperty("url") @SerialName("url") override val url: String, - @JsonProperty("apiName") @SerialName("apiName") override val apiName: String, - @JsonProperty("type") @SerialName("type") override var type: TvType? = null, - @JsonProperty("posterUrl") @SerialName("posterUrl") override var posterUrl: String?, - @JsonProperty("watchPos") @SerialName("watchPos") val watchPos: PosDur?, - @JsonProperty("id") @SerialName("id") override var id: Int?, - @JsonProperty("parentId") @SerialName("parentId") val parentId: Int?, - @JsonProperty("episode") @SerialName("episode") val episode: Int?, - @JsonProperty("season") @SerialName("season") val season: Int?, - @JsonProperty("isFromDownload") @SerialName("isFromDownload") val isFromDownload: Boolean, - @JsonProperty("quality") @SerialName("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") @SerialName("posterHeaders") override var posterHeaders: Map? = null, - @JsonProperty("score") @SerialName("score") override var score: Score? = null, + @JsonProperty("name") override val name: String, + @JsonProperty("url") override val url: String, + @JsonProperty("apiName") override val apiName: String, + @JsonProperty("type") override var type: TvType? = null, + @JsonProperty("posterUrl") override var posterUrl: String?, + @JsonProperty("watchPos") val watchPos: PosDur?, + @JsonProperty("id") override var id: Int?, + @JsonProperty("parentId") val parentId: Int?, + @JsonProperty("episode") val episode: Int?, + @JsonProperty("season") val season: Int?, + @JsonProperty("isFromDownload") val isFromDownload: Boolean, + @JsonProperty("quality") override var quality: SearchQuality? = null, + @JsonProperty("posterHeaders") override var posterHeaders: Map? = null, + @JsonProperty("score") override var score: Score? = null, ) : SearchResponse /** * A datastore wide account for future implementations of a multiple account system - */ + **/ fun getAllWatchStateIds(): List? { val folder = "$currentAccount/$RESULT_WATCH_STATE" @@ -557,7 +500,7 @@ object DataStoreHelper { } fun migrateResumeWatching() { - // if (getKey(RESULT_RESUME_WATCHING_HAS_MIGRATED, false) != true) { + // if (getKey(RESULT_RESUME_WATCHING_HAS_MIGRATED, false) != true) { setKey(RESULT_RESUME_WATCHING_HAS_MIGRATED, true) getAllResumeStateIdsOld()?.forEach { id -> getLastWatchedOld(id)?.let { @@ -567,12 +510,12 @@ object DataStoreHelper { it.episode, it.season, it.isFromDownload, - it.updateTime, + it.updateTime ) removeLastWatchedOld(it.parentId) } } - // } + //} } fun setLastWatched( @@ -593,7 +536,7 @@ object DataStoreHelper { episode, season, updateTime ?: System.currentTimeMillis(), - isFromDownload, + isFromDownload ) ) } @@ -610,7 +553,7 @@ object DataStoreHelper { fun getLastWatched(id: Int?): DownloadObjects.ResumeWatching? { if (id == null) return null - return getKey( + return getKey( "$currentAccount/$RESULT_RESUME_WATCHING", id.toString(), ) @@ -618,7 +561,7 @@ object DataStoreHelper { private fun getLastWatchedOld(id: Int?): DownloadObjects.ResumeWatching? { if (id == null) return null - return getKey( + return getKey( "$currentAccount/$RESULT_RESUME_WATCHING_OLD", id.toString(), ) @@ -632,18 +575,18 @@ object DataStoreHelper { fun getBookmarkedData(id: Int?): BookmarkedData? { if (id == null) return null - return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) + return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) } fun getAllBookmarkedData(): List { return getKeys("$currentAccount/$RESULT_WATCH_STATE_DATA")?.mapNotNull { - getKey(it) + getKey(it) } ?: emptyList() } fun getAllSubscriptions(): List { return getKeys("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA")?.mapNotNull { - getKey(it) + getKey(it) } ?: emptyList() } @@ -655,12 +598,12 @@ object DataStoreHelper { /** * Set new seen episodes and update time - */ + **/ fun updateSubscribedData(id: Int?, data: SubscribedData?, episodeResponse: EpisodeResponse?) { if (id == null || data == null || episodeResponse == null) return val newData = data.copy( latestUpdatedTime = unixTimeMS, - lastSeenEpisodeCount = episodeResponse.getLatestEpisodes(), + lastSeenEpisodeCount = episodeResponse.getLatestEpisodes() ) setKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString(), newData) } @@ -673,12 +616,12 @@ object DataStoreHelper { fun getSubscribedData(id: Int?): SubscribedData? { if (id == null) return null - return getKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString()) + return getKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString()) } fun getAllFavorites(): List { return getKeys("$currentAccount/$RESULT_FAVORITES_STATE_DATA")?.mapNotNull { - getKey(it) + getKey(it) } ?: emptyList() } @@ -696,7 +639,7 @@ object DataStoreHelper { fun getFavoritesData(id: Int?): FavoritesData? { if (id == null) return null - return getKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString()) + return getKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString()) } fun setViewPos(id: Int?, pos: Long, dur: Long) { @@ -705,10 +648,10 @@ object DataStoreHelper { setKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), PosDur(pos, dur)) } - /** - * Sets the position, duration, and resume data of an episode/movie, - * If nextEpisode is not specified it will not be able to set the next episode as resumable if progress > NEXT_WATCH_EPISODE_PERCENTAGE - */ + /** Sets the position, duration, and resume data of an episode/movie, + * + * if nextEpisode is not specified it will not be able to set the next episode as resumable if progress > NEXT_WATCH_EPISODE_PERCENTAGE + * */ fun setViewPosAndResume(id: Int?, position: Long, duration: Long, currentEpisode: Any?, nextEpisode: Any?) { setViewPos(id, position, duration) if (id != null) { @@ -744,7 +687,7 @@ object DataStoreHelper { resumeMeta.id, resumeMeta.episode, resumeMeta.season, - isFromDownload = false, + isFromDownload = false ) } @@ -754,7 +697,7 @@ object DataStoreHelper { resumeMeta.id, resumeMeta.episode, resumeMeta.season, - isFromDownload = true, + isFromDownload = true ) } } @@ -763,16 +706,17 @@ object DataStoreHelper { fun getViewPos(id: Int?): PosDur? { if (id == null) return null - return getKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), null) + return getKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), null) } fun getVideoWatchState(id: Int?): VideoWatchState? { if (id == null) return null - return getKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString(), null) + return getKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString(), null) } fun setVideoWatchState(id: Int?, watchState: VideoWatchState) { if (id == null) return + // None == No key if (watchState == VideoWatchState.None) { removeKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString()) @@ -783,7 +727,7 @@ object DataStoreHelper { fun getDub(id: Int): DubStatus? { return DubStatus.entries - .getOrNull(getKey("$currentAccount/$RESULT_DUB", id.toString(), -1) ?: -1) + .getOrNull(getKey("$currentAccount/$RESULT_DUB", id.toString(), -1) ?: -1) } fun setDub(id: Int, status: DubStatus) { @@ -804,13 +748,13 @@ object DataStoreHelper { getKey( "$currentAccount/$RESULT_WATCH_STATE", id.toString(), - null, + null ) ) } fun getResultSeason(id: Int): Int? { - return getKey("$currentAccount/$RESULT_SEASON", id.toString(), null) + return getKey("$currentAccount/$RESULT_SEASON", id.toString(), null) } fun setResultSeason(id: Int, value: Int?) { @@ -818,7 +762,7 @@ object DataStoreHelper { } fun getResultEpisode(id: Int): Int? { - return getKey("$currentAccount/$RESULT_EPISODE", id.toString(), null) + return getKey("$currentAccount/$RESULT_EPISODE", id.toString(), null) } fun setResultEpisode(id: Int, value: Int?) { @@ -831,11 +775,12 @@ object DataStoreHelper { fun getSync(id: Int, idPrefixes: List): List { return idPrefixes.map { idPrefix -> - getKey("${idPrefix}_sync", id.toString()) + getKey("${idPrefix}_sync", id.toString()) } } var pinnedProviders: Array - get() = getKey>(USER_PINNED_PROVIDERS) ?: emptyArray() + get() = getKey(USER_PINNED_PROVIDERS) ?: emptyArray() set(value) = setKey(USER_PINNED_PROVIDERS, value) + } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt index 8ec46cbe5..8456094d1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt @@ -10,14 +10,12 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.getTMDbId import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.result.getId -import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.Coroutines.main -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import java.io.InputStream import java.lang.Thread.sleep import java.util.* import kotlin.concurrent.thread +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import java.io.InputStream import kotlin.let object FillerEpisodeCheck { @@ -27,45 +25,66 @@ object FillerEpisodeCheck { return q + "cache" + z } - @Serializable data class Show( - @JsonProperty("slug") @SerialName("slug") val slug: String, - @JsonProperty("title") @SerialName("title") val title: String, - @JsonProperty("filler") @SerialName("filler") val filler: ArrayList, - @JsonProperty("mixedCanon") @SerialName("mixedCanon") val mixedCanon: ArrayList, - @JsonProperty("mangaCanon") @SerialName("mangaCanon") val mangaCanon: ArrayList, - @JsonProperty("animeCanon") @SerialName("animeCanon") val animeCanon: ArrayList, + @JsonProperty("slug") + val slug: String, + @JsonProperty("title") + val title: String, + @JsonProperty("filler") + val filler: ArrayList, + @JsonProperty("mixedCanon") + val mixedCanon: ArrayList, + @JsonProperty("mangaCanon") + val mangaCanon: ArrayList, + @JsonProperty("animeCanon") + val animeCanon: ArrayList, ) - @Serializable data class MappingRoot( - @JsonProperty("type") @SerialName("type") val type: String?, - @JsonProperty("anidb_id") @SerialName("anidb_id") val anidbId: Long?, - @JsonProperty("anilist_id") @SerialName("anilist_id") val anilistId: Long?, - @JsonProperty("animecountdown_id") @SerialName("animecountdown_id") val animecountdownId: Long?, - @JsonProperty("animenewsnetwork_id") @SerialName("animenewsnetwork_id") val animenewsnetworkId: Long?, - @JsonProperty("anime-planet_id") @SerialName("anime-planet_id") val animePlanetId: String?, - @JsonProperty("anisearch_id") @SerialName("anisearch_id") val anisearchId: Long?, - @JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String?, - @JsonProperty("kitsu_id") @SerialName("kitsu_id") val kitsuId: Long?, - @JsonProperty("livechart_id") @SerialName("livechart_id") val livechartId: Long?, - @JsonProperty("mal_id") @SerialName("mal_id") val malId: Long?, - @JsonProperty("simkl_id") @SerialName("simkl_id") val simklId: Long?, - @JsonProperty("themoviedb_id") @SerialName("themoviedb_id") val themoviedbId: Long?, - @JsonProperty("tvdb_id") @SerialName("tvdb_id") val tvdbId: Long?, - @JsonProperty("season") @SerialName("season") val season: Season?, + @JsonProperty("type") + val type: String?, + @JsonProperty("anidb_id") + val anidbId: Long?, + @JsonProperty("anilist_id") + val anilistId: Long?, + @JsonProperty("animecountdown_id") + val animecountdownId: Long?, + @JsonProperty("animenewsnetwork_id") + val animenewsnetworkId: Long?, + @JsonProperty("anime-planet_id") + val animePlanetId: String?, + @JsonProperty("anisearch_id") + val anisearchId: Long?, + @JsonProperty("imdb_id") + val imdbId: String?, + @JsonProperty("kitsu_id") + val kitsuId: Long?, + @JsonProperty("livechart_id") + val livechartId: Long?, + @JsonProperty("mal_id") + val malId: Long?, + @JsonProperty("simkl_id") + val simklId: Long?, + @JsonProperty("themoviedb_id") + val themoviedbId: Long?, + @JsonProperty("tvdb_id") + val tvdbId: Long?, + @JsonProperty("season") + val season: Season?, ) - @Serializable data class Season( - @JsonProperty("tvdb") @SerialName("tvdb") val tvdb: Long?, - @JsonProperty("tmdb") @SerialName("tmdb") val tmdb: Long?, + @JsonProperty("tvdb") + val tvdb: Long?, + @JsonProperty("tmdb") + val tmdb: Long?, ) - @Serializable data class CombinedMedia( - @JsonProperty("mapping") @SerialName("mapping") val mapping: MappingRoot?, - @JsonProperty("show") @SerialName("show") val show: Show, + @JsonProperty("mapping") + val mapping: MappingRoot?, + @JsonProperty("show") + val show: Show ) data class Database( @@ -161,4 +180,4 @@ object FillerEpisodeCheck { return counter } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt index 96193fe45..9d5c75289 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.utils import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.Uri -import android.os.Build import android.os.Build.VERSION.SDK_INT import android.util.Log import android.widget.ImageView @@ -12,7 +11,6 @@ import coil3.EventListener import coil3.ImageLoader import coil3.PlatformContext import coil3.SingletonImageLoader -import coil3.decode.BitmapFactoryDecoder import coil3.disk.DiskCache import coil3.dispose import coil3.load @@ -24,86 +22,82 @@ import coil3.request.CachePolicy import coil3.request.ErrorResult import coil3.request.ImageRequest import coil3.request.allowHardware -import coil3.request.bitmapConfig import coil3.request.crossfade import coil3.util.DebugLogger import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.network.buildDefaultClient +import okhttp3.HttpUrl import okio.Path.Companion.toOkioPath import java.io.File import java.nio.ByteBuffer object ImageLoader { + private const val TAG = "CoilImgLoader" - internal fun buildImageLoader(context: PlatformContext): ImageLoader { - val isBrokenHardware = hasPotentialBrokenHardware() - return ImageLoader.Builder(context) + + internal fun buildImageLoader(context: PlatformContext): ImageLoader = ImageLoader.Builder(context) .crossfade(200) - .allowHardware(SDK_INT >= 28 && !isBrokenHardware) + .allowHardware(SDK_INT >= 28) // SDK_INT >= 28, cant use hardware bitmaps for Palette Builder .diskCachePolicy(CachePolicy.ENABLED) .networkCachePolicy(CachePolicy.ENABLED) .memoryCache { - MemoryCache.Builder().maxSizePercent(context, 0.1)//10 % of heap for mem-cache - .strongReferencesEnabled(false) + MemoryCache.Builder().maxSizePercent(context, 0.1) // Use 10 % of the app's available memory for caching .build() } .diskCache { DiskCache.Builder() .directory(context.cacheDir.resolve("cs3_image_cache").toOkioPath()) .maxSizeBytes(512L * 1024 * 1024) // 512 MB - .maxSizePercent(0.04) // max 4% of storage for disk caching + .maxSizePercent(0.04) // Use 4 % of the device's storage space for disk caching .build() } /** Pass interceptors with care, unnecessary passing tokens to servers or image hosting services causes unauthorized exceptions **/ - .components { - add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) })) - if (isBrokenHardware) { - add(BitmapFactoryDecoder.Factory()) - } // sw decoder - } - .apply { - if (isBrokenHardware) { // coil will auto choose optimal config on modern device - bitmapConfig(Bitmap.Config.ARGB_8888) - } - setupCoilLogger() + .components { add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) })) } + .also { + it.setupCoilLogger() + Log.d(TAG, "buildImageLoader: Setting COIL Image Loader.") } .build() - } - /** DebugLogger on debug builds which won't slow down release builds & use EventListener for + /** Use DebugLogger on debug builds which won't slow down release builds & use EventListener for Errors on release builds. **/ internal fun ImageLoader.Builder.setupCoilLogger() { if (BuildConfig.DEBUG) { logger(DebugLogger()) + Log.d(TAG, "setupCoilLogger: Activated DEBUG_LOGGER FOR COIL") } else { eventListener(object : EventListener() { override fun onError(request: ImageRequest, result: ErrorResult) { super.onError(request, result) - Log.e(TAG, "Image load error: ${result.throwable.message ?: result.throwable}") - Log.e(TAG, " URL: ${request.data}") - Log.e(TAG, " allowHardware: ${request.allowHardware}") - Log.e(TAG, " hardware: ${Build.HARDWARE}, board: ${Build.BOARD}") + Log.e(TAG, "Error loading image: ${result.throwable}") } }) + Log.d(TAG, "setupCoilLogger: Activated EVENT_LISTENER FOR COIL") } } - /** coil's built in loader attached w/ global synchronized instance **/ + /** we use coil's built in loader with our global synchronized instance, this way we achieve + latest and complete functionality as well as stability **/ private fun ImageView.loadImageInternal( imageData: Any?, headers: Map? = null, builder: ImageRequest.Builder.() -> Unit = {} // for placeholder, error & transformations ) { - // clear image to avoid loading & flickering issue at fast scrolling (~recycler view/lazy column) + // clear image to avoid loading & flickering issue at fast scrolling (e.g, an image recycler) this.dispose() - if (imageData == null) return + + if(imageData == null) return // Just in case + // setImageResource is better than coil3 on resources due to attr - if (imageData is Int) { - this.setImageResource(imageData); return + if(imageData is Int) { + this.setImageResource(imageData) + return } - // headers can be overridden by extensions. + + // Use Coil's built-in load method but with our custom module & a decent USER-AGENT always + // which can be overridden by extensions. this.load(imageData, SingletonImageLoader.get(context)) { this.httpHeaders(NetworkHeaders.Builder().also { headerBuilder -> headerBuilder["User-Agent"] = USER_AGENT @@ -111,22 +105,11 @@ object ImageLoader { headerBuilder[key] = value } }.build()) + builder() // if passed } } - private fun hasPotentialBrokenHardware(): Boolean { - val hardware = Build.HARDWARE?.lowercase() ?: "" - val board = Build.BOARD?.lowercase() ?: "" - val model = Build.MODEL?.lowercase() ?: "" - val manufacturer = Build.MANUFACTURER?.lowercase() ?: "" - val allwinnerPatterns = listOf("sun50iw9", "h713", "allwinner", "sunxi") - val problematicModels = - listOf("hy320", "hy300", "a10plus", "magcubic", "sinoy", "android tv box") - return allwinnerPatterns.any { it in hardware || it in board || it in manufacturer } || - problematicModels.any { it in model } - } - /** TYPE_SAFE_LOADERS **/ fun ImageView.loadImage( imageData: UiImage?, @@ -155,6 +138,12 @@ object ImageLoader { builder: ImageRequest.Builder.() -> Unit = {} ) = loadImageInternal(imageData = imageData, headers = headers, builder = builder) + fun ImageView.loadImage( + imageData: HttpUrl?, + headers: Map? = null, + builder: ImageRequest.Builder.() -> Unit = {} + ) = loadImageInternal(imageData = imageData, headers = headers, builder = builder) + fun ImageView.loadImage( imageData: File?, builder: ImageRequest.Builder.() -> Unit = {} @@ -184,4 +173,4 @@ object ImageLoader { imageData: ByteBuffer?, builder: ImageRequest.Builder.() -> Unit = {} ) = loadImageInternal(imageData = imageData, builder = builder) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index f4d6b4271..8bcd1b88e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -27,8 +27,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.GitInfo.currentCommitHash import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import okio.BufferedSink import okio.buffer import okio.sink @@ -44,43 +42,38 @@ object InAppUpdater { private const val PRERELEASE_PACKAGE_NAME = "com.lagradost.cloudstream3.prerelease" private const val LOG_TAG = "InAppUpdater" - @Serializable private data class GithubAsset( - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("size") @SerialName("size") val size: Int, // Size in bytes - @JsonProperty("browser_download_url") @SerialName("browser_download_url") val browserDownloadUrl: String, - @JsonProperty("content_type") @SerialName("content_type") val contentType: String, // application/vnd.android.package-archive + @JsonProperty("name") val name: String, + @JsonProperty("size") val size: Int, // Size in bytes + @JsonProperty("browser_download_url") val browserDownloadUrl: String, + @JsonProperty("content_type") val contentType: String, // application/vnd.android.package-archive ) - @Serializable private data class GithubRelease( - @JsonProperty("tag_name") @SerialName("tag_name") val tagName: String, // Version code - @JsonProperty("body") @SerialName("body") val body: String, // Description - @JsonProperty("assets") @SerialName("assets") val assets: List, - @JsonProperty("target_commitish") @SerialName("target_commitish") val targetCommitish: String, // Branch - @JsonProperty("prerelease") @SerialName("prerelease") val prerelease: Boolean, - @JsonProperty("node_id") @SerialName("node_id") val nodeId: String, + @JsonProperty("tag_name") val tagName: String, // Version code + @JsonProperty("body") val body: String, // Description + @JsonProperty("assets") val assets: List, + @JsonProperty("target_commitish") val targetCommitish: String, // Branch + @JsonProperty("prerelease") val prerelease: Boolean, + @JsonProperty("node_id") val nodeId: String, ) - @Serializable private data class GithubObject( - @JsonProperty("sha") @SerialName("sha") val sha: String, // SHA-256 hash - @JsonProperty("type") @SerialName("type") val type: String, - @JsonProperty("url") @SerialName("url") val url: String, + @JsonProperty("sha") val sha: String, // SHA-256 hash + @JsonProperty("type") val type: String, + @JsonProperty("url") val url: String, ) - @Serializable private data class GithubTag( - @JsonProperty("object") @SerialName("object") val githubObject: GithubObject, + @JsonProperty("object") val githubObject: GithubObject, ) - @Serializable private data class Update( - @JsonProperty("shouldUpdate") @SerialName("shouldUpdate") val shouldUpdate: Boolean, - @JsonProperty("updateURL") @SerialName("updateURL") val updateURL: String?, - @JsonProperty("updateVersion") @SerialName("updateVersion") val updateVersion: String?, - @JsonProperty("changelog") @SerialName("changelog") val changelog: String?, - @JsonProperty("updateNodeId") @SerialName("updateNodeId") val updateNodeId: String?, + @JsonProperty("shouldUpdate") val shouldUpdate: Boolean, + @JsonProperty("updateURL") val updateURL: String?, + @JsonProperty("updateVersion") val updateVersion: String?, + @JsonProperty("changelog") val changelog: String?, + @JsonProperty("updateNodeId") val updateNodeId: String?, ) private suspend fun Activity.getAppUpdate(installPrerelease: Boolean): Update { @@ -100,9 +93,9 @@ object InAppUpdater { private suspend fun Activity.getReleaseUpdate(): Update { val url = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases" val headers = mapOf("Accept" to "application/vnd.github.v3+json") - val response = parseJson>( + val response = parseJson>( app.get(url, headers = headers).text - ).toList() + ) val versionRegex = Regex("""(.*?((\d+)\.(\d+)\.(\d+))\.apk)""") val versionRegexLocal = Regex("""(.*?((\d+)\.(\d+)\.(\d+)).*)""") @@ -110,7 +103,9 @@ object InAppUpdater { !rel.prerelease }.sortedWith(compareBy { release -> release.assets.firstOrNull { it.contentType == "application/vnd.android.package-archive" }?.name?.let { it1 -> - versionRegex.find(it1)?.groupValues?.let { + versionRegex.find( + it1 + )?.groupValues?.let { it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() } } @@ -155,9 +150,9 @@ object InAppUpdater { "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/git/ref/tags/pre-release" val releaseUrl = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases" val headers = mapOf("Accept" to "application/vnd.github.v3+json") - val response = parseJson>( + val response = parseJson>( app.get(releaseUrl, headers = headers).text - ).toList() + ) val found = response.lastOrNull { rel -> rel.prerelease || rel.tagName == "pre-release" diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/Markdown.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/Markdown.kt deleted file mode 100644 index fb0b86e36..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/Markdown.kt +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Modified ver of a basic Markdown parser: - * https://github.com/DAKSHSEMWAL/mdparserkit - * **/ -package com.lagradost.cloudstream3.utils - -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.LinkAnnotation -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.withLink -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.sp - -//private val lineNumberRegex: Regex = Regex("^\\d+\\.\\s.*$") -private val boldItalicPattern = Regex("\\*\\*[*_](.*?)[*_]\\*\\*") -private val boldPattern = Regex("\\*\\*(.*?)\\*\\*") -private val italicPattern = Regex("[*_](.*?)[*_]") -private val strikethroughPattern = Regex("~~(.+?)~~") -private val linkPattern = Regex("\\[(.*?)]\\((.*?)\\)") - -/** - * Parses a given Markdown text and converts it into an [AnnotatedString] with appropriate styles. - * - * @param markdownText The input Markdown text to parse. - * @return An [AnnotatedString] with styles applied according to the Markdown syntax. - */ -fun parseMarkdown(markdownText: String): AnnotatedString { - try { - val lines = markdownText.split("\n") - val resultBuilder = AnnotatedString.Builder() - - lines.forEach { line -> - when { - // Heading 1: Extracting content, applying bold style, and appending to resultBuilder - line.startsWith("# ") -> { - textMarkDown( - line.removePrefix("# ").trim(), - resultBuilder, - fontSize = 24.sp, - fontWeight = FontWeight.Bold - ) - } - // Similar processing for Heading 2 to Heading 6 - // Heading 2 - line.startsWith("## ") -> { - textMarkDown( - line.removePrefix("## ").trim(), - resultBuilder, - fontSize = 20.sp, - fontWeight = FontWeight.Bold - ) - } - // Heading 3 - line.startsWith("### ") -> { - textMarkDown( - line.removePrefix("### ").trim(), - resultBuilder, - fontSize = 18.sp, - fontWeight = FontWeight.Bold - ) - } - // Heading 4 - line.startsWith("#### ") -> { - textMarkDown( - line.removePrefix("#### ").trim(), - resultBuilder, - fontSize = 16.sp, - fontWeight = FontWeight.Bold - ) - } - // Heading 5 - line.startsWith("##### ") -> { - textMarkDown( - line.removePrefix("##### ").trim(), - resultBuilder, - fontSize = 14.sp, - fontWeight = FontWeight.Bold - ) - } - // Heading 6 - line.startsWith("###### ") -> { - textMarkDown( - line.removePrefix("###### ").trim(), - resultBuilder, - fontSize = 12.sp, - fontWeight = FontWeight.Bold - ) - } - - // Unordered list item: Extracting content, applying bold style, appending bullet point symbol, and appending to resultBuilder - line.startsWith("* ") || line.startsWith("- ") -> { - val content = line.removePrefix("* ").removePrefix("- ").trim() - val currentStyle = SpanStyle( - fontWeight = FontWeight.Bold, - fontSize = 14.sp - ) - resultBuilder.append( - AnnotatedString("• ", currentStyle) - ) - textMarkDown(content, resultBuilder, fontSize = 14.sp) - } - // Ordered list item: Extracting content, applying bold style, appending number and period, and appending to resultBuilder - /*line.matches(lineNumberRegex) -> { - val startIndex = lineNumberRegex.find(line)?.range?.first ?: 0 - val currentStyle = SpanStyle( - fontWeight = FontWeight.Bold, - fontSize = 14.sp - ) - val annotatedString = buildAnnotatedString { - if (startIndex > 0) { - append(line.substring(0, startIndex)) - } - withStyle(currentStyle) { - append(line.substring(startIndex, startIndex + 2)) - } - } - resultBuilder.append(annotatedString) - textMarkDown( - inputText = line.substring(startIndex + 2, line.length), - resultBuilder = resultBuilder, - fontSize = 14.sp - ) - }*/ - // Remaining Text - else -> { - textMarkDown(line, resultBuilder, fontSize = 14.sp) - } - } // Appending new line - resultBuilder.append("\n") - } - return resultBuilder.toAnnotatedString().trim() as AnnotatedString - } catch (_: Throwable) { - // Just in case! We never want the UI to crash! - return AnnotatedString(text = markdownText) - } -} - -/** - * Converts markdown-style text formatting to [AnnotatedString] with appropriate [SpanStyle]s. - * - * @param inputText The input text to be converted. - * @param resultBuilder The [AnnotatedString.Builder] to append the converted text to. - * @param fontSize The desired font size for the text. - * @return The converted text with Markdown formatting replaced by appropriate [SpanStyle]s. - */ -private fun textMarkDown( - inputText: String, - resultBuilder: AnnotatedString.Builder, - fontSize: TextUnit, - fontWeight: FontWeight = FontWeight.Normal -) { - var currentIndex = 0 - - while (currentIndex < inputText.length) { - val nextBoldItalic = boldItalicPattern.find(inputText, startIndex = currentIndex) - val nextBold = boldPattern.find(inputText, startIndex = currentIndex) - val nextItalic = italicPattern.find(inputText, startIndex = currentIndex) - val nextStrikethrough = strikethroughPattern.find(inputText, startIndex = currentIndex) - val nextLink = linkPattern.find(inputText, startIndex = currentIndex) - - val nextMarkDown = listOfNotNull( - nextBoldItalic, - nextBold, - nextItalic, - nextStrikethrough, - nextLink - ).minByOrNull { it.range.first } - - if (nextMarkDown == null) { - // Append any remaining text if no more markdown found - val normalText = inputText.substring(currentIndex) - val style = SpanStyle(fontWeight = fontWeight, fontSize = fontSize) - resultBuilder.append(AnnotatedString(normalText, style)) - break - } - - if (nextMarkDown.range.first > currentIndex) { - // Append any normal text before the markdown - val normalText = inputText.substring(currentIndex, nextMarkDown.range.first) - val style = SpanStyle(fontWeight = fontWeight, fontSize = fontSize) - resultBuilder.append(AnnotatedString(normalText, style)) - } - - val matchText = nextMarkDown.groupValues.getOrNull(1) ?: "" - - val style = when (nextMarkDown) { - nextBoldItalic -> SpanStyle( - fontWeight = FontWeight.Bold, - fontStyle = FontStyle.Italic, - fontSize = fontSize - ) - - nextBold -> SpanStyle( - fontWeight = FontWeight.Bold, - fontSize = fontSize - ) - - nextItalic -> SpanStyle( - fontStyle = FontStyle.Italic, - fontSize = fontSize - ) - - nextStrikethrough -> SpanStyle( - textDecoration = TextDecoration.LineThrough, - fontSize = fontSize - ) - - nextLink -> { - resultBuilder.append(buildAnnotatedString { - withLink( - LinkAnnotation.Url( - nextMarkDown.groupValues.getOrNull(2) ?: "", - ) - ) { - append(matchText) - } - }) - currentIndex = nextMarkDown.range.last + 1 - continue - } - - else -> throw IllegalStateException("Unhandled markdown type") - } - - resultBuilder.append(AnnotatedString(matchText, style)) - - currentIndex = nextMarkDown.range.last + 1 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt index af3795834..e3c7d68df 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt @@ -63,7 +63,7 @@ object BatteryOptimizationChecker { return isRestricted && isOptimizedNotShown && isLayout(PHONE) } - fun Context.showRequestIgnoreBatteryOptDialog() { + private fun Context.showRequestIgnoreBatteryOptDialog() { try { val intent = Intent().apply { action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index 43e61ad88..351e77c8d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -5,11 +5,10 @@ package com.lagradost.cloudstream3.utils import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.APIHolder.apis +//import com.lagradost.cloudstream3.animeproviders.AniflixProvider import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import java.util.concurrent.TimeUnit object SyncUtil { @@ -25,16 +24,18 @@ object SyncUtil { private const val NINE_ANIME = "9anime" private const val TWIST_MOE = "Twistmoe" - private val matchList = mapOf( - "9anime" to NINE_ANIME, - "gogoanime" to GOGOANIME, - "gogoanimes" to GOGOANIME, - "twist.moe" to TWIST_MOE, - ) + private val matchList = + mapOf( + "9anime" to NINE_ANIME, + "gogoanime" to GOGOANIME, + "gogoanimes" to GOGOANIME, + "twist.moe" to TWIST_MOE + ) suspend fun getIdsFromUrl(url: String?): Pair? { if (url == null) return null Log.i(TAG, "getIdsFromUrl $url") + for (regex in regexs) { regex.find(url)?.let { match -> if (match.groupValues.size == 3) { @@ -55,120 +56,117 @@ object SyncUtil { } } } - return null } - /** - * first. Mal, second. Anilist, - * Valid sites are: Gogoanime, Twistmoe and 9anime - */ + /** first. Mal, second. Anilist, + * valid sites are: Gogoanime, Twistmoe and 9anime*/ private suspend fun getIdsFromSlug( slug: String, - site: String = "Gogoanime", + site: String = "Gogoanime" ): Pair? { Log.i(TAG, "getIdsFromSlug $slug $site") try { - // Gogoanime, Twistmoe and 9anime - val url = "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/pages/$site/$slug.json" + //Gogoanime, Twistmoe and 9anime + val url = + "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/pages/$site/$slug.json" val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text - val mapped = tryParseJson(response) + val mapped = parseJson(response) val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id + if (overrideMal != null) { return overrideMal.toString() to overrideAnilist?.toString() } - return null } catch (e: Exception) { logError(e) } - return null } suspend fun getUrlsFromId(id: String, type: String = "anilist"): List { - val url = "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json" + val url = + "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json" val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).parsed() val pages = response.pages ?: return emptyList() - val current = pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values) - .mapNotNull { it.url }.toMutableList() + val current = + pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values) + .mapNotNull { it.url }.toMutableList() if (type == "anilist") { // TODO MAKE BETTER - apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { - current.add("${it.mainUrl}/anime/$id") + synchronized(apis) { + apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { + current.add("${it.mainUrl}/anime/$id") + } } } - return current } - @Serializable data class SyncPage( - @JsonProperty("Pages") @SerialName("Pages") val pages: SyncPages?, + @JsonProperty("Pages") val pages: SyncPages?, ) - @Serializable data class SyncPages( - @JsonProperty("9anime") @SerialName("9anime") val nineanime: Map = emptyMap(), - @JsonProperty("Gogoanime") @SerialName("Gogoanime") val gogoanime: Map = emptyMap(), - @JsonProperty("Twistmoe") @SerialName("Twistmoe") val twistmoe: Map = emptyMap(), + @JsonProperty("9anime") val nineanime: Map = emptyMap(), + @JsonProperty("Gogoanime") val gogoanime: Map = emptyMap(), + @JsonProperty("Twistmoe") val twistmoe: Map = emptyMap(), ) - @Serializable data class ProviderPage( - @JsonProperty("url") @SerialName("url") val url: String?, + @JsonProperty("url") val url: String?, ) - @Serializable data class MalSyncPage( - @JsonProperty("identifier") @SerialName("identifier") val identifier: String?, - @JsonProperty("type") @SerialName("type") val type: String?, - @JsonProperty("page") @SerialName("page") val page: String?, - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("url") @SerialName("url") val url: String?, - @JsonProperty("image") @SerialName("image") val image: String?, - @JsonProperty("hentai") @SerialName("hentai") val hentai: Boolean?, - @JsonProperty("sticky") @SerialName("sticky") val sticky: Boolean?, - @JsonProperty("active") @SerialName("active") val active: Boolean?, - @JsonProperty("actor") @SerialName("actor") val actor: String?, - @JsonProperty("malId") @SerialName("malId") val malId: Int?, - @JsonProperty("aniId") @SerialName("aniId") val aniId: Int?, - @JsonProperty("createdAt") @SerialName("createdAt") val createdAt: String?, - @JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: String?, - @JsonProperty("deletedAt") @SerialName("deletedAt") val deletedAt: String?, - @JsonProperty("Mal") @SerialName("Mal") val mal: Mal?, - @JsonProperty("Anilist") @SerialName("Anilist") val anilist: Anilist?, - @JsonProperty("malUrl") @SerialName("malUrl") val malUrl: String?, + @JsonProperty("identifier") val identifier: String?, + @JsonProperty("type") val type: String?, + @JsonProperty("page") val page: String?, + @JsonProperty("title") val title: String?, + @JsonProperty("url") val url: String?, + @JsonProperty("image") val image: String?, + @JsonProperty("hentai") val hentai: Boolean?, + @JsonProperty("sticky") val sticky: Boolean?, + @JsonProperty("active") val active: Boolean?, + @JsonProperty("actor") val actor: String?, + @JsonProperty("malId") val malId: Int?, + @JsonProperty("aniId") val aniId: Int?, + @JsonProperty("createdAt") val createdAt: String?, + @JsonProperty("updatedAt") val updatedAt: String?, + @JsonProperty("deletedAt") val deletedAt: String?, + @JsonProperty("Mal") val mal: Mal?, + @JsonProperty("Anilist") val anilist: Anilist?, + @JsonProperty("malUrl") val malUrl: String? ) - @Serializable data class Anilist( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("malId") @SerialName("malId") val malId: Int?, - @JsonProperty("type") @SerialName("type") val type: String?, - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("url") @SerialName("url") val url: String?, - @JsonProperty("image") @SerialName("image") val image: String?, - @JsonProperty("category") @SerialName("category") val category: String?, - @JsonProperty("hentai") @SerialName("hentai") val hentai: Boolean?, - @JsonProperty("createdAt") @SerialName("createdAt") val createdAt: String?, - @JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: String?, - @JsonProperty("deletedAt") @SerialName("deletedAt") val deletedAt: String?, +// @JsonProperty("altTitle") val altTitle: List?, +// @JsonProperty("externalLinks") val externalLinks: List?, + @JsonProperty("id") val id: Int?, + @JsonProperty("malId") val malId: Int?, + @JsonProperty("type") val type: String?, + @JsonProperty("title") val title: String?, + @JsonProperty("url") val url: String?, + @JsonProperty("image") val image: String?, + @JsonProperty("category") val category: String?, + @JsonProperty("hentai") val hentai: Boolean?, + @JsonProperty("createdAt") val createdAt: String?, + @JsonProperty("updatedAt") val updatedAt: String?, + @JsonProperty("deletedAt") val deletedAt: String? ) - @Serializable data class Mal( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("type") @SerialName("type") val type: String?, - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("url") @SerialName("url") val url: String?, - @JsonProperty("image") @SerialName("image") val image: String?, - @JsonProperty("category") @SerialName("category") val category: String?, - @JsonProperty("hentai") @SerialName("hentai") val hentai: Boolean?, - @JsonProperty("createdAt") @SerialName("createdAt") val createdAt: String?, - @JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: String?, - @JsonProperty("deletedAt") @SerialName("deletedAt") val deletedAt: String?, +// @JsonProperty("altTitle") val altTitle: List?, + @JsonProperty("id") val id: Int?, + @JsonProperty("type") val type: String?, + @JsonProperty("title") val title: String?, + @JsonProperty("url") val url: String?, + @JsonProperty("image") val image: String?, + @JsonProperty("category") val category: String?, + @JsonProperty("hentai") val hentai: Boolean?, + @JsonProperty("createdAt") val createdAt: String?, + @JsonProperty("updatedAt") val updatedAt: String?, + @JsonProperty("deletedAt") val deletedAt: String? ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index 149cd52ff..91c8a2fc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -1,22 +1,9 @@ package com.lagradost.cloudstream3.utils -import com.lagradost.cloudstream3.AnimeLoadResponse -import com.lagradost.cloudstream3.LiveStreamLoadResponse -import com.lagradost.cloudstream3.MainAPI -import com.lagradost.cloudstream3.MainPageRequest -import com.lagradost.cloudstream3.MovieLoadResponse -import com.lagradost.cloudstream3.SearchResponse -import com.lagradost.cloudstream3.TvSeriesLoadResponse -import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.mvvm.getStackTracePretty +import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream4.state.Log -import com.lagradost.cloudstream4.state.LogItem -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import org.junit.Assert import kotlin.random.Random object TestingUtils { @@ -27,21 +14,39 @@ object TestingUtils { } } - data class Logger( - var messageLog: PersistentList = persistentListOf() - ) : Log { - override fun log(item: LogItem) { - messageLog = messageLog.adding(item) + class Logger { + enum class LogLevel { + Normal, + Warning, + Error; } - } - private fun fail(message: String): Nothing = throw AssertionError(message) - private fun assertTrue(message: String, condition: Boolean) { - if (!condition) fail(message) - } + data class Message(val level: LogLevel, val message: String) { + override fun toString(): String { + val level = when (this.level) { + LogLevel.Normal -> "" + LogLevel.Warning -> "Warning: " + LogLevel.Error -> "Error: " + } + return "$level$message" + } + } - private fun assertNotNull(message: String, value: Any?) { - if (value == null) fail(message) + private val messageLog = mutableListOf() + + fun getRawLog(): List = messageLog + + fun log(message: String) { + messageLog.add(Message(LogLevel.Normal, message)) + } + + fun warn(message: String) { + messageLog.add(Message(LogLevel.Warning, message)) + } + + fun error(message: String) { + messageLog.add(Message(LogLevel.Error, message)) + } } class TestResultList(val results: List) : TestResult(true) @@ -49,15 +54,16 @@ object TestingUtils { class TestResultProvider( success: Boolean, + val log: List, val exception: Throwable? ) : TestResult(success) @Throws(AssertionError::class, CancellationException::class) suspend fun testHomepage( - api: MainAPI, logger: Log + api: MainAPI, + logger: Logger ): TestResult { - val tag = "Homepage" if (api.hasMainPage) { try { val f = api.mainPage.first() @@ -65,18 +71,15 @@ object TestingUtils { api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages)) when { homepage == null -> { - logger.e(tag, "Provider ${api.name} did not correctly load homepage!") + logger.error("Provider ${api.name} did not correctly load homepage!") } homepage.items.isEmpty() -> { - logger.w(tag, "Provider ${api.name} does not contain any homepage rows!") + logger.warn("Provider ${api.name} does not contain any homepage rows!") } homepage.items.any { it.list.isEmpty() } -> { - logger.w( - tag, - "Provider ${api.name} does not have any items in a homepage row!" - ) + logger.warn("Provider ${api.name} does not have any items in a homepage row!") } } val homePageList = homepage?.items?.flatMap { it.list } ?: emptyList() @@ -84,7 +87,7 @@ object TestingUtils { } catch (e: Throwable) { when (e) { is NotImplementedError -> { - fail("Provider marked as hasMainPage, while in reality is has not been implemented") + Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") } is CancellationException -> { @@ -92,12 +95,7 @@ object TestingUtils { } else -> { - e.message?.let { - logger.w( - tag, - "Exception thrown when loading homepage: \"$it\"" - ) - } + e.message?.let { logger.warn("Exception thrown when loading homepage: \"$it\"") } } } } @@ -109,15 +107,15 @@ object TestingUtils { private suspend fun testSearch( api: MainAPI, testQueries: List, - logger: Log, + logger: Logger, ): TestResult { val searchResults = testQueries.firstNotNullOfOrNull { query -> try { - logger.i("Search", "Searching for: $query") + logger.log("Searching for: $query") api.search(query, 1)?.items?.takeIf { it.isNotEmpty() } } catch (e: Throwable) { if (e is NotImplementedError) { - fail("Provider has not implemented search()") + Assert.fail("Provider has not implemented search()") } else if (e is CancellationException) { throw e } @@ -127,7 +125,7 @@ object TestingUtils { } return if (searchResults.isNullOrEmpty()) { - fail("Api ${api.name} did not return any search responses") + Assert.fail("Api ${api.name} did not return any search responses") TestResult.Fail // Should not be reached } else { TestResultList(searchResults) @@ -139,33 +137,26 @@ object TestingUtils { private suspend fun testLoad( api: MainAPI, result: SearchResponse, - logger: Log + logger: Logger ): TestResult { - val tag = "load" try { if (result.apiName != api.name) { - logger.w(tag, "Wrong apiName on SearchResponse: ${api.name} != ${result.apiName}") + logger.warn("Wrong apiName on SearchResponse: ${api.name} != ${result.apiName}") } val loadResponse = api.load(result.url) if (loadResponse == null) { - logger.e(tag, "Returned null loadResponse on ${result.url} on ${api.name}") + logger.error("Returned null loadResponse on ${result.url} on ${api.name}") return TestResult.Fail } if (loadResponse.apiName != api.name) { - logger.w( - tag, - "Wrong apiName on LoadResponse: ${api.name} != ${loadResponse.apiName}" - ) + logger.warn("Wrong apiName on LoadResponse: ${api.name} != ${loadResponse.apiName}") } if (!api.supportedTypes.contains(loadResponse.type)) { - logger.w( - tag, - "Api ${api.name} on load does not contain any of the supportedTypes: ${loadResponse.type}" - ) + logger.warn("Api ${api.name} on load does not contain any of the supportedTypes: ${loadResponse.type}") } val url = when (loadResponse) { @@ -174,7 +165,7 @@ object TestingUtils { loadResponse.episodes.keys.isEmpty() || loadResponse.episodes.keys.any { loadResponse.episodes[it].isNullOrEmpty() } if (gotNoEpisodes) { - logger.e(tag, "Api ${api.name} got no episodes on ${loadResponse.url}") + logger.error("Api ${api.name} got no episodes on ${loadResponse.url}") return TestResult.Fail } @@ -184,7 +175,7 @@ object TestingUtils { is MovieLoadResponse -> { val gotNoEpisodes = loadResponse.dataUrl.isBlank() if (gotNoEpisodes) { - logger.e(tag, "Api ${api.name} got no movie on ${loadResponse.url}") + logger.error("Api ${api.name} got no movie on ${loadResponse.url}") return TestResult.Fail } @@ -194,7 +185,7 @@ object TestingUtils { is TvSeriesLoadResponse -> { val gotNoEpisodes = loadResponse.episodes.isEmpty() if (gotNoEpisodes) { - logger.e(tag, "Api ${api.name} got no episodes on ${loadResponse.url}") + logger.error("Api ${api.name} got no episodes on ${loadResponse.url}") return TestResult.Fail } loadResponse.episodes.firstOrNull()?.data @@ -205,7 +196,7 @@ object TestingUtils { } else -> { - logger.e(tag, "Unknown load response: ${loadResponse::class.qualifiedName}") + logger.error("Unknown load response: ${loadResponse.javaClass.name}") return TestResult.Fail } } ?: return TestResult.Fail @@ -225,7 +216,7 @@ object TestingUtils { // return TestResult(validResults) } catch (e: Throwable) { if (e is NotImplementedError) { - fail("Provider has not implemented load()") + Assert.fail("Provider has not implemented load()") } throw e } @@ -235,36 +226,35 @@ object TestingUtils { private suspend fun testLinkLoading( api: MainAPI, url: String?, - logger: Log + logger: Logger ): TestResult { - val tag = "Link" - assertNotNull("Api ${api.name} has invalid url on episode", url) + Assert.assertNotNull("Api ${api.name} has invalid url on episode", url) if (url == null) return TestResult.Fail // Should never trigger var linksLoaded = 0 try { val success = api.loadLinks(url, false, {}) { link -> - logger.i(tag, "Video loaded: ${link.name}") - assertTrue( + logger.log("Video loaded: ${link.name}") + Assert.assertTrue( "Api ${api.name} returns link with invalid url ${link.url}", link.url.length > 4 ) linksLoaded++ } if (success) { - logger.i(tag, "Links loaded: $linksLoaded") + logger.log("Links loaded: $linksLoaded") return TestResult(linksLoaded > 0) } else { - fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") + Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") } } catch (e: Throwable) { when (e) { is NotImplementedError -> { - fail("Provider has not implemented loadLinks()") + Assert.fail("Provider has not implemented loadLinks()") } else -> { - logger.e(tag, "Failed link loading on ${api.name} using data: $url") + logger.error("Failed link loading on ${api.name} using data: $url") throw e } } @@ -275,68 +265,60 @@ object TestingUtils { fun getDeferredProviderTests( scope: CoroutineScope, providers: Array, - callback: (MainAPI, TestResultProvider, Logger) -> Unit, + callback: (MainAPI, TestResultProvider) -> Unit ) { providers.forEach { api -> scope.launch { val logger = Logger() - val result = runSingleProviderTest(api, logger) - callback.invoke(api, result, logger) - } - } - } - suspend fun runSingleProviderTest(api: MainAPI, logger: Log): TestResultProvider { - val tag = "Test" - return try { - logger.i(tag, "Trying ${api.name}") + val result = try { + logger.log("Trying ${api.name}") - // Test Homepage - val homepage = testHomepage(api, logger) - assertTrue("Homepage failed to load", homepage.success) - val homePageList = (homepage as? TestResultList)?.results ?: emptyList() + // Test Homepage + val homepage = testHomepage(api, logger) + Assert.assertTrue("Homepage failed to load", homepage.success) + val homePageList = (homepage as? TestResultList)?.results ?: emptyList() - // Test Search Results - val searchQueries = - // Use the random 3 home page results as queries since they are guaranteed to exist - (homePageList.shuffled(Random).take(3).map { it.name.split(" ").first() } + - // If home page is sparse then use generic search queries - listOf("over", "iron", "guy")).take(3) + // Test Search Results + val searchQueries = + // Use the random 3 home page results as queries since they are guaranteed to exist + (homePageList.shuffled(Random).take(3).map { it.name.split(" ").first() } + + // If home page is sparse then use generic search queries + listOf("over", "iron", "guy")).take(3) - val searchResults = testSearch(api, searchQueries, logger) - assertTrue("Failed to get search results", searchResults.success) - searchResults as TestResultList + val searchResults = testSearch(api, searchQueries, logger) + Assert.assertTrue("Failed to get search results", searchResults.success) + searchResults as TestResultList - // Test Load and LoadLinks - // Only try the first 3 search results to prevent spamming - val success = searchResults.results.take(3).any { searchResponse -> - logger.i("Search", "Testing search result: ${searchResponse.url}") - val loadResponse = testLoad(api, searchResponse, logger) - if (loadResponse !is TestResultLoad) { - false - } else { - if (loadResponse.shouldLoadLinks) { - testLinkLoading(api, loadResponse.extractorData, logger).success - } else { - logger.i(tag, "Skipping link loading test") - true + // Test Load and LoadLinks + // Only try the first 3 search results to prevent spamming + val success = searchResults.results.take(3).any { searchResponse -> + logger.log("Testing search result: ${searchResponse.url}") + val loadResponse = testLoad(api, searchResponse, logger) + if (loadResponse !is TestResultLoad) { + false + } else { + if (loadResponse.shouldLoadLinks) { + testLinkLoading(api, loadResponse.extractorData, logger).success + } else { + logger.log("Skipping link loading test") + true + } + } } - } - } - if (success) { - logger.i(tag, "Success ${api.name}") - TestResultProvider(true, null) - } else { - logger.e(tag, "Link loading failed") - TestResultProvider(false, null) + if (success) { + logger.log("Success ${api.name}") + TestResultProvider(true, logger.getRawLog(), null) + } else { + logger.error("Link loading failed") + TestResultProvider(false, logger.getRawLog(), null) + } + } catch (e: Throwable) { + TestResultProvider(false, logger.getRawLog(), e) + } + callback.invoke(api, result) } - } catch (e : CancellationException) { - logger.e(tag, "Testing of ${api.name} was cancelled") - TestResultProvider(false, e) - } catch (e: Throwable) { - logger.e(tag, e.getStackTracePretty().trim()) - TestResultProvider(false, e) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TvChannelUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TvChannelUtils.kt index b5b367262..feecbe312 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TvChannelUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TvChannelUtils.kt @@ -23,15 +23,15 @@ const val PROGRAM_ID_LIST_KEY = "persistent_program_ids" object TvChannelUtils { fun Context.saveProgramId(programId: Long) { - val existing: List = getKey>(PROGRAM_ID_LIST_KEY) ?: emptyList() + val existing: List = getKey(PROGRAM_ID_LIST_KEY) ?: emptyList() val updated = (existing + programId).distinct() setKey(PROGRAM_ID_LIST_KEY, updated) } fun Context.getStoredProgramIds(): List { - return getKey>(PROGRAM_ID_LIST_KEY) ?: emptyList() + return getKey(PROGRAM_ID_LIST_KEY) ?: emptyList() } fun Context.removeProgramId(programId: Long) { - val existing: List = getKey>(PROGRAM_ID_LIST_KEY) ?: emptyList() + val existing: List = getKey(PROGRAM_ID_LIST_KEY) ?: emptyList() val updated = existing.filter { it != programId } setKey(PROGRAM_ID_LIST_KEY, updated) } @@ -149,12 +149,10 @@ object TvChannelUtils { .setInputId(inputId) .build() - val channelUri = runCatching { - context.contentResolver.insert( - TvContractCompat.Channels.CONTENT_URI, - channel.toContentValues() - ) - }.getOrNull() + val channelUri = context.contentResolver.insert( + TvContractCompat.Channels.CONTENT_URI, + channel.toContentValues() + ) channelUri?.let { val channelId = ContentUris.parseId(it) @@ -163,4 +161,4 @@ object TvChannelUtils { } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index a848bee26..c12674816 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -65,12 +65,9 @@ import androidx.navigation.fragment.NavHostFragment import androidx.palette.graphics.Palette import androidx.preference.PreferenceManager import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.button.MaterialButton import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup -import com.google.android.material.progressindicator.CircularProgressIndicatorSpec -import com.google.android.material.progressindicator.IndeterminateDrawable import com.lagradost.cloudstream3.CloudStreamApp.Companion.context import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.showToast @@ -586,43 +583,6 @@ object UIHelper { } } - /** - * Source: https://stackoverflow.com/questions/70954321/circular-progress-indicator-inside-buttons-android-material-design - * - * Shows indeterminate progress bar on this button in place of where icon would be. - * By default the tint of progress bar is the same as iconTint. - * - * @param tintColor (@ColorInt Int) Sets custom progress bar tint color. - */ - fun MaterialButton.showProgress(@ColorInt tintColor: Int = this.iconTint.defaultColor) = - // Use runOnMainThreadNative to allow process on io threads, to make the code a bit cleaner - runOnMainThreadNative { - // No need to set it again, as then it will reset the animation - if(this.icon is IndeterminateDrawable<*>) { - return@runOnMainThreadNative - } - val spec = CircularProgressIndicatorSpec( - context, null, 0, - com.google.android.material.R.style.Widget_Material3_CircularProgressIndicator_ExtraSmall - ) - - spec.indicatorColors = intArrayOf(tintColor) - - val progressIndicatorDrawable = - IndeterminateDrawable.createCircularDrawable(context, spec) - - this.icon = progressIndicatorDrawable - if (this.getTag(R.id.text1) == null) - this.setTag(R.id.text1, this.text) - this.text = "" - } - - fun MaterialButton.hideProgress() = - runOnMainThreadNative { - this.text = this.getTag(R.id.text1) as? String - this.icon = null - } - /**id, stringRes */ @SuppressLint("RestrictedApi") fun View.popupMenuNoIcons( diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt index adefedd20..12fcc0c33 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt @@ -804,7 +804,6 @@ object VideoDownloadManager { private suspend fun resolve( startByte: Long, endByte: Long?, - buffer: ByteArray, callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) ): Long = withContext(Dispatchers.IO) { var currentByte: Long = startByte @@ -823,6 +822,7 @@ object VideoDownloadManager { ) val requestStream = request.body.byteStream() + val buffer = ByteArray(bufferSize) var read: Int try { @@ -853,7 +853,6 @@ object VideoDownloadManager { suspend fun resolveSafe( index: Int, retries: Int = 3, - buffer: ByteArray, callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) ): Boolean { var start = chuckStartByte.getOrNull(index) ?: return false @@ -862,7 +861,7 @@ object VideoDownloadManager { for (i in 0 until retries) { try { // in case - start = resolve(start, end, buffer, callback) + start = resolve(start, end, callback) // no end defined, so we don't care exactly where it ended if (end == null) return true // we have download more or exactly what we needed @@ -1159,29 +1158,13 @@ object VideoDownloadManager { } } - // Reuse a download buffer to decrease unnecessary alloc - val buffer = ByteArray(items.bufferSize) - - // This will take up the first available job and resolve + // this will take up the first available job and resolve while (true) { if (!isActive) return@launch - - var isTooFarAhead = false fileMutex.withLock { if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed ) return@launch - - // Limit RAM usage by throttling if too much data is downloaded but not yet written to disk - // 50MB limit - if (metadata.bytesDownloaded - metadata.bytesWritten > 50_000_000) { - isTooFarAhead = true - } - } - - if (isTooFarAhead) { - delay(500) - continue } // mutex just in case, we never want this to fail due to multithreading @@ -1192,7 +1175,7 @@ object VideoDownloadManager { // in case something has gone wrong set to failed if the fail is not caused by // user cancellation - if (!items.resolveSafe(index, buffer = buffer, callback = callback)) { + if (!items.resolveSafe(index, callback = callback)) { fileMutex.withLock { if (metadata.type != DownloadType.IsStopped) { metadata.type = DownloadType.IsFailed @@ -1350,23 +1333,10 @@ object VideoDownloadManager { launch(Dispatchers.IO) { while (true) { if (!isActive) return@launch - - var isTooFarAhead = false fileMutex.withLock { if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed ) return@launch - - // Limit RAM usage by throttling if too much data is downloaded but not yet written to disk - // 50MB limit - if (metadata.bytesDownloaded - metadata.bytesWritten > 50_000_000) { - isTooFarAhead = true - } - } - - if (isTooFarAhead) { - delay(500) - continue } // mutex just in case, we never want this to fail due to multithreading @@ -1640,11 +1610,11 @@ object VideoDownloadManager { } fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? { - return context.getKey(KEY_RESUME_PACKAGES, id.toString()) + return context.getKey(KEY_RESUME_PACKAGES, id.toString()) } fun getDownloadQueuePackage(context: Context, id: Int): DownloadQueueWrapper? { - return context.getKey(KEY_RESUME_IN_QUEUE, id.toString()) + return context.getKey(KEY_RESUME_IN_QUEUE, id.toString()) } fun getDownloadEpisodeMetadata( diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt index 946e7ecf9..25a9fdf2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt @@ -1,32 +1,23 @@ package com.lagradost.cloudstream3.utils.downloader import android.net.Uri -import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.Score -import com.lagradost.cloudstream3.SkipSerializationTest import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.services.DownloadQueueService import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.serializers.UriSerializer -import com.lagradost.cloudstream3.utils.serializers.WriteOnlySerializer import com.lagradost.safefile.SafeFile -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KeepGeneratedSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.io.IOException import java.io.OutputStream import java.util.Objects object DownloadObjects { /** An item can either be something to resume or something new to start */ - @Serializable data class DownloadQueueWrapper( - @JsonProperty("resumePackage") @SerialName("resumePackage") val resumePackage: DownloadResumePackage?, - @JsonProperty("downloadItem") @SerialName("downloadItem") val downloadItem: DownloadQueueItem?, + @JsonProperty("resumePackage") val resumePackage: DownloadResumePackage?, + @JsonProperty("downloadItem") val downloadItem: DownloadQueueItem?, ) { init { assert(resumePackage != null || downloadItem != null) { @@ -35,66 +26,56 @@ object DownloadObjects { } /** Loop through the current download instances to see if it is currently downloading. Also includes link loading. */ - @JsonIgnore fun isCurrentlyDownloading(): Boolean { return DownloadQueueService.downloadInstances.value.any { it.downloadQueueWrapper.id == this.id } } - @JsonProperty("id") @SerialName("id") + @JsonProperty("id") val id = resumePackage?.item?.ep?.id ?: downloadItem!!.episode.id - @JsonProperty("parentId") @SerialName("parentId") + @JsonProperty("parentId") val parentId = resumePackage?.item?.ep?.parentId ?: downloadItem!!.episode.parentId } /** General data about the episode and show to start a download from. */ - @Serializable data class DownloadQueueItem( - @JsonProperty("episode") @SerialName("episode") val episode: ResultEpisode, - @JsonProperty("isMovie") @SerialName("isMovie") val isMovie: Boolean, - @JsonProperty("resultName") @SerialName("resultName") val resultName: String, - @JsonProperty("resultType") @SerialName("resultType") val resultType: TvType, - @JsonProperty("resultPoster") @SerialName("resultPoster") val resultPoster: String?, - @JsonProperty("apiName") @SerialName("apiName") val apiName: String, - @JsonProperty("resultId") @SerialName("resultId") val resultId: Int, - @JsonProperty("resultUrl") @SerialName("resultUrl") val resultUrl: String, - @JsonProperty("links") @SerialName("links") val links: List? = null, - @JsonProperty("subs") @SerialName("subs") val subs: List? = null, + @JsonProperty("episode") val episode: ResultEpisode, + @JsonProperty("isMovie") val isMovie: Boolean, + @JsonProperty("resultName") val resultName: String, + @JsonProperty("resultType") val resultType: TvType, + @JsonProperty("resultPoster") val resultPoster: String?, + @JsonProperty("apiName") val apiName: String, + @JsonProperty("resultId") val resultId: Int, + @JsonProperty("resultUrl") val resultUrl: String, + @JsonProperty("links") val links: List? = null, + @JsonProperty("subs") val subs: List? = null, ) { fun toWrapper(): DownloadQueueWrapper { return DownloadQueueWrapper(null, this) } } - interface DownloadCached { - val id: Int - } - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = DownloadEpisodeCached.Serializer::class) + abstract class DownloadCached( + @JsonProperty("id") open val id: Int, + ) + data class DownloadEpisodeCached( - @JsonProperty("name") @SerialName("name") val name: String?, - @JsonProperty("poster") @SerialName("poster") val poster: String?, - @JsonProperty("episode") @SerialName("episode") val episode: Int, - @JsonProperty("season") @SerialName("season") val season: Int?, - @JsonProperty("parentId") @SerialName("parentId") val parentId: Int, - @JsonProperty("score") @SerialName("score") var score: Score? = null, - @JsonProperty("description") @SerialName("description") val description: String?, - @JsonProperty("cacheTime") @SerialName("cacheTime") val cacheTime: Long, - @JsonProperty("id") @SerialName("id") override val id: Int, - ) : DownloadCached { - object Serializer : WriteOnlySerializer( - DownloadEpisodeCached.generatedSerializer(), - setOf("rating"), - ) - + @JsonProperty("name") val name: String?, + @JsonProperty("poster") val poster: String?, + @JsonProperty("episode") val episode: Int, + @JsonProperty("season") val season: Int?, + @JsonProperty("parentId") val parentId: Int, + @JsonProperty("score") var score: Score? = null, + @JsonProperty("description") val description: String?, + @JsonProperty("cacheTime") val cacheTime: Long, + override val id: Int, + ) : DownloadCached(id) { @JsonProperty("rating", access = JsonProperty.Access.WRITE_ONLY) - @SerialName("rating") @Deprecated( "`rating` is the old scoring system, use score instead", replaceWith = ReplaceWith("score"), - level = DeprecationLevel.ERROR, + level = DeprecationLevel.ERROR ) var rating: Int? = null set(value) { @@ -106,81 +87,74 @@ object DownloadObjects { } /** What to display to the user for a downloaded show/movie. Includes info such as name, poster and url */ - @Serializable data class DownloadHeaderCached( - @JsonProperty("apiName") @SerialName("apiName") val apiName: String, - @JsonProperty("url") @SerialName("url") val url: String, - @JsonProperty("type") @SerialName("type") val type: TvType, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("poster") @SerialName("poster") val poster: String?, - @JsonProperty("cacheTime") @SerialName("cacheTime") val cacheTime: Long, - @JsonProperty("id") @SerialName("id") override val id: Int, - ) : DownloadCached + @JsonProperty("apiName") val apiName: String, + @JsonProperty("url") val url: String, + @JsonProperty("type") val type: TvType, + @JsonProperty("name") val name: String, + @JsonProperty("poster") val poster: String?, + @JsonProperty("cacheTime") val cacheTime: Long, + override val id: Int, + ) : DownloadCached(id) - @Serializable data class DownloadResumePackage( - @JsonProperty("item") @SerialName("item") val item: DownloadItem, + @JsonProperty("item") val item: DownloadItem, /** Tills which link should get resumed */ - @JsonProperty("linkIndex") @SerialName("linkIndex") val linkIndex: Int?, + @JsonProperty("linkIndex") val linkIndex: Int?, ) { fun toWrapper(): DownloadQueueWrapper { return DownloadQueueWrapper(this, null) } } - @Serializable data class DownloadItem( - @JsonProperty("source") @SerialName("source") val source: String?, - @JsonProperty("folder") @SerialName("folder") val folder: String?, - @JsonProperty("ep") @SerialName("ep") val ep: DownloadEpisodeMetadata, - @JsonProperty("links") @SerialName("links") val links: List, + @JsonProperty("source") val source: String?, + @JsonProperty("folder") val folder: String?, + @JsonProperty("ep") val ep: DownloadEpisodeMetadata, + @JsonProperty("links") val links: List, ) /** Metadata for a specific episode and how to display it. */ - @Serializable data class DownloadEpisodeMetadata( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("parentId") @SerialName("parentId") val parentId: Int, - @JsonProperty("mainName") @SerialName("mainName") val mainName: String, - @JsonProperty("sourceApiName") @SerialName("sourceApiName") val sourceApiName: String?, - @JsonProperty("poster") @SerialName("poster") val poster: String?, - @JsonProperty("name") @SerialName("name") val name: String?, - @JsonProperty("season") @SerialName("season") val season: Int?, - @JsonProperty("episode") @SerialName("episode") val episode: Int?, - @JsonProperty("type") @SerialName("type") val type: TvType?, + @JsonProperty("id") val id: Int, + @JsonProperty("parentId") val parentId: Int, + @JsonProperty("mainName") val mainName: String, + @JsonProperty("sourceApiName") val sourceApiName: String?, + @JsonProperty("poster") val poster: String?, + @JsonProperty("name") val name: String?, + @JsonProperty("season") val season: Int?, + @JsonProperty("episode") val episode: Int?, + @JsonProperty("type") val type: TvType?, ) - @Serializable + data class DownloadedFileInfo( - @JsonProperty("totalBytes") @SerialName("totalBytes") val totalBytes: Long, - @JsonProperty("relativePath") @SerialName("relativePath") val relativePath: String, - @JsonProperty("displayName") @SerialName("displayName") val displayName: String, - @JsonProperty("extraInfo") @SerialName("extraInfo") val extraInfo: String? = null, - @JsonProperty("basePath") @SerialName("basePath") val basePath: String? = null, // null is for legacy downloads. See getBasePath() + @JsonProperty("totalBytes") val totalBytes: Long, + @JsonProperty("relativePath") val relativePath: String, + @JsonProperty("displayName") val displayName: String, + @JsonProperty("extraInfo") val extraInfo: String? = null, + @JsonProperty("basePath") val basePath: String? = null, // null is for legacy downloads. See getBasePath() // Hash of the link associated with this DownloadFile, used so not override old data in the DownloadedFileInfo - @JsonProperty("linkHash") @SerialName("linkHash") val linkHash: Int? = null, + @JsonProperty("linkHash") val linkHash : Int? = null ) - @Serializable - @SkipSerializationTest // Uri has issues with Jackson data class DownloadedFileInfoResult( - @JsonProperty("fileLength") @SerialName("fileLength") val fileLength: Long, - @JsonProperty("totalBytes") @SerialName("totalBytes") val totalBytes: Long, - @JsonProperty("path") @SerialName("path") - @Serializable(with = UriSerializer::class) - val path: Uri, + @JsonProperty("fileLength") val fileLength: Long, + @JsonProperty("totalBytes") val totalBytes: Long, + @JsonProperty("path") val path: Uri, ) - @Serializable + data class ResumeWatching( - @JsonProperty("parentId") @SerialName("parentId") val parentId: Int, - @JsonProperty("episodeId") @SerialName("episodeId") val episodeId: Int?, - @JsonProperty("episode") @SerialName("episode") val episode: Int?, - @JsonProperty("season") @SerialName("season") val season: Int?, - @JsonProperty("updateTime") @SerialName("updateTime") val updateTime: Long, - @JsonProperty("isFromDownload") @SerialName("isFromDownload") val isFromDownload: Boolean, + @JsonProperty("parentId") val parentId: Int, + @JsonProperty("episodeId") val episodeId: Int?, + @JsonProperty("episode") val episode: Int?, + @JsonProperty("season") val season: Int?, + @JsonProperty("updateTime") val updateTime: Long, + @JsonProperty("isFromDownload") val isFromDownload: Boolean, ) + data class DownloadStatus( /** if you should retry with the same args and hope for a better result */ val retrySame: Boolean, @@ -190,19 +164,20 @@ object DownloadObjects { val success: Boolean, ) + data class CreateNotificationMetadata( val type: VideoDownloadManager.DownloadType, val bytesDownloaded: Long, val bytesTotal: Long, val hlsProgress: Long? = null, val hlsTotal: Long? = null, - val bytesPerSecond: Long, + val bytesPerSecond: Long ) data class StreamData( private val fileLength: Long, val file: SafeFile, - // val fileStream: OutputStream, + //val fileStream: OutputStream, ) { @Throws(IOException::class) fun open(): OutputStream { @@ -223,11 +198,9 @@ object DownloadObjects { val exists: Boolean get() = file.exists() == true } - /** - * Bytes have the size end-start where the byte range is [start,end) - * note that ByteArray is a pointer and therefore can't be stored - * without cloning it. - */ + + /** bytes have the size end-start where the byte range is [start,end) + * note that ByteArray is a pointer and therefore cant be stored without cloning it */ data class LazyStreamDownloadResponse( val bytes: ByteArray, val startByte: Long, @@ -248,4 +221,4 @@ object DownloadObjects { return Objects.hash(startByte, endByte) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/serializers/UriSerializer.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/serializers/UriSerializer.kt deleted file mode 100644 index adceede42..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/serializers/UriSerializer.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.lagradost.cloudstream3.utils.serializers - -import android.net.Uri -import com.lagradost.cloudstream3.InternalAPI -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder - -/** - * Custom KSerializer for Android's [Uri] type. - * - * Uri is an Android platform type and cannot be annotated with @Serializable directly. - * Registering it in a SerializersModule globally would require a custom module passed to - * every Json instance, which adds hidden coupling. This serializer is also used sparingly - * across the codebase, so the overhead of a global registration isn't justified. - * Instead, we keep it explicit so that each usage site opts in intentionally and the - * serialization behavior remains visible. - * - * Usage: - * - * @Serializable - * data class MyData( - * @Serializable(with = UriSerializer::class) - * val uri: Uri, - * ) - */ -@InternalAPI -object UriSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: Uri) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): Uri { - return Uri.parse(decoder.decodeString()) - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt index 4bb78a1b0..0db90afea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt @@ -1,14 +1,12 @@ package com.lagradost.cloudstream3.utils.videoskip -import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.lagradost.cloudstream3.AnimeLoadResponse import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.ui.result.ResultEpisode -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable // taken from https://github.com/saikou-app/saikou/blob/3803f8a7a59b826ca193664d46af3a22bbc989f7/app/src/main/java/ani/saikou/others/AniSkip.kt // the following is GPLv3 code https://github.com/saikou-app/saikou/blob/main/LICENSE.md @@ -49,25 +47,22 @@ class AniSkip : SkipAPI() { } } - @Serializable data class AniSkipResponse( - @JsonProperty("found") @SerialName("found") val found: Boolean, - @JsonProperty("results") @SerialName("results") val results: List?, - @JsonProperty("message") @SerialName("message") val message: String?, - @JsonProperty("statusCode") @SerialName("statusCode") val statusCode: Int, + @JsonSerialize val found: Boolean, + @JsonSerialize val results: List?, + @JsonSerialize val message: String?, + @JsonSerialize val statusCode: Int ) - @Serializable data class Stamp( - @JsonProperty("interval") @SerialName("interval") val interval: AniSkipInterval, - @JsonProperty("skipType") @SerialName("skipType") val skipType: String, - @JsonProperty("skipId") @SerialName("skipId") val skipId: String, - @JsonProperty("episodeLength") @SerialName("episodeLength") val episodeLength: Double, + @JsonSerialize val interval: AniSkipInterval, + @JsonSerialize val skipType: String, + @JsonSerialize val skipId: String, + @JsonSerialize val episodeLength: Double ) - @Serializable data class AniSkipInterval( - @JsonProperty("startTime") @SerialName("startTime") val startTime: Double, - @JsonProperty("endTime") @SerialName("endTime") val endTime: Double, + @JsonSerialize val startTime: Double, + @JsonSerialize val endTime: Double ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AnimeSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AnimeSkip.kt index 687f746e3..f9254576b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AnimeSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AnimeSkip.kt @@ -4,7 +4,6 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.AnimeLoadResponse import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.LoadResponse -import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvSeriesLoadResponse import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app @@ -18,15 +17,12 @@ import com.lagradost.cloudstream3.syncproviders.PlainAuthRepo import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.math.BigInteger import java.util.concurrent.ConcurrentHashMap import java.security.MessageDigest class AnimeSkipAuth : AuthAPI() { override val name = "AnimeSkip" - override val icon: Int = R.drawable.animeskip override val inAppLoginRequirement: AuthLoginRequirement = AuthLoginRequirement(password = true, username = true) override val idPrefix = "anime-skip" @@ -38,51 +34,58 @@ class AnimeSkipAuth : AuthAPI() { return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0') } - @Serializable data class LoginRoot( - @JsonProperty("data") @SerialName("data") val data: LoginData, + @JsonProperty("data") + val data: LoginData, ) - @Serializable data class LoginData( - @JsonProperty("login") @SerialName("login") val login: Login, + @JsonProperty("login") + val login: Login, ) - @Serializable data class Login( - @JsonProperty("authToken") @SerialName("authToken") val authToken: String, - @JsonProperty("refreshToken") @SerialName("refreshToken") val refreshToken: String, - @JsonProperty("account") @SerialName("account") val account: Account, + @JsonProperty("authToken") + val authToken: String, + @JsonProperty("refreshToken") + val refreshToken: String, + @JsonProperty("account") + val account: Account, ) - @Serializable data class ApiRoot( - @JsonProperty("data") @SerialName("data") val data: ApiData, + @JsonProperty("data") + val data: ApiData, ) - @Serializable data class ApiData( - @JsonProperty("myApiClients") @SerialName("myApiClients") val myApiClients: List, + @JsonProperty("myApiClients") + val myApiClients: List, ) - @Serializable data class MyApiClient( - @JsonProperty("id") @SerialName("id") val id: String, + @JsonProperty("id") + val id: String, ) - @Serializable data class Account( - @JsonProperty("profileUrl") @SerialName("profileUrl") val profileUrl: String, - @JsonProperty("username") @SerialName("username") val username: String, - @JsonProperty("email") @SerialName("email") val email: String, + @JsonProperty("profileUrl") + val profileUrl: String, + @JsonProperty("username") + val username: String, + @JsonProperty("email") + val email: String, ) - @Serializable data class Payload( - @JsonProperty("profileUrl") @SerialName("profileUrl") val profileUrl: String, - @JsonProperty("username") @SerialName("username") val username: String, - @JsonProperty("email") @SerialName("email") val email: String, - @JsonProperty("clientId") @SerialName("clientId") val clientId: String, + @JsonProperty("profileUrl") + val profileUrl: String, + @JsonProperty("username") + val username: String, + @JsonProperty("email") + val email: String, + @JsonProperty("clientId") + val clientId: String, ) override suspend fun user(token: AuthToken?): AuthUser? { @@ -184,43 +187,52 @@ class AnimeSkip : SkipAPI() { name?.replace(asciiRegex, "")?.lowercase() } - @Serializable data class Root( - @JsonProperty("data") @SerialName("data") val data: Data, + @JsonProperty("data") + val data: Data, ) - @Serializable data class Data( - @JsonProperty("searchShows") @SerialName("searchShows") val searchShows: List, + @JsonProperty("searchShows") + val searchShows: List, ) - @Serializable data class SearchShow( - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("originalName") @SerialName("originalName") val originalName: String?, - @JsonProperty("seasonCount") @SerialName("seasonCount") val seasonCount: Long, - @JsonProperty("episodeCount") @SerialName("episodeCount") val episodeCount: Long, - @JsonProperty("baseDuration") @SerialName("baseDuration") val baseDuration: Double, - @JsonProperty("episodes") @SerialName("episodes") val episodes: List, + @JsonProperty("name") + val name: String, + @JsonProperty("originalName") + val originalName: String?, + @JsonProperty("seasonCount") + val seasonCount: Long, + @JsonProperty("episodeCount") + val episodeCount: Long, + @JsonProperty("baseDuration") + val baseDuration: Double, + @JsonProperty("episodes") + val episodes: List, ) - @Serializable data class Episode( - @JsonProperty("number") @SerialName("number") val number: String?, - @JsonProperty("absoluteNumber") @SerialName("absoluteNumber") val absoluteNumber: String?, - @JsonProperty("season") @SerialName("season") val season: String?, - @JsonProperty("timestamps") @SerialName("timestamps") val timestamps: List, + @JsonProperty("number") + val number: String?, + @JsonProperty("absoluteNumber") + val absoluteNumber: String?, + @JsonProperty("season") + val season: String?, + @JsonProperty("timestamps") + val timestamps: List, ) - @Serializable data class Timestamp( - @JsonProperty("at") @SerialName("at") val at: Double, - @JsonProperty("type") @SerialName("type") val type: Type, + @JsonProperty("at") + val at: Double, + @JsonProperty("type") + val type: Type, ) - @Serializable data class Type( - @JsonProperty("name") @SerialName("name") val name: String, + @JsonProperty("name") + val name: String, ) val cache: ConcurrentHashMap = ConcurrentHashMap() @@ -355,3 +367,4 @@ class AnimeSkip : SkipAPI() { return result } } + diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt index 75e22a15f..869515f43 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt @@ -6,8 +6,6 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.ui.result.ResultEpisode -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable class IntroDbSkip : SkipAPI() { override val name = "IntroDb" @@ -57,24 +55,23 @@ class IntroDbSkip : SkipAPI() { ) } - @Serializable + data class IntroDbResponse( - @JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String?, - @JsonProperty("season") @SerialName("season") val season: Int?, - @JsonProperty("episode") @SerialName("episode") val episode: Int?, - @JsonProperty("intro") @SerialName("intro") val intro: Segment?, - @JsonProperty("recap") @SerialName("recap") val recap: Segment?, - @JsonProperty("outro") @SerialName("outro") val outro: Segment?, + @JsonProperty("imdb_id") val imdbId: String?, + val season: Int?, + val episode: Int?, + val intro: Segment?, + val recap: Segment?, + val outro: Segment?, ) - @Serializable data class Segment( - @JsonProperty("start_sec") @SerialName("start_sec") val startSec: Double?, - @JsonProperty("end_sec") @SerialName("end_sec") val endSec: Double?, - @JsonProperty("start_ms") @SerialName("start_ms") val startMs: Long?, - @JsonProperty("end_ms") @SerialName("end_ms") val endMs: Long?, - @JsonProperty("confidence") @SerialName("confidence") val confidence: Double?, - @JsonProperty("submission_count") @SerialName("submission_count") val submissionCount: Int?, - @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String?, + @JsonProperty("start_sec") val startSec: Double?, + @JsonProperty("end_sec") val endSec: Double?, + @JsonProperty("start_ms") val startMs: Long?, + @JsonProperty("end_ms") val endMs: Long?, + val confidence: Double?, + @JsonProperty("submission_count") val submissionCount: Int?, + @JsonProperty("updated_at") val updatedAt: String?, ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt index 642a3bae8..cc2661cb0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt @@ -8,8 +8,6 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.app -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable /** https://theintrodb.org/docs */ class TheIntroDBSkip : SkipAPI() { @@ -32,10 +30,10 @@ class TheIntroDBSkip : SkipAPI() { ?: return null val url = if (data.isMovie()) { - "$mainUrl/v4/media?$idSuffix" + "$mainUrl/v2/media?$idSuffix" } else { val season = episode.season ?: return null - "$mainUrl/v4/media?$idSuffix&season=$season&episode=${episode.episode}" + "$mainUrl/v2/media?$idSuffix&season=$season&episode=${episode.episode}" } val root = app.get(url).parsed() return arrayOf( @@ -43,7 +41,7 @@ class TheIntroDBSkip : SkipAPI() { root.credits to SkipType.Credits, root.recap to SkipType.Recap, root.preview to SkipType.Preview - ).flatMap { (list, type) -> + ).map { (list, type) -> list.map { stamp -> SkipStamp( type, @@ -51,21 +49,28 @@ class TheIntroDBSkip : SkipAPI() { stamp.endMs ?: episodeDurationMs ) } - } + }.flatten() } - @Serializable data class Root( - @JsonProperty("type") @SerialName("type") val type: String, - @JsonProperty("intro") @SerialName("intro") val intro: List = emptyList(), - @JsonProperty("recap") @SerialName("recap") val recap: List = emptyList(), - @JsonProperty("credits") @SerialName("credits") val credits: List = emptyList(), - @JsonProperty("preview") @SerialName("preview") val preview: List = emptyList(), + @JsonProperty("tmdb_id") + val tmdbId: Long, + @JsonProperty("type") + val type: String, + @JsonProperty("intro") + val intro: List = emptyList(), + @JsonProperty("recap") + val recap: List = emptyList(), + @JsonProperty("credits") + val credits: List = emptyList(), + @JsonProperty("preview") + val preview: List = emptyList(), ) - @Serializable data class Stamp( - @JsonProperty("start_ms") @SerialName("start_ms") val startMs: Long?, - @JsonProperty("end_ms") @SerialName("end_ms") val endMs: Long?, + @JsonProperty("start_ms") + val startMs: Long?, + @JsonProperty("end_ms") + val endMs: Long?, ) -} +} \ No newline at end of file diff --git a/app/src/main/res/color/account_edit_btn_tint.xml b/app/src/main/res/color/account_edit_btn_tint.xml deleted file mode 100644 index 7205c77fc..000000000 --- a/app/src/main/res/color/account_edit_btn_tint.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/add_24px.xml b/app/src/main/res/drawable/add_24px.xml deleted file mode 100644 index fe955f0ab..000000000 --- a/app/src/main/res/drawable/add_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/add_photo_alternate_24px.xml b/app/src/main/res/drawable/add_photo_alternate_24px.xml deleted file mode 100644 index de281a3c0..000000000 --- a/app/src/main/res/drawable/add_photo_alternate_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/arrow_circle_down_24px.xml b/app/src/main/res/drawable/arrow_circle_down_24px.xml deleted file mode 100644 index 03edb80d2..000000000 --- a/app/src/main/res/drawable/arrow_circle_down_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/article_24px.xml b/app/src/main/res/drawable/article_24px.xml deleted file mode 100644 index 5b0f45801..000000000 --- a/app/src/main/res/drawable/article_24px.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/aspect_ratio_24px.xml b/app/src/main/res/drawable/aspect_ratio_24px.xml deleted file mode 100644 index 429ed7237..000000000 --- a/app/src/main/res/drawable/aspect_ratio_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/audio_capture_24px.xml b/app/src/main/res/drawable/audio_capture_24px.xml deleted file mode 100644 index a9ed40e02..000000000 --- a/app/src/main/res/drawable/audio_capture_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/autorenew_24px.xml b/app/src/main/res/drawable/autorenew_24px.xml deleted file mode 100644 index 4ac69c08f..000000000 --- a/app/src/main/res/drawable/autorenew_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/battery_alert_24px.xml b/app/src/main/res/drawable/battery_alert_24px.xml deleted file mode 100644 index 3675de44d..000000000 --- a/app/src/main/res/drawable/battery_alert_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/border_color_24px.xml b/app/src/main/res/drawable/border_color_24px.xml deleted file mode 100644 index f4a5bbf3b..000000000 --- a/app/src/main/res/drawable/border_color_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/build_24px.xml b/app/src/main/res/drawable/build_24px.xml deleted file mode 100644 index 0616691da..000000000 --- a/app/src/main/res/drawable/build_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/cast.xml b/app/src/main/res/drawable/cast.xml deleted file mode 100644 index 7600cb706..000000000 --- a/app/src/main/res/drawable/cast.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/close_24px.xml b/app/src/main/res/drawable/close_24px.xml deleted file mode 100644 index 77be1d01e..000000000 --- a/app/src/main/res/drawable/close_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/closed_caption_24px.xml b/app/src/main/res/drawable/closed_caption_24px.xml deleted file mode 100644 index 793e1e061..000000000 --- a/app/src/main/res/drawable/closed_caption_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/codeberg.xml b/app/src/main/res/drawable/codeberg.xml deleted file mode 100644 index 9ac01ce95..000000000 --- a/app/src/main/res/drawable/codeberg.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/colors_24px.xml b/app/src/main/res/drawable/colors_24px.xml deleted file mode 100644 index d86304cf9..000000000 --- a/app/src/main/res/drawable/colors_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/construction_24px.xml b/app/src/main/res/drawable/construction_24px.xml deleted file mode 100644 index 00c976c90..000000000 --- a/app/src/main/res/drawable/construction_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/copy_all_24px.xml b/app/src/main/res/drawable/copy_all_24px.xml deleted file mode 100644 index 8e4db679b..000000000 --- a/app/src/main/res/drawable/copy_all_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/delete_24px.xml b/app/src/main/res/drawable/delete_24px.xml deleted file mode 100644 index 237e75aa6..000000000 --- a/app/src/main/res/drawable/delete_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/delete_sweep_24px.xml b/app/src/main/res/drawable/delete_sweep_24px.xml deleted file mode 100644 index 220f68509..000000000 --- a/app/src/main/res/drawable/delete_sweep_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/description_24px.xml b/app/src/main/res/drawable/description_24px.xml deleted file mode 100644 index f55b9ad85..000000000 --- a/app/src/main/res/drawable/description_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/disabled_by_default_24px.xml b/app/src/main/res/drawable/disabled_by_default_24px.xml deleted file mode 100644 index bc935c293..000000000 --- a/app/src/main/res/drawable/disabled_by_default_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/dns_24px.xml b/app/src/main/res/drawable/dns_24px.xml deleted file mode 100644 index 2f9b4c8bf..000000000 --- a/app/src/main/res/drawable/dns_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/edit_24px.xml b/app/src/main/res/drawable/edit_24px.xml deleted file mode 100644 index 4d1d11fac..000000000 --- a/app/src/main/res/drawable/edit_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/encrypted_24px.xml b/app/src/main/res/drawable/encrypted_24px.xml deleted file mode 100644 index 66231650a..000000000 --- a/app/src/main/res/drawable/encrypted_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/error_24px.xml b/app/src/main/res/drawable/error_24px.xml deleted file mode 100644 index 31feeeca1..000000000 --- a/app/src/main/res/drawable/error_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/extension_24px.xml b/app/src/main/res/drawable/extension_24px.xml deleted file mode 100644 index ce2cfc260..000000000 --- a/app/src/main/res/drawable/extension_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/extension_off_24px.xml b/app/src/main/res/drawable/extension_off_24px.xml deleted file mode 100644 index 6386d44ee..000000000 --- a/app/src/main/res/drawable/extension_off_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/extention_download.xml b/app/src/main/res/drawable/extention_download.xml deleted file mode 100644 index 97f3dbd67..000000000 --- a/app/src/main/res/drawable/extention_download.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/extention_renew.xml b/app/src/main/res/drawable/extention_renew.xml deleted file mode 100644 index a9e6e07f9..000000000 --- a/app/src/main/res/drawable/extention_renew.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/extention_renew2.xml b/app/src/main/res/drawable/extention_renew2.xml deleted file mode 100644 index d34e91355..000000000 --- a/app/src/main/res/drawable/extention_renew2.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/face_24px.xml b/app/src/main/res/drawable/face_24px.xml deleted file mode 100644 index 152c278e7..000000000 --- a/app/src/main/res/drawable/face_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/filter_alt_24px.xml b/app/src/main/res/drawable/filter_alt_24px.xml deleted file mode 100644 index 0afa9b51d..000000000 --- a/app/src/main/res/drawable/filter_alt_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/folder_24px.xml b/app/src/main/res/drawable/folder_24px.xml deleted file mode 100644 index a1e978b69..000000000 --- a/app/src/main/res/drawable/folder_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/font_download_24px.xml b/app/src/main/res/drawable/font_download_24px.xml deleted file mode 100644 index 82f04c483..000000000 --- a/app/src/main/res/drawable/font_download_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/format_align_center_24px.xml b/app/src/main/res/drawable/format_align_center_24px.xml deleted file mode 100644 index 8a425daa4..000000000 --- a/app/src/main/res/drawable/format_align_center_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/format_bold_24px.xml b/app/src/main/res/drawable/format_bold_24px.xml deleted file mode 100644 index 61aa7f509..000000000 --- a/app/src/main/res/drawable/format_bold_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/format_color_fill_24px.xml b/app/src/main/res/drawable/format_color_fill_24px.xml deleted file mode 100644 index 3dc2047e9..000000000 --- a/app/src/main/res/drawable/format_color_fill_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/format_color_text_24px.xml b/app/src/main/res/drawable/format_color_text_24px.xml deleted file mode 100644 index e1d88be33..000000000 --- a/app/src/main/res/drawable/format_color_text_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/format_italic_24px.xml b/app/src/main/res/drawable/format_italic_24px.xml deleted file mode 100644 index 0f69910e8..000000000 --- a/app/src/main/res/drawable/format_italic_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/format_paint_24px.xml b/app/src/main/res/drawable/format_paint_24px.xml deleted file mode 100644 index b27cf246e..000000000 --- a/app/src/main/res/drawable/format_paint_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/hard_drive_24px.xml b/app/src/main/res/drawable/hard_drive_24px.xml deleted file mode 100644 index 45fecb4d2..000000000 --- a/app/src/main/res/drawable/hard_drive_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/high_res_24px.xml b/app/src/main/res/drawable/high_res_24px.xml deleted file mode 100644 index 183f2ae69..000000000 --- a/app/src/main/res/drawable/high_res_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/history_toggle_off_24px.xml b/app/src/main/res/drawable/history_toggle_off_24px.xml deleted file mode 100644 index 30186016c..000000000 --- a/app/src/main/res/drawable/history_toggle_off_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_baseline_clock_24.xml b/app/src/main/res/drawable/ic_baseline_clock_24.xml deleted file mode 100644 index 3436c6208..000000000 --- a/app/src/main/res/drawable/ic_baseline_clock_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_compressor_24.xml b/app/src/main/res/drawable/ic_compressor_24.xml deleted file mode 100644 index 0d3945f18..000000000 --- a/app/src/main/res/drawable/ic_compressor_24.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/imagesearch_roller_24px.xml b/app/src/main/res/drawable/imagesearch_roller_24px.xml deleted file mode 100644 index 184f6f60a..000000000 --- a/app/src/main/res/drawable/imagesearch_roller_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/keyboard_arrow_left_24px.xml b/app/src/main/res/drawable/keyboard_arrow_left_24px.xml deleted file mode 100644 index b4b89a922..000000000 --- a/app/src/main/res/drawable/keyboard_arrow_left_24px.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/keyboard_double_arrow_right_24px.xml b/app/src/main/res/drawable/keyboard_double_arrow_right_24px.xml deleted file mode 100644 index 2e564cea4..000000000 --- a/app/src/main/res/drawable/keyboard_double_arrow_right_24px.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/label_24px.xml b/app/src/main/res/drawable/label_24px.xml deleted file mode 100644 index fb1d1b506..000000000 --- a/app/src/main/res/drawable/label_24px.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/language_download.xml b/app/src/main/res/drawable/language_download.xml deleted file mode 100644 index 129dd4eea..000000000 --- a/app/src/main/res/drawable/language_download.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/language_download2.xml b/app/src/main/res/drawable/language_download2.xml deleted file mode 100644 index 30cb8bf13..000000000 --- a/app/src/main/res/drawable/language_download2.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/language_korean_latin_24px.xml b/app/src/main/res/drawable/language_korean_latin_24px.xml deleted file mode 100644 index 55bf0cf21..000000000 --- a/app/src/main/res/drawable/language_korean_latin_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/light_mode_24px.xml b/app/src/main/res/drawable/light_mode_24px.xml deleted file mode 100644 index 8d278ec46..000000000 --- a/app/src/main/res/drawable/light_mode_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/lock_icon_bg.xml b/app/src/main/res/drawable/lock_icon_bg.xml deleted file mode 100644 index 7d5a233e7..000000000 --- a/app/src/main/res/drawable/lock_icon_bg.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/match_word_24px.xml b/app/src/main/res/drawable/match_word_24px.xml deleted file mode 100644 index d2a7334f8..000000000 --- a/app/src/main/res/drawable/match_word_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/memory_24px.xml b/app/src/main/res/drawable/memory_24px.xml deleted file mode 100644 index 7e4dd5e9b..000000000 --- a/app/src/main/res/drawable/memory_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/memory_alt_24px.xml b/app/src/main/res/drawable/memory_alt_24px.xml deleted file mode 100644 index 21807a9e9..000000000 --- a/app/src/main/res/drawable/memory_alt_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/mobile_arrow_down_24px.xml b/app/src/main/res/drawable/mobile_arrow_down_24px.xml deleted file mode 100644 index caab6850e..000000000 --- a/app/src/main/res/drawable/mobile_arrow_down_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/mobile_code_24px.xml b/app/src/main/res/drawable/mobile_code_24px.xml deleted file mode 100644 index 9b78af094..000000000 --- a/app/src/main/res/drawable/mobile_code_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/mobile_wrench_24px.xml b/app/src/main/res/drawable/mobile_wrench_24px.xml deleted file mode 100644 index 5cc975528..000000000 --- a/app/src/main/res/drawable/mobile_wrench_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/movie_edit_24px.xml b/app/src/main/res/drawable/movie_edit_24px.xml deleted file mode 100644 index 41df964e6..000000000 --- a/app/src/main/res/drawable/movie_edit_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/movie_info_24px.xml b/app/src/main/res/drawable/movie_info_24px.xml deleted file mode 100644 index 7d7dfe9e9..000000000 --- a/app/src/main/res/drawable/movie_info_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/notifications_active_24px.xml b/app/src/main/res/drawable/notifications_active_24px.xml deleted file mode 100644 index b3982c522..000000000 --- a/app/src/main/res/drawable/notifications_active_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/outline_drawable_forced_round.xml b/app/src/main/res/drawable/outline_drawable_forced_round.xml index e028ef10a..7736f088a 100644 --- a/app/src/main/res/drawable/outline_drawable_forced_round.xml +++ b/app/src/main/res/drawable/outline_drawable_forced_round.xml @@ -1,8 +1,5 @@ - - - - - + \ No newline at end of file diff --git a/app/src/main/res/drawable/oval_stroke_focus.xml b/app/src/main/res/drawable/oval_stroke_focus.xml deleted file mode 100644 index a2af717f1..000000000 --- a/app/src/main/res/drawable/oval_stroke_focus.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/palette_24px.xml b/app/src/main/res/drawable/palette_24px.xml deleted file mode 100644 index f701027cc..000000000 --- a/app/src/main/res/drawable/palette_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/pastebin.xml b/app/src/main/res/drawable/pastebin.xml deleted file mode 100644 index 9a2d5cd97..000000000 --- a/app/src/main/res/drawable/pastebin.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/pause_24px.xml b/app/src/main/res/drawable/pause_24px.xml deleted file mode 100644 index e8de0339c..000000000 --- a/app/src/main/res/drawable/pause_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/picture_in_picture_center_24px.xml b/app/src/main/res/drawable/picture_in_picture_center_24px.xml deleted file mode 100644 index 60d7e133b..000000000 --- a/app/src/main/res/drawable/picture_in_picture_center_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/pip_24px.xml b/app/src/main/res/drawable/pip_24px.xml deleted file mode 100644 index 5fba5b85e..000000000 --- a/app/src/main/res/drawable/pip_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/play_arrow_24px.xml b/app/src/main/res/drawable/play_arrow_24px.xml deleted file mode 100644 index b7b67fccc..000000000 --- a/app/src/main/res/drawable/play_arrow_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/plugin_lang.xml b/app/src/main/res/drawable/plugin_lang.xml deleted file mode 100644 index ac33489ac..000000000 --- a/app/src/main/res/drawable/plugin_lang.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/quick_novel_icon.xml b/app/src/main/res/drawable/quick_novel_icon.xml index 8f0b3cf3e..b19aaafea 100644 --- a/app/src/main/res/drawable/quick_novel_icon.xml +++ b/app/src/main/res/drawable/quick_novel_icon.xml @@ -1,23 +1,31 @@ - - - - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="108" + android:viewportHeight="108" + android:name="vector" + android:tint="?attr/white" +> + + + + + + - + \ No newline at end of file diff --git a/app/src/main/res/drawable/reset_colors_24px.xml b/app/src/main/res/drawable/reset_colors_24px.xml deleted file mode 100644 index 6cfa81413..000000000 --- a/app/src/main/res/drawable/reset_colors_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/responsive_layout_24px.xml b/app/src/main/res/drawable/responsive_layout_24px.xml deleted file mode 100644 index 4d3ea085f..000000000 --- a/app/src/main/res/drawable/responsive_layout_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/restore_page_24px.xml b/app/src/main/res/drawable/restore_page_24px.xml deleted file mode 100644 index 65a7d274f..000000000 --- a/app/src/main/res/drawable/restore_page_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/rounded_corner_24px.xml b/app/src/main/res/drawable/rounded_corner_24px.xml deleted file mode 100644 index 6d3180c42..000000000 --- a/app/src/main/res/drawable/rounded_corner_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/rounded_outline.xml b/app/src/main/res/drawable/rounded_outline.xml index 752cd7823..b85ace8ea 100644 --- a/app/src/main/res/drawable/rounded_outline.xml +++ b/app/src/main/res/drawable/rounded_outline.xml @@ -1,11 +1,13 @@ - - - - + + - + + - + diff --git a/app/src/main/res/drawable/save_as_24px.xml b/app/src/main/res/drawable/save_as_24px.xml deleted file mode 100644 index 0c3c01414..000000000 --- a/app/src/main/res/drawable/save_as_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/save_clock_24px.xml b/app/src/main/res/drawable/save_clock_24px.xml deleted file mode 100644 index c86924cc1..000000000 --- a/app/src/main/res/drawable/save_clock_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/screen_rotation_alt_24px.xml b/app/src/main/res/drawable/screen_rotation_alt_24px.xml deleted file mode 100644 index a1d01a5c6..000000000 --- a/app/src/main/res/drawable/screen_rotation_alt_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/shadow_24px.xml b/app/src/main/res/drawable/shadow_24px.xml deleted file mode 100644 index 09bf8d62e..000000000 --- a/app/src/main/res/drawable/shadow_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/shadow_add_24px.xml b/app/src/main/res/drawable/shadow_add_24px.xml deleted file mode 100644 index 9ff30d54c..000000000 --- a/app/src/main/res/drawable/shadow_add_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/shuffle_24px.xml b/app/src/main/res/drawable/shuffle_24px.xml deleted file mode 100644 index d25985f8b..000000000 --- a/app/src/main/res/drawable/shuffle_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/skip_next_24px.xml b/app/src/main/res/drawable/skip_next_24px.xml deleted file mode 100644 index 3beb9c760..000000000 --- a/app/src/main/res/drawable/skip_next_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/subtitles_gear_24px.xml b/app/src/main/res/drawable/subtitles_gear_24px.xml deleted file mode 100644 index d84e9f883..000000000 --- a/app/src/main/res/drawable/subtitles_gear_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/swipe_24px.xml b/app/src/main/res/drawable/swipe_24px.xml deleted file mode 100644 index 9b2183713..000000000 --- a/app/src/main/res/drawable/swipe_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/swipe_vertical_24px.xml b/app/src/main/res/drawable/swipe_vertical_24px.xml deleted file mode 100644 index 48048f334..000000000 --- a/app/src/main/res/drawable/swipe_vertical_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/text_ad_off_24px.xml b/app/src/main/res/drawable/text_ad_off_24px.xml deleted file mode 100644 index 2a108446b..000000000 --- a/app/src/main/res/drawable/text_ad_off_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/text_fields_24px.xml b/app/src/main/res/drawable/text_fields_24px.xml deleted file mode 100644 index 49153d403..000000000 --- a/app/src/main/res/drawable/text_fields_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/text_select_move_up_24px.xml b/app/src/main/res/drawable/text_select_move_up_24px.xml deleted file mode 100644 index 7311798a9..000000000 --- a/app/src/main/res/drawable/text_select_move_up_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/tooltip_24px.xml b/app/src/main/res/drawable/tooltip_24px.xml deleted file mode 100644 index 5770d07b4..000000000 --- a/app/src/main/res/drawable/tooltip_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/touch_double_24px.xml b/app/src/main/res/drawable/touch_double_24px.xml deleted file mode 100644 index f008bd374..000000000 --- a/app/src/main/res/drawable/touch_double_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/undo_24px.xml b/app/src/main/res/drawable/undo_24px.xml deleted file mode 100644 index 89a3f5b80..000000000 --- a/app/src/main/res/drawable/undo_24px.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/uppercase_24px.xml b/app/src/main/res/drawable/uppercase_24px.xml deleted file mode 100644 index 68b9166ae..000000000 --- a/app/src/main/res/drawable/uppercase_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/visibility_off_24px.xml b/app/src/main/res/drawable/visibility_off_24px.xml deleted file mode 100644 index 50ba19a57..000000000 --- a/app/src/main/res/drawable/visibility_off_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/volunteer_activism_24px.xml b/app/src/main/res/drawable/volunteer_activism_24px.xml deleted file mode 100644 index f2e719d3a..000000000 --- a/app/src/main/res/drawable/volunteer_activism_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/warning_24px.xml b/app/src/main/res/drawable/warning_24px.xml deleted file mode 100644 index ab1606dc9..000000000 --- a/app/src/main/res/drawable/warning_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/wifi_proxy_24px.xml b/app/src/main/res/drawable/wifi_proxy_24px.xml deleted file mode 100644 index 92798c29c..000000000 --- a/app/src/main/res/drawable/wifi_proxy_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/layout-port/player_select_source_priority.xml b/app/src/main/res/layout-port/player_select_source_priority.xml index 926227c58..2cba9c869 100644 --- a/app/src/main/res/layout-port/player_select_source_priority.xml +++ b/app/src/main/res/layout-port/player_select_source_priority.xml @@ -1,5 +1,6 @@ - - + @@ -32,21 +33,14 @@ android:textSize="20sp" android:textStyle="bold" /> - - + android:src="@drawable/baseline_help_outline_24" + android:contentDescription="@string/help" /> - + + tools:text="@string/profile_number" + tools:ignore="LabelFor" /> + android:text="@string/sort_save" + style="@style/WhiteButton" /> + android:text="@string/sort_close" + style="@style/BlackButton" /> diff --git a/app/src/main/res/layout/account_edit_dialog.xml b/app/src/main/res/layout/account_edit_dialog.xml index ae91c16a5..f52c8ea51 100644 --- a/app/src/main/res/layout/account_edit_dialog.xml +++ b/app/src/main/res/layout/account_edit_dialog.xml @@ -37,58 +37,32 @@ android:layout_marginBottom="60dp" android:orientation="vertical"> - + - + android:scaleType="centerCrop" + android:src="@drawable/profile_bg_blue" /> - - - - - - - - - - + android:background="@color/skipOpTransparent" + android:padding="5dp" + android:src="@drawable/ic_baseline_edit_24" /> + @@ -108,11 +83,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:buttonTint="?attr/textColor" - android:nextFocusUp="@id/account_name" - android:nextFocusDown="@id/apply_btt" android:text="@string/lock_profile" android:textColor="?attr/grayTextColor" /> + @@ -140,8 +113,6 @@ android:layout_width="wrap_content" android:layout_gravity="center_vertical|end" android:layout_toStartOf="@+id/cancel_btt" - android:nextFocusUp="@id/lockProfileCheckbox" - android:nextFocusDown="@id/cancel_btt" android:nextFocusLeft="@id/delete_btt" android:nextFocusRight="@id/cancel_btt" android:text="@string/sort_apply" /> @@ -152,8 +123,6 @@ android:layout_width="wrap_content" android:layout_alignParentEnd="true" android:layout_gravity="center_vertical|end" - android:nextFocusUp="@id/apply_btt" - android:nextFocusDown="@id/apply_btt" android:nextFocusLeft="@id/apply_btt" android:text="@string/sort_cancel" /> diff --git a/app/src/main/res/layout/account_list_item.xml b/app/src/main/res/layout/account_list_item.xml index 9b3cef349..3cbfc72fb 100644 --- a/app/src/main/res/layout/account_list_item.xml +++ b/app/src/main/res/layout/account_list_item.xml @@ -1,70 +1,59 @@ - + android:foreground="?attr/selectableItemBackground" + app:cardCornerRadius="@dimen/rounded_image_radius" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintDimensionRatio="1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHeight_percent="0.4" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - + + + android:background="@drawable/outline_card" + android:visibility="gone" + tools:visibility="visible" /> - - - - - - - - - + + android:padding="10dp" + android:textColor="@color/textColor" + android:textSize="16sp" + tools:text="Hello World!" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/account_list_item_add.xml b/app/src/main/res/layout/account_list_item_add.xml index 7f91f6580..dea64484f 100644 --- a/app/src/main/res/layout/account_list_item_add.xml +++ b/app/src/main/res/layout/account_list_item_add.xml @@ -1,51 +1,29 @@ - + app:cardCornerRadius="@dimen/rounded_image_radius" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintDimensionRatio="1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHeight_percent="0.4" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - + - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/account_list_item_edit.xml b/app/src/main/res/layout/account_list_item_edit.xml index d1dbef2ef..3f41a23c2 100644 --- a/app/src/main/res/layout/account_list_item_edit.xml +++ b/app/src/main/res/layout/account_list_item_edit.xml @@ -1,79 +1,67 @@ - + android:foreground="?attr/selectableItemBackground" + app:cardCornerRadius="@dimen/rounded_image_radius" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintDimensionRatio="1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHeight_percent="0.4" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - + + + android:background="@drawable/outline_card" + android:visibility="gone" + tools:visibility="visible" /> - + - - - - - - - - - + + android:padding="10dp" + android:textColor="@color/textColor" + android:textSize="16sp" + tools:text="Hello World!" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/account_select_linear.xml b/app/src/main/res/layout/account_select_linear.xml index 65c2ffba9..b78c0d44c 100644 --- a/app/src/main/res/layout/account_select_linear.xml +++ b/app/src/main/res/layout/account_select_linear.xml @@ -26,8 +26,6 @@ android:layout_marginTop="10dp" android:layout_width="match_parent" android:layout_height="wrap_content" - android:clipChildren="false" - android:clipToPadding="false" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:itemCount="4" tools:listitem="@layout/account_list_item" /> diff --git a/app/src/main/res/layout/activity_account_select.xml b/app/src/main/res/layout/activity_account_select.xml index 444a04a18..9f62d5601 100644 --- a/app/src/main/res/layout/activity_account_select.xml +++ b/app/src/main/res/layout/activity_account_select.xml @@ -18,7 +18,7 @@ android:nextFocusDown="@id/account_recycler_view" android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="@string/manage_accounts" - app:tint="@color/account_edit_btn_tint" /> + app:tint="@color/player_on_button_tv_attr" /> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index eb7c67145..2483a3714 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -82,9 +82,4 @@ tools:ignore="FragmentTagUsage" /> - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main_tv.xml b/app/src/main/res/layout/activity_main_tv.xml index 0d81285a6..c51d77d16 100644 --- a/app/src/main/res/layout/activity_main_tv.xml +++ b/app/src/main/res/layout/activity_main_tv.xml @@ -93,11 +93,6 @@ - - diff --git a/app/src/main/res/layout/bottom_input_dialog.xml b/app/src/main/res/layout/bottom_input_dialog.xml index 63f5e876d..bfb728052 100644 --- a/app/src/main/res/layout/bottom_input_dialog.xml +++ b/app/src/main/res/layout/bottom_input_dialog.xml @@ -4,7 +4,7 @@ android:nextFocusDown="@id/nginx_text_input" android:orientation="vertical" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="match_parent"> @@ -51,8 +50,6 @@ android:layout_gravity="center_vertical|end" android:text="@string/sort_apply" android:id="@+id/apply_btt" - android:nextFocusUp="@id/nginx_text_input" - android:nextFocusRight="@id/cancel_btt" style="@style/WhiteButton" /> diff --git a/app/src/main/res/layout/bottom_selection_dialog.xml b/app/src/main/res/layout/bottom_selection_dialog.xml index 78916970b..55ca6562e 100644 --- a/app/src/main/res/layout/bottom_selection_dialog.xml +++ b/app/src/main/res/layout/bottom_selection_dialog.xml @@ -37,7 +37,6 @@ android:paddingTop="10dp" android:dividerHeight="1dp" android:requiresFadingEdge="vertical" - android:paddingBottom="32dp" android:nextFocusRight="@id/cancel_btt" android:nextFocusLeft="@id/apply_btt" tools:listitem="@layout/sort_bottom_single_choice" /> diff --git a/app/src/main/res/layout/cast_item.xml b/app/src/main/res/layout/cast_item.xml index 610b4e3e4..4f7bdf74d 100644 --- a/app/src/main/res/layout/cast_item.xml +++ b/app/src/main/res/layout/cast_item.xml @@ -47,7 +47,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_downloads.xml b/app/src/main/res/layout/fragment_downloads.xml index ac5d383df..96e1e50ab 100644 --- a/app/src/main/res/layout/fragment_downloads.xml +++ b/app/src/main/res/layout/fragment_downloads.xml @@ -261,7 +261,7 @@ android:padding="8dp" android:src="@drawable/ic_network_stream" - app:tint="?attr/textColor" /> + app:tint="?attr/textColor"> @@ -306,7 +306,7 @@ android:paddingBottom="100dp" android:clipToPadding="false" android:descendantFocusability="afterDescendants" - android:nextFocusUp="@id/download_stream_button_tv" + android:nextFocusUp="@id/download_appbar" android:nextFocusLeft="@id/navigation_downloads" android:nextFocusDown="@id/download_queue_button" android:tag="@string/tv_no_focus_tag" diff --git a/app/src/main/res/layout/fragment_extensions.xml b/app/src/main/res/layout/fragment_extensions.xml index 6dd2d8662..b7cf4b6cd 100644 --- a/app/src/main/res/layout/fragment_extensions.xml +++ b/app/src/main/res/layout/fragment_extensions.xml @@ -4,39 +4,20 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/extensions_root" + android:background="?attr/primaryGrayBackground" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/primaryGrayBackground" android:orientation="vertical"> - - - - - - - + - - @@ -97,14 +64,14 @@ android:layout_height="80dp" android:layout_gravity="bottom" android:background="?attr/primaryGrayBackground" - android:baselineAligned="false" android:focusable="true" android:foreground="@drawable/outline_drawable" android:nextFocusRight="@id/add_repo_button_imageview" android:nextFocusUp="@id/repo_recycler_view" android:orientation="horizontal" android:padding="10dp" - android:visibility="visible"> + android:visibility="visible" + android:baselineAligned="false"> diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index e2baa3d2e..99a764dee 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -27,7 +27,9 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" + android:layout_marginTop="15dp" android:orientation="vertical" + android:paddingTop="40dp" app:shimmer_auto_start="true" app:shimmer_base_alpha="0.2" app:shimmer_duration="@integer/loading_time" @@ -38,11 +40,39 @@ android:layout_height="wrap_content" android:orientation="vertical"> - + android:layout_height="wrap_content" + android:orientation="horizontal"> + + + + + + + + + - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home_head.xml b/app/src/main/res/layout/fragment_home_head.xml index b62173ede..c57c32cee 100644 --- a/app/src/main/res/layout/fragment_home_head.xml +++ b/app/src/main/res/layout/fragment_home_head.xml @@ -26,8 +26,6 @@ - - - - @@ -27,8 +28,10 @@ android:id="@+id/home_loading_shimmer" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginTop="60dp" + android:layout_gravity="center" + android:layout_marginTop="15dp" android:orientation="vertical" + android:paddingTop="40dp" app:shimmer_auto_start="true" app:shimmer_base_alpha="0.2" app:shimmer_duration="@integer/loading_time" @@ -39,21 +42,46 @@ android:layout_height="wrap_content" android:orientation="vertical"> - + android:layout_height="wrap_content" + android:orientation="horizontal"> + + + + + + + + + @@ -65,7 +93,6 @@ - - - - - - - + + diff --git a/app/src/main/res/layout/fragment_setup_language.xml b/app/src/main/res/layout/fragment_setup_language.xml index bdba3f209..93afe823e 100644 --- a/app/src/main/res/layout/fragment_setup_language.xml +++ b/app/src/main/res/layout/fragment_setup_language.xml @@ -31,8 +31,6 @@ android:layout_height="match_parent" android:layout_rowWeight="1" android:layout_marginBottom="60dp" - android:nextFocusLeft="@id/skip_btt" - android:nextFocusRight="@id/next_btt" android:paddingTop="10dp" android:requiresFadingEdge="vertical" tools:listitem="@layout/sort_bottom_single_choice" /> diff --git a/app/src/main/res/layout/fragment_setup_layout.xml b/app/src/main/res/layout/fragment_setup_layout.xml index 9e891e1aa..18815ced3 100644 --- a/app/src/main/res/layout/fragment_setup_layout.xml +++ b/app/src/main/res/layout/fragment_setup_layout.xml @@ -29,8 +29,8 @@ android:layout_height="match_parent" android:layout_rowWeight="1" android:layout_marginBottom="60dp" - android:nextFocusLeft="@id/prev_btt" - android:nextFocusRight="@id/next_btt" + android:nextFocusLeft="@id/apply_btt" + android:nextFocusRight="@id/cancel_btt" android:paddingTop="10dp" android:requiresFadingEdge="vertical" tools:listitem="@layout/sort_bottom_single_choice" /> diff --git a/app/src/main/res/layout/fragment_setup_media.xml b/app/src/main/res/layout/fragment_setup_media.xml index c1154405e..154db3386 100644 --- a/app/src/main/res/layout/fragment_setup_media.xml +++ b/app/src/main/res/layout/fragment_setup_media.xml @@ -26,8 +26,8 @@ android:layout_height="match_parent" android:layout_rowWeight="1" android:layout_marginBottom="60dp" - android:nextFocusLeft="@id/prev_btt" - android:nextFocusRight="@id/next_btt" + android:nextFocusLeft="@id/apply_btt" + android:nextFocusRight="@id/cancel_btt" android:paddingTop="10dp" android:requiresFadingEdge="vertical" tools:listitem="@layout/sort_bottom_single_choice" /> diff --git a/app/src/main/res/layout/fragment_setup_provider_languages.xml b/app/src/main/res/layout/fragment_setup_provider_languages.xml index e92f1135b..8dc889178 100644 --- a/app/src/main/res/layout/fragment_setup_provider_languages.xml +++ b/app/src/main/res/layout/fragment_setup_provider_languages.xml @@ -29,8 +29,8 @@ android:layout_height="match_parent" android:layout_rowWeight="1" android:layout_marginBottom="60dp" - android:nextFocusLeft="@id/prev_btt" - android:nextFocusRight="@id/next_btt" + android:nextFocusLeft="@id/apply_btt" + android:nextFocusRight="@id/cancel_btt" android:paddingTop="10dp" android:requiresFadingEdge="vertical" tools:listitem="@layout/sort_bottom_single_choice" /> diff --git a/app/src/main/res/layout/loading_hero_banner.xml b/app/src/main/res/layout/loading_hero_banner.xml deleted file mode 100644 index efd3493db..000000000 --- a/app/src/main/res/layout/loading_hero_banner.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 9a79e6590..407de4a3f 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -11,7 +11,6 @@ android:id="@+id/player_metadata_scrim" android:layout_width="640dp" android:layout_height="match_parent" - android:layout_marginTop="-10dp" android:background="@drawable/bg_player_metadata_scrim_netflix" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -264,15 +263,6 @@ tools:visibility="visible" android:layout_gravity="center"/> - - @@ -927,19 +917,10 @@ android:layout_height="40dp" android:nextFocusLeft="@id/player_sources_btt" - android:nextFocusRight="@id/player_compressor_btt" + android:nextFocusRight="@id/player_skip_op" android:text="@string/tracks" app:icon="@drawable/ic_baseline_equalizer_24" /> - - @@ -1176,4 +1155,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/player_custom_layout_tv.xml b/app/src/main/res/layout/player_custom_layout_tv.xml index 06ec055c8..3a3076943 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -12,7 +12,6 @@ android:id="@+id/player_metadata_scrim" android:layout_width="680dp" android:layout_height="match_parent" - android:layout_marginTop="-10dp" android:background="@drawable/bg_player_metadata_scrim_netflix" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -352,22 +351,6 @@ android:layout_marginEnd="32dp" android:orientation="vertical"> - - - - + android:text="@string/tracks" + app:icon="@drawable/ic_baseline_equalizer_24" /> @@ -1143,8 +1115,6 @@ @@ -1201,4 +1171,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/player_select_source_priority.xml b/app/src/main/res/layout/player_select_source_priority.xml index 0d825de77..182cd1861 100644 --- a/app/src/main/res/layout/player_select_source_priority.xml +++ b/app/src/main/res/layout/player_select_source_priority.xml @@ -160,13 +160,6 @@ - - - - - diff --git a/app/src/main/res/layout/sort_bottom_single_provider_choice.xml b/app/src/main/res/layout/sort_bottom_single_provider_choice.xml index ea778c488..657e2f2b8 100644 --- a/app/src/main/res/layout/sort_bottom_single_provider_choice.xml +++ b/app/src/main/res/layout/sort_bottom_single_provider_choice.xml @@ -10,31 +10,13 @@ style="@style/CheckLabel" android:layout_gravity="center" tools:text="hello" /> - - - - - - + android:src="@drawable/pin_ic" + android:visibility="gone" + tools:visibility="visible" + tools:ignore="RtlHardcoded" /> \ No newline at end of file diff --git a/app/src/main/res/layout/source_profile_settings_dialog.xml b/app/src/main/res/layout/source_profile_settings_dialog.xml deleted file mode 100644 index 3f1f39fed..000000000 --- a/app/src/main/res/layout/source_profile_settings_dialog.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/trailer_custom_layout.xml b/app/src/main/res/layout/trailer_custom_layout.xml index b1dc96479..76231a2d3 100644 --- a/app/src/main/res/layout/trailer_custom_layout.xml +++ b/app/src/main/res/layout/trailer_custom_layout.xml @@ -11,7 +11,6 @@ android:id="@+id/player_metadata_scrim" android:layout_width="640dp" android:layout_height="match_parent" - android:layout_marginTop="-10dp" android:background="@drawable/bg_player_metadata_scrim_netflix" android:visibility="gone" app:layout_constraintStart_toStartOf="parent" @@ -102,8 +101,6 @@ @@ -171,12 +168,6 @@ android:layout_marginEnd="32dp" android:orientation="vertical"> - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index b29fc5127..27f186a00 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -137,7 +137,7 @@ diff --git a/app/src/main/res/values-arz/strings.xml b/app/src/main/res/values-arz/strings.xml index 4a4cb755f..55344e519 100644 --- a/app/src/main/res/values-arz/strings.xml +++ b/app/src/main/res/values-arz/strings.xml @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/app/src/main/res/values-b+af/strings.xml b/app/src/main/res/values-b+af/strings.xml index b81848db6..81d7a96ae 100644 --- a/app/src/main/res/values-b+af/strings.xml +++ b/app/src/main/res/values-b+af/strings.xml @@ -24,7 +24,8 @@ Rand tipe Klaar Afgelaai Kyk verder - Nuwe opdatering gevind! \n%1$s -> %2$s + Nuwe opdatering gevind! +\n%1$s -> %2$s Laai Tale af Soek deur verskaffers te gebruik Gaan terug diff --git a/app/src/main/res/values-b+am/strings.xml b/app/src/main/res/values-b+am/strings.xml index 56b71f5a0..7fd3274b9 100644 --- a/app/src/main/res/values-b+am/strings.xml +++ b/app/src/main/res/values-b+am/strings.xml @@ -8,7 +8,8 @@ %1$dሰዓት %2$dደቂቃ ፖስተር የወረዱ - አዲስ ማሻሻያ ተገኝቷል! \n%1$s -> %2$s + አዲስ ማሻሻያ ተገኝቷል! +\n%1$s -> %2$s ተመለስ ተጨማሪ አማራጮች በማየት ላይ diff --git a/app/src/main/res/values-b+apc/strings.xml b/app/src/main/res/values-b+apc/strings.xml index 90eba4218..3bf88ce13 100644 --- a/app/src/main/res/values-b+apc/strings.xml +++ b/app/src/main/res/values-b+apc/strings.xml @@ -29,7 +29,8 @@ فرجي الـLogcat 🐈 +30 كفي حضر - في أپدايت جديدة! \n%1$s ← %2$s + في أپدايت جديدة! +\n%1$s ← %2$s نزل الترجمات مع الڤيديو عوزو المصادر لَ تنبّشو رجاع @@ -95,7 +96,8 @@ لون الكتيبة مخلص عوز قوة ضوّ الشاشة تبع السيستام بدل من تغميئ الڤيديو - فشل ترجيع النسخة الإحتياطية من ملف \n%s + فشل ترجيع النسخة الإحتياطية من ملف +\n%s مشّي المقطع الدعائي مشّي البث المباشر م لقينا ولا حلقة @@ -164,7 +166,8 @@ طفي الترجمة القصة مستعمل - %dد \nباقي + %dد +\nباقي عم ينعرض حاليًا بلايحة النَطر حالة @@ -191,7 +194,7 @@ في مشكلة بجهاز العرض (Renderer error) العِنوان پروكسي \"گِت هَب\" - فرجي معلومات مشغل الڤيديو + جودة مشغل الڤيديو ملصق الترجمة أوڤا نَزِل من مصادر وجودات مختلفة @@ -358,7 +361,11 @@ العشوائي يللي بعده خيال عم نجدِد المثلثلات يللي مشتركينلها - مبين إنو فيه عنصر متل هيدا موجود بال مكتبة عندكن: \n \n%s \n \nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوه مع العنصر الموجود، أو تلغو الإجراء؟ + مبين إنو فيه عنصر متل هيدا موجود بال مكتبة عندكن: +\n +\n%s +\n +\nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوه مع العنصر الموجود، أو تلغو الإجراء؟ نزلت %1$d %2$s معرف مش صالح أفّي %s @@ -366,7 +373,9 @@ حطو الأرقام السرية لـ\"%s\" الطريقة القديمة معلى - \"كلود ستريم\" م بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپويات. \n \nفيه معلومات على الـ\"ديسكورد\" تبعنا، أو فيكن تنبشو ع معلومات على الإنترنت. + \"كلود ستريم\" م بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپويات. +\n +\nفيه معلومات على الـ\"ديسكورد\" تبعنا، أو فيكن تنبشو ع معلومات على الإنترنت. زبد تتبع 3G/4G… نفَتح %s @@ -379,7 +388,8 @@ دايمًا كتوب ب أحرف كاپيتال، A بدل a مشغل الڤيديو المفضل 4K - بَلَش تنزيل %1$d %2$s \n… + بَلَش تنزيل %1$d %2$s +\n… الوصف شوف الريپويات تبع مجتمع \"كلاود ستريم\" إنت هلّق بال وضع الآمن @@ -409,7 +419,9 @@ أفّى الإعداد فتت ع أكونت \"%s\" تبعك حدود خطية - في مشكلة بطعمير الـUI. هيدي مشكبة كبيرة. پليز بَلِغ عنّا \n(UI was unable to be created correctly) \n%s + في مشكلة بطعمير الـUI. هيدي مشكبة كبيرة. پليز بَلِغ عنّا +\n(UI was unable to be created correctly) +\n%s عَدِل تجَدَد (من الجديد للقديم) TC @@ -465,13 +477,14 @@ SD الإضافات شيل الإعلانات من الترجمة - رفّكن فاضي ☹ \nفوتو على أكونت فيها رفّ الڤيديوات يللي حضرينها أو زيدو ڤيديوات بال رفّ المحلي. + رفّكن فاضي ☹ +\nفوتو على أكونت فيها رفّ الڤيديوات يللي حضرينها أو زيدو ڤيديوات بال رفّ المحلي. اسم الريپو (مش ضروري) الجودات بيانات مش صالحة عوز SDR - كاميرا HD + كاميرا شيلو من المفضل UHD نزادت الإضافة @@ -492,7 +505,8 @@ رايتينگ (من الواطي للعالي) فتاح من ملف طفي - لقينا ملف الوضع الآمن! \nمش رح نعوز إضافيات وقت ما ينفَتَح الآپ حتّى ينشال الملف. + لقينا ملف الوضع الآمن! +\nمش رح نعوز إضافيات وقت ما ينفَتَح الآپ حتّى ينشال الملف. مش مغير وقت الترجمة مشكلة مصدر @@ -517,7 +531,7 @@ إضافات م قدرنا نفتح %s رايتينگ: %s - تحزير: \"كلاود ستريم\" مش مسؤولة عن الإضافات المش رسمية، و م بتدعمن أبدًا! + تحزير: \"كلاود ستريم 3\" مش مسؤولة عن الإضافات المش رسمية، و م بتدعمن أبدًا! الحالة محي الريپو مشغل الڤيديو @@ -526,7 +540,10 @@ إنتو أصلًا مصوتين كاميرا م لقينا ولا إضافة بال ريپو - مبين إنو في عنصر متل هيدا موجود بالمكتبة عندكن: \n\"%s\" \n \nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوّ مع العنصر الموجود، أو تلغو الإجراء؟ + مبين إنو في عنصر متل هيدا موجود بالمكتبة عندكن: +\n\"%s\" +\n +\nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوّ مع العنصر الموجود، أو تلغو الإجراء؟ رايط مش صالح 1000 مللي ثانية إصدار @@ -546,7 +563,13 @@ @string/home_play شيلو من لايحة المحتوى الحاضرينو الإعتمادات - فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يللي عنده أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يللي بتفضلوها. \n \nمتلًا: \nإزا المصدر \"أ\" بتفضلوه، بتعطوه كتير نقات (متلًا 8). \nإزا الجودة 480 ما بتحبوها، بتعطوها نقات قليلة (متلًا 1). \n \nعلامت المصدر وال جودة تبعه بينجمعو مع بعض (8 + 1 = 9). يللي علامته 10 أو أعلى، بينحط تلقائيًا، من دون ما ينعمل لود لكل المصادر! + فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يللي عنده أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يللي بتفضلوها. +\n +\nمتلًا: +\nإزا المصدر \"أ\" بتفضلوه، بتعطوه كتير نقات (متلًا 8). +\nإزا الجودة 480 ما بتحبوها، بتعطوها نقات قليلة (متلًا 1). +\n +\nعلامت المصدر وال جودة تبعه بينجمعو مع بعض (8 + 1 = 9). يللي علامته 10 أو أعلى، بينحط تلقائيًا، من دون ما ينعمل لود لكل المصادر! حطو الأرقام السرية الحالية صوت حط كبسة لبرم إتجاه الشاشة @@ -566,14 +589,16 @@ رمز/كلمة مرور للمصادقة فتاح التطبيق باستعمال البصمة، آي دي الوج، پِن، النمط، أو الپاسورد. بعد كذا محاولة فاشلة، هيدا الشباك رح يسكر. بكل بساطة، سكر الآپ ورجاع فتحه حتى تجرب بعد مرة. - %s \nباقي + %s +\nباقي المصادقة البيومترية مش مدعومة ع هالجهاز شيله من المفضل اسم وعنوان الريپو نتسخ! فيه ارور بال وصول ل الكليپ-بورد. پليز جرب مرة أخرى. فيه ارور بال نسخ. پليز نسوخ الـLogcat 🐈 وبعته ل المسؤولين عن دعم الآپ. - هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. \nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج. + هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. +\nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج. أوكي وقف اپتميزايشن بطارية جهازك بطارية الآپ اصلًا محطوطة ع «غير مقيد» \"Unrestricted\" @@ -609,13 +634,21 @@ نقي الإشيا اللي بدك تمحيها موجود لينحضر بلا إنترنت محي الفايلات - متأكد إنو بدك تمحي هيدي الإشيا ل الأبد؟ \n \n%s - رح كمان تمحي ل الأبد كل الحلقات اللي ب هيدا المسلسل؟ \n \n%s + متأكد إنو بدك تمحي هيدي الإشيا ل الأبد؟ +\n +\n%s + رح كمان تمحي ل الأبد كل الحلقات اللي ب هيدا المسلسل؟ +\n +\n%s نقي كل شي شيل التنقاية محي (%1$d | %2$s) - متأكد إنو بدك تمحي هيدي الحلقات من %1$s؟ \n \n%2$s - متأكد إنو بدك تمحي كل الحلقات اللي بهيدا المسلسل؟ \n \n%s + متأكد إنو بدك تمحي هيدي الحلقات من %1$s؟ +\n +\n%2$s + متأكد إنو بدك تمحي كل الحلقات اللي بهيدا المسلسل؟ +\n +\n%s صورة زغيرة مع التقريب وال تبعيد بت حط صورة زغير من الڤيديو إنت و عم بت قرب أو ترجع بال ڤيديو بعد مش معمول لود لولا ترجمة @@ -650,7 +683,7 @@ م تجدد شي. رايتينگ %s بلشت عملية تجديد الإضافات! - نجح تجديد %d إضافات + نجح تجديد %d إضافات! نوتيفيكايشن بت خليك تتحكم بال ڤيديو لما م يكون الآپ مفتوح اونلاين داخلياً @@ -706,47 +739,4 @@ فوق، عال شمال فوق، بال نُص فوق، عال يمين - ليستة التنزيلات - مافي شي عم يتنزّل هلّق. - قوة ضو إضافية - بت حط فلتر للبرايتنس لمّا تعلي قوة الضو ل أكتر من 100% - extra_brightness_enabled - اقتراحات التنبيش - بت فرجي اقتراحات إنتا و عم بت نَبّش - مساح الاقتراحات - فرجي ميتا-ديتا فوق الڤيديو - فرجي ليستة الممثلين - ڤيديو - معلومات الڤيديو - أولوية المصدر - حدد ترتيب المصادر بال مشغل - اسم المصدر - نزلن كلن - لغين كلن - بدك تنزل الحلقة %s؟ - بدك تلغي كل شي عم يتنَزَّل؟ - - م شي عم يتنزل - شي واحد عم يتنزل - - - مافي شي بعد بده يبلش يتنزل - فيه شي واحد بعد بده يبلش يتنزل - - لايڤ - پريڤيو - حفة - لوگ كات 🐈 - بلش الڤيديو مع پوز - بت قلل السترس وقت تنقاية المصدر - فرجي ساعة وقت - بت فرجس الوقت ع أعلى الشاشة بال صفحة الرئيسية و مشغل الڤيديو - ستنگز الپروفيل - خبي المصادر اللي عندها أولوية نيگاتيڤ - خبي المصادر اللي عندها أخطاء - - ولا لينك مخبا - لينك واحد مخبا - - خلفية diff --git a/app/src/main/res/values-b+ar/strings.xml b/app/src/main/res/values-b+ar/strings.xml index 5456b1a28..f7186c0ef 100644 --- a/app/src/main/res/values-b+ar/strings.xml +++ b/app/src/main/res/values-b+ar/strings.xml @@ -12,7 +12,8 @@ سرعة (%.2fx) تقييم: %.1f - يوجد تحديث جديد! \n%1$s -> %2$s + يوجد تحديث جديد! +\n%1$s -> %2$s %d دقيقة CloudStream تشغيل بواسطة CloudStream @@ -175,8 +176,10 @@ إستئناف -٣٠ +٣٠ - سوف يتم الحذف نهائيا %s \nهل أنت متأكد? - %dm \nمتبقية + سوف يتم الحذف نهائيا %s +\nهل أنت متأكد? + %dm +\nمتبقية جاري التنفيذ اكتمل الحالة @@ -340,7 +343,7 @@ عشوائي قريبا… تصوير كام - مقطع مسجل CamRip + تصوير كام قطع تصوير كام جودة عالية HQ عالِ الدقة @@ -398,7 +401,9 @@ تم تحميل: %d مُعطل %d غير مُحمل: %d - لا يحتوي CloudStream على مواقع مثبتة افتراضيًا. تحتاج إلى تثبيت المواقع من المستودعات. \n \nانضم إلى ديسكورد أو ابحث عبر الإنترنت. + لا يحتوي CloudStream على مواقع مثبتة افتراضيًا. تحتاج إلى تثبيت المواقع من المستودعات. +\n +\nانضم إلى ديسكورد أو ابحث عبر الإنترنت. عرض مستودعات المجتمع قائمة عامة جميع الترجمات حروف كبيرة @@ -488,13 +493,15 @@ التقييم (من الأعلى إلى الأدنى) التقييم (من الأدنى إلى الأعلى) الترتيب الأبجدي (من ي إلى أ) - مكتبتك فارغة :( \nقم بتسجيل الدخول على حساب مكتبة أو أضف عروضا إلى مكتبتك المحلية. + مكتبتك فارغة :( +\nقم بتسجيل الدخول على حساب مكتبة أو أضف عروضا إلى مكتبتك المحلية. محدث (من القديم إلى الجديد) فرز حسب افرز فتح بواسطة المكتبة - تم العثور على ملف الوضع الآمن! \nلا يتم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف. + تم العثور على ملف الوضع الآمن! +\nلا يتم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف. مدة التقديم عنما يكون المشغل مخفيا مدة التقديم - المشغل مخفي تلفزيون أندرويد @@ -526,7 +533,13 @@ تعديل الملفات التعريفية مساعدة - ‎‎هنا يمكنك تغيير طريقة ترتيب المصادر. إذا كان للفيديو أولوية أعلى ، فسيظهر في الأعلى في اختيار المصدر. يمثل مجموع أولوية المصدر وأولوية الجودة أولوية الفيديو. \n \nالمصدر أ: 3 \nالجودة ب: 7 \nسيكون لها أولوية فيديو مجمعة تبلغ 10. \n \nملاحظة: إذا كان المجموع 10 أو أكثر ، فسيتخطى المشغل التحميل تلقائيًا عند تحميل هذا الرابط! + ‎‎هنا يمكنك تغيير طريقة ترتيب المصادر. إذا كان للفيديو أولوية أعلى ، فسيظهر في الأعلى في اختيار المصدر. يمثل مجموع أولوية المصدر وأولوية الجودة أولوية الفيديو. +\n +\nالمصدر أ: 3 +\nالجودة ب: 7 +\nسيكون لها أولوية فيديو مجمعة تبلغ 10. +\n +\nملاحظة: إذا كان المجموع 10 أو أكثر ، فسيتخطى المشغل التحميل تلقائيًا عند تحميل هذا الرابط! النوعيات خلفية الملف الشخصي تعذر إنشاء واجهة المستخدم بشكل صحيح ، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور %s @@ -539,7 +552,11 @@ تمت إزالة %s من المفضلة المفضلة تمت إضافة %s إلى المفضلة - احتمال وجود تكرارات في مكتبتك. \n \n%s \n \nهل تريد الاضافة على اي حال مستبدلاً النسخة الموجودة بالفعل, أم تفضل إلغاء العملية؟ + احتمال وجود تكرارات في مكتبتك. +\n +\n%s +\n +\nهل تريد الاضافة على اي حال مستبدلاً النسخة الموجودة بالفعل, أم تفضل إلغاء العملية؟ احتمال أن يكون موجود بالفعل قفل الحساب اضافة الى المفضلة @@ -552,7 +569,9 @@ إشترك إزالة من المفضلة اختار حساب - يبدو أن هناك عنصرًا مكررًا موجود بالفعل في مكتبتك: \'%s\'. \n \nهل ترغب في إضافة هذا العنصر على أية حال، أو استبدال العنصر الموجود، أو إلغاء الإجراء؟ + يبدو أن هناك عنصرًا مكررًا موجود بالفعل في مكتبتك: \'%s\'. +\n +\nهل ترغب في إضافة هذا العنصر على أية حال، أو استبدال العنصر الموجود، أو إلغاء الإجراء؟ ادخال ال PIN PIN أدخل ال PIN الحالي @@ -580,7 +599,8 @@ مصادقة كلمة المرور/رقم التعريف الشخصي بعد عدة محاولات فاشلة، سيتم إغلاق المطالبة. ما عليك سوى إعادة تشغيل التطبيق للمحاولة مرة أخرى. لقد تم الآن نسخ بيانات CloudStream احتياطيًا. على الرغم من أن احتمال حدوث ذلك منخفض جدًا، إلا أن جميع الأجهزة يمكن أن تتصرف بشكل مختلف. في الحالات النادرة، التي يتم فيها منعك من الوصول إلى التطبيق، قم بمسح بيانات التطبيق بالكامل واستعادتها من نسخة احتياطية. نحن نأسف جدًا لأي إزعاج ناتج عن هذا. - %s \nمتبقي + %s +\nمتبقي المفضلة إزالة من المفضلة اسم و عنوان المخزن @@ -622,13 +642,21 @@ الرجاء تحديد العناصر للحذف تحديد الكل حذف الملفات - هل أنت متأكد أنك تريد حذف الحلقات التالية نهائيًا في %1$s ؟ \n \n%2$s - ستقوم أيضًا بحذف جميع الحلقات في السلسلة التالية نهائيًا: \n \n%s + هل أنت متأكد أنك تريد حذف الحلقات التالية نهائيًا في %1$s ؟ +\n +\n%2$s + ستقوم أيضًا بحذف جميع الحلقات في السلسلة التالية نهائيًا: +\n +\n%s حذف (%1$d | %2$s) متاح للمشاهدة في وضع عدم الاتصال إلغاء تحديد الكل - هل أنت متأكد أنك تريد حذف العناصر التالية نهائيًا ؟ \n \n%s - هل أنت متأكد أنك تريد حذف جميع الحلقات في السلسلة التالية نهائيًا ؟ \n \n%s + هل أنت متأكد أنك تريد حذف العناصر التالية نهائيًا ؟ +\n +\n%s + هل أنت متأكد أنك تريد حذف جميع الحلقات في السلسلة التالية نهائيًا ؟ +\n +\n%s معاينة شريط البحث تمكين معاينة الصورة المصغرة على شريط البحث لم يتم تحميل أي ترجمات بعد @@ -721,7 +749,7 @@ اقتراحات البحث عرض اقتراحات البحث أثناء الكتابة مسح الاقتراحات - عرض لوحة فريق التمثيل + عرض لوحة البث تثبيت الإصدار التجريبي تم تثبيت الإصدار التجريبي بالفعل. فشل تثبيت الإصدار التجريبي. @@ -753,17 +781,4 @@ %d تنزيل قيد الانتظار عرض واجهة منبثقة للبيانات الوصفية للمشغِّل - مقطع - استعراض - البث قائم - أظهر ساعة الوقت الحقيقي - أظهر شاشة الوقت الحقيقي في قمة الشاشة. ينطبق على الصفحة الرئيسية و المشغل - اعدادات الملف الشخصي - أخفي المصادر صاحبة الأولوية السلبية - أخفي المصادر التي تحتوي أخطاء - حواف النص - Logcat 🐈 - استئناف المقاطع الموقوفة - تقليل الضغط عند اختيار مصادر - خلفية النص diff --git a/app/src/main/res/values-b+ars/strings.xml b/app/src/main/res/values-b+ars/strings.xml index cd3830a01..3104e6a9a 100644 --- a/app/src/main/res/values-b+ars/strings.xml +++ b/app/src/main/res/values-b+ars/strings.xml @@ -35,7 +35,8 @@ توقف التنزيل خطط للمشاهدة إعادة المشاهدة - !تم العثور على تحديث جديد \n%1$s -> %2$s + !تم العثور على تحديث جديد +\n%1$s -> %2$s %.1f:قدر %dاقل كلاودستريم @@ -156,15 +157,23 @@ تم التحديث (من الجديد إلى القديم) تم التحديث (القديم إلى الجديد) أبجديًا (من الألف إلى الياء) - مكتبتك فارغة :( \nقم بتسجيل الدخول إلى حساب المكتبة أو قم بإضافة العروض إلى مكتبتك المحلية. - !تم العثور على ملف الوضع الآمن \n.عدم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف + مكتبتك فارغة :( +\nقم بتسجيل الدخول إلى حساب المكتبة أو قم بإضافة العروض إلى مكتبتك المحلية. + !تم العثور على ملف الوضع الآمن +\n.عدم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف ارجع تحديث العروض المشتركة الوضع العادي حرر ملفات تعريفية مساعدة - .هنا يمكنك تغيير كيفية ترتيب المصادر. إذا كان للفيديو أولوية أعلى، فسيظهر في مكان أعلى في تحديد المصدر. مجموع أولوية المصدر وأولوية الجودة هو أولوية الفيديو \n \nالمصدر أ: 3 \nالجودة ب: 7 \nستكون أولوية الفيديو المدمجة .10 \n \n!ملاحظة: إذا كان المجموع 10 أو أكثر، فسيقوم اللاعب تلقائيًا بتخطي التحميل عند تحميل هذا الرابط + .هنا يمكنك تغيير كيفية ترتيب المصادر. إذا كان للفيديو أولوية أعلى، فسيظهر في مكان أعلى في تحديد المصدر. مجموع أولوية المصدر وأولوية الجودة هو أولوية الفيديو +\n +\nالمصدر أ: 3 +\nالجودة ب: 7 +\nستكون أولوية الفيديو المدمجة .10 +\n +\n!ملاحظة: إذا كان المجموع 10 أو أكثر، فسيقوم اللاعب تلقائيًا بتخطي التحميل عند تحميل هذا الرابط لقد صوت بالفعل أبجديًا (ياء إلى ألف) ترتيب حسب @@ -218,7 +227,8 @@ قدم وصف يستمر التشغيل في مشغل مصغر فوق التطبيقات الأخرى - نهائيا %sسيؤدي هذا الى حذف \nهل أنت متأكد؟ + نهائيا %sسيؤدي هذا الى حذف +\nهل أنت متأكد؟ الخط حجم الخط زيل @@ -245,7 +255,8 @@ 🐈عرض لوجكات سجل صور في صور - %d \nباقي + %d +\nباقي مصدر اللاعب مخفي - ابحث عن المبلغ تكرار النسخ الاحتياطي diff --git a/app/src/main/res/values-b+as/strings.xml b/app/src/main/res/values-b+as/strings.xml index b7efb3341..16be7e313 100644 --- a/app/src/main/res/values-b+as/strings.xml +++ b/app/src/main/res/values-b+as/strings.xml @@ -252,9 +252,12 @@ পুনৰ আৰম্ভ কৰক -৩০ +৩০ - এইটো স্থায়ীভাৱে %s ডিলিট কৰিব। \nআপুনি নিশ্চিত নেকি? - %dm \nবাকী - %s \nবাকী + এইটো স্থায়ীভাৱে %s ডিলিট কৰিব। +\nআপুনি নিশ্চিত নেকি? + %dm +\nবাকী + %s +\nবাকী চলমান সম্পূৰ্ণ স্থিতি @@ -453,7 +456,9 @@ %d প্লাগইন আপডেট কৰা হ\'ল নিষ্ক্ৰিয় কৰা: %d ডাউনলোড কৰা নহয়: %d - CloudStreamত কোনো চাইট ডিফল্টভাৱে ইনষ্টল হোৱা নাই। আপুনিয়ে ৰিপ\'জিট\'ৰিবোৰৰ পৰা চাইটসমূহ ইনষ্টল কৰিব লাগিব। \n \nআমাৰ Discordত যোগদান কৰক বা অনলাইন বিচাৰক। + CloudStreamত কোনো চাইট ডিফল্টভাৱে ইনষ্টল হোৱা নাই। আপুনিয়ে ৰিপ\'জিট\'ৰিবোৰৰ পৰা চাইটসমূহ ইনষ্টল কৰিব লাগিব। +\n +\nআমাৰ Discordত যোগদান কৰক বা অনলাইন বিচাৰক। সম্প্ৰদায়ৰ ৰিপ\'জিট\'ৰিসমূহ চাওক সকলো চাবটাইটল মুকলি আখৰত সতর্কতা: CloudStream 3 কোৱা নাই যে তৃতীয় পক্ষৰ বৃদ্ধিসমূহ ব্যৱহাৰ কৰিবলৈ আপুনি সম্পূৰ্ণ দায়িত্ব ল\'ব আৰু কোনো সহায় নাপাব! @@ -518,9 +523,11 @@ বৰ্ণানুক্ৰমিক (A ৰ পৰা Z) পুথিভঁৰালী বাছক ইয়াৰ সহায়ত খুলক - আপোনাৰ পুথিভঁৰালী খালি আছে :( \nএখন পুথিভঁৰালী একাউণ্টত লগ ইন কৰক বা স্থানীয় পুথিভঁৰালীত শ্ব\'সমূহ যোগ কৰক। + আপোনাৰ পুথিভঁৰালী খালি আছে :( +\nএখন পুথিভঁৰালী একাউণ্টত লগ ইন কৰক বা স্থানীয় পুথিভঁৰালীত শ্ব\'সমূহ যোগ কৰক। এই তালিকা খালি। অন্য এটি তালিকালৈ সলনি কৰি চাওক। - নিরাপদ ম\'ড ফাইল পোৱা গৈছে! \nফাইল আঁতৰোৱা নোহোৱালৈকে কোনো এক্সটেনশ্যন আৰম্ভ নকৰা হৈছে। + নিরাপদ ম\'ড ফাইল পোৱা গৈছে! +\nফাইল আঁতৰোৱা নোহোৱালৈকে কোনো এক্সটেনশ্যন আৰম্ভ নকৰা হৈছে। ঘূৰাই দিয়া সদস্যতা গ্ৰহণ কৰা শ্ব\'সমূহ আপডেট কৰিছে %s-ত সদস্যতা গ্ৰহণ কৰা হৈছে @@ -532,7 +539,13 @@ সম্পাদনা কৰক প্ৰ\'ফাইলসমূহ সহায় - ইয়াত আপুনি উৎসসমূহ কেনেকৈ ক্ৰম অনুযায়ী চিহ্নিত কৰিব সেই বিষয়ে সলনি কৰিব পাৰে। যদি এখন ভিডিঅ\'ৰ উচ্চ অগ্ৰাধিকাৰ থাকে তেন্তে সেইটো উৎস নিৰ্বাচনৰ সময়ত ওপৰত দেখা যাব। উৎস অগ্ৰাধিকাৰ আৰু গুণ অগ্ৰাধিকাৰৰ মুঠ যোগফল হৈছে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ। \n \nউৎস A: 3 \nগুণ B: 7 \nসৰহযোগে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ হ\'ব 10। \n \nদ্ৰষ্টব্য: যদি যোগফল 10 বা তাৰ ওপৰত থাকে তেন্তে প্লেয়াৰটো নিজেই লিংক ল\'ড কৰাৰ সময়ত ল\'ডিং এড়াই যাব! + ইয়াত আপুনি উৎসসমূহ কেনেকৈ ক্ৰম অনুযায়ী চিহ্নিত কৰিব সেই বিষয়ে সলনি কৰিব পাৰে। যদি এখন ভিডিঅ\'ৰ উচ্চ অগ্ৰাধিকাৰ থাকে তেন্তে সেইটো উৎস নিৰ্বাচনৰ সময়ত ওপৰত দেখা যাব। উৎস অগ্ৰাধিকাৰ আৰু গুণ অগ্ৰাধিকাৰৰ মুঠ যোগফল হৈছে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ। +\n +\nউৎস A: 3 +\nগুণ B: 7 +\nসৰহযোগে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ হ\'ব 10। +\n +\nদ্ৰষ্টব্য: যদি যোগফল 10 বা তাৰ ওপৰত থাকে তেন্তে প্লেয়াৰটো নিজেই লিংক ল\'ড কৰাৰ সময়ত ল\'ডিং এড়াই যাব! প্ৰ\'ফাইলৰ পটভূমি UI সঠিকভাৱে সৃষ্টি কৰিব পৰা নগ\'ল, ই এটা গুৰুত্বপূৰ্ণ সমস্যা আৰু তাক অবিলম্বে জনোৱা উচিত %s আপুনি ইতিমধ্যে ভোট দিছে @@ -543,8 +556,14 @@ প্ৰিয় তালিকাৰ পৰা আঁতৰ কৰক সম্ভাৱ্য নকল বস্ত্ত পোৱা গৈছে সকলো প্ৰতিস্থাপন কৰক - আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তু পোৱা গৈছে: \'%s.\' \n \nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমান বস্তু প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে? - আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তুসমূহ পোৱা গৈছে: \n \n%s \n \nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমানবোৰ প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে? + আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তু পোৱা গৈছে: \'%s.\' +\n +\nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমান বস্তু প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে? + আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তুসমূহ পোৱা গৈছে: +\n +\n%s +\n +\nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমানবোৰ প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে? %s ৰ বাবে পিন সন্নিবিষ্ট কৰক বৰ্তমান পিন সন্নিবিষ্ট কৰক প্ৰফাইল লক কৰক @@ -569,7 +588,8 @@ ডাউনলোড এপ্‌ আৰম্ভণিৰ পিছত নতুন আপডেটৰ সন্ধান কৰক। একেই ডেভেলপাৰৰ দ্বাৰা এনিম এপ্‌ - নতুন আপডেট পোৱা গ’ল! \n%1$s -> %2$s + নতুন আপডেট পোৱা গ’ল! +\n%1$s -> %2$s ফিলাৰ CloudStreamৰে প্লে কৰক সন্ধান @@ -589,10 +609,18 @@ প্ৰয়োগ কৰক ফাইলসমূহ ডিলিট কৰক ডিলিট (%1$d | %2$s) - আপুনি স্থায়ীভাৱে তলত দিয়া আইটেমসমূহ ডিলিট কৰিবলৈ নিশ্চিত নেকি? \n \n%s - %1$s ত তলত দিয়া এপিচ’ডসমূহ স্থায়ীভাৱে ডিলিট কৰিব নেকি? \n \n%2$s - আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিব: \n \n%s - আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিবলৈ নিশ্চিত নেকি? \n \n%s + আপুনি স্থায়ীভাৱে তলত দিয়া আইটেমসমূহ ডিলিট কৰিবলৈ নিশ্চিত নেকি? +\n +\n%s + %1$s ত তলত দিয়া এপিচ’ডসমূহ স্থায়ীভাৱে ডিলিট কৰিব নেকি? +\n +\n%2$s + আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিব: +\n +\n%s + আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিবলৈ নিশ্চিত নেকি? +\n +\n%s মুক্তিৰ তাৰিখ (পুৰণাৰ পৰা নতুন) সতৰ্কবাৰ্তা স্থানীয়ভাৱে প্ৰমাণীকৰণ কৰক diff --git a/app/src/main/res/values-b+bg/strings.xml b/app/src/main/res/values-b+bg/strings.xml index 4fb7757fd..e59f19462 100644 --- a/app/src/main/res/values-b+bg/strings.xml +++ b/app/src/main/res/values-b+bg/strings.xml @@ -16,7 +16,8 @@ Визуализация на фона Скорост (%.2fx) Оценка: %.1f - Намерена е нова актуализация! \n%1$s -> %2$s + Намерена е нова актуализация! +\n%1$s -> %2$s Шаблон %d мин CloudStream @@ -182,8 +183,10 @@ Продължи -30 30 - Това ще изтрие за постоянно %s \nСигурни ли сте? - %dm \nостава + Това ще изтрие за постоянно %s +\nСигурни ли сте? + %dm +\nостава Продължава Завършен Статус @@ -402,7 +405,9 @@ Деактивирано: %d Не е изтеглено: %d Актуализирани %d плъгини - CloudStream няма инсталирани сайтове по подразбиране. Трябва да инсталирате сайтовете от хранилища. \n \nПрисъединете се към нашия Дискорд или потърсете онлайн. + CloudStream няма инсталирани сайтове по подразбиране. Трябва да инсталирате сайтовете от хранилища. +\n +\nПрисъединете се към нашия Дискорд или потърсете онлайн. Вижте хранилищата на общността Публичен списък Всички субтитри с главни букви @@ -514,10 +519,12 @@ Профил %d По азбучен ред (A до Z) Отваряне с - Вашата библиотека е празна :( \nВпишете се в акаунт с библиотеки или добавете сериали в локалната Ви библиотека. + Вашата библиотека е празна :( +\nВпишете се в акаунт с библиотеки или добавете сериали в локалната Ви библиотека. Използване Епизод %d е публикуван! - Намерен е файл за безопасен режим! \nНяма да се зареждат никакви разширения при стартиране, докато файлът не бъде премахнат. + Намерен е файл за безопасен режим! +\nНяма да се зареждат никакви разширения при стартиране, докато файлът не бъде премахнат. Вече сте гласували Задаване по подразбиране ПИН трябва да е 4 символа @@ -544,11 +551,23 @@ Скрит играч - сума за търсене Сумата за търсене, използвана, когато играчът е скрит Актуализирано (от ново към старо) - Тук можете да промените начина на подреждане на източниците. Ако даден видеоклип има по-висок приоритет, той ще се показва по-високо в избора на източник. Сборът от приоритета на източника и приоритета на качеството е приоритетът на видеото. \n \nИзточник A: 3 \nКачество B: 7 \nЩе има комбиниран видео приоритет от 10. \n \nЗАБЕЛЕЖКА: Ако сумата е 10 или повече, играчът автоматично ще пропусне зареждането, когато тази връзка се зареди! + Тук можете да промените начина на подреждане на източниците. Ако даден видеоклип има по-висок приоритет, той ще се показва по-високо в избора на източник. Сборът от приоритета на източника и приоритета на качеството е приоритетът на видеото. +\n +\nИзточник A: 3 +\nКачество B: 7 +\nЩе има комбиниран видео приоритет от 10. +\n +\nЗАБЕЛЕЖКА: Ако сумата е 10 или повече, играчът автоматично ще пропусне зареждането, когато тази връзка се зареди! Замени Замени Всички - Изглежда, че потенциално дублиран елемент вече съществува във вашата библиотека: „%s“. \n \nИскате ли все пак да добавите този елемент, да замените съществуващия или да отмените действието? - Във вашата библиотека са намерени потенциални дублиращи се елементи: \n \n%s \n \nИскате ли все пак да добавите този елемент, да замените съществуващите или да отмените действието? + Изглежда, че потенциално дублиран елемент вече съществува във вашата библиотека: „%s“. +\n +\nИскате ли все пак да добавите този елемент, да замените съществуващия или да отмените действието? + Във вашата библиотека са намерени потенциални дублиращи се елементи: +\n +\n%s +\n +\nИскате ли все пак да добавите този елемент, да замените съществуващите или да отмените действието? Заключи Профил Вкарай Сегашен ПИН Управлявай Профили diff --git a/app/src/main/res/values-b+bn/strings.xml b/app/src/main/res/values-b+bn/strings.xml index 19974c7d2..87aa8e7eb 100644 --- a/app/src/main/res/values-b+bn/strings.xml +++ b/app/src/main/res/values-b+bn/strings.xml @@ -1,9 +1,9 @@ পোস্টার - CloudStream দিয়ে প্লে করুন + ক্লাউডস্ট্রিম দিয়ে চালান হোম - অভিনয়ে: %s + অভিনয়েঃ %s %1$dদিন %2$dঘন্টা %3$dমিনিট %1$dঘন্টা %2$dমিনিট %d মিনিট @@ -15,25 +15,26 @@ ব্যাকগ্রাউন্ড দেখান গতি (%.2f গুণ) মূল্যায়নঃ %.1f - নতুন আপডেট এসেছে! \n%1$s -> %2$s + নতুন আপডেট এসেছে! +\n%1$s -> %2$s ফিলার %d মিনিট ক্লাউডস্ট্রিম খুঁজুন ডাউনলোডসমূহ সেটিংস - %1$s পর্ব %2$d + %1$s এপি %2$d পর্ব %d মুক্তির তারিখ শেয়ার ব্রাউজারে খুলুন জনরা খুঁজুন… খুঁজুন %s… - মুভি প্লে করুন - লাইভস্ট্রিম প্লে করুন + মুভি চালান + স্ট্রিম চালান পিছনে যান কোন তথ্য নেই - আরও বিকল্প + আরো অপশন পরবর্তী পর্ব লোডিং বাদ দিন লোডিং… @@ -47,7 +48,7 @@ উৎসসমূহ সাবটাইটেলসমূহ আবার সংযোগ দেওয়ার চেষ্টা করুন… - পর্ব প্লে করুন + পর্ব চালান ডাউনলোড ডাউনলোড হয়েছে ডাউনলোড চলছে @@ -59,11 +60,11 @@ লিংক লোডিং ব্যর্থ ডাব সাব - ফাইল প্লে করুন - ডাউনলোড পজ করুন - আরও তথ্য - লুকান - প্লে + ফাইল চালান + ডাউনলোড থামান + আরো তথ্য + বন্ধ করুন + চালান তথ্য বুকমার্কসমূহ বাদ দিন @@ -71,7 +72,7 @@ প্রয়োগ করুন কপি করুন বন্ধ করুন - পরিষ্কার করুন + মুছুন সংরক্ষণ করুন ভিডিও এর গতি ফন্ট আউটলাইন কালার @@ -85,8 +86,8 @@ সাবটাইটেল ভাষা প্রাথমিক সেটিংসে নিতে চেপে রাখুন দেখতে থাকুন - সরান - আরও তথ্য + মুছুন + আরো তথ্য বিবরণ কোন প্লট পাওয়া যায়নি কোন বিবরণ পাওয়া যায়নি @@ -97,7 +98,7 @@ ইন্টারনাল স্টোরেজ বুকমার্ক ফিল্টার করুন ফাইল মুছুন - ডাউনলোড পুনরায় চালু করুন + ডাউনলোড চালু করুন সাবটাইটেল সেটিংস সাবটাইটেল ব্যাকগ্রাউন্ড কালার লেখার রং @@ -131,8 +132,8 @@ স্টোরেজ এর অনুমতি অনুপস্থিত। দয়া করে আবার চেষ্টা করুন। তথ্য ট্রেইলার প্রদর্শন করুন - Kitsu থেকে পোস্টারসমূহ প্রদর্শন করুন - স্বয়ংক্রিয় প্লাগইন আপডেট + কিটসু হতে পোস্টারসমূহ প্রদর্শন করুন + স্বয়ংক্রিয়ভাবে প্লাগিন এর হালনাগাদ স্বয়ংক্রিয়ভাবে প্লাগিনসমুহের ডাউনলোড হালনাগাদ ও ব্যাকআপ অ্যাপ এর হালনাগাদ দেখান @@ -148,17 +149,18 @@ সামনে বা পিছনের দিকে যেতে ডান বা বাম দিকে দুবার আলতো চাপুন ফাইল ডিলিট মান ডিফল্ট এ রিসেট করুন - স্টার্টআপে নতুন আপডেটের জন্য স্বয়ংক্রিয়ভাবে অনুসন্ধান করুন যোগ। - মুভি - অ্যানিমে ফিলার পর্ব প্রদর্শন + স্টার্টআপে নতুন আপডেটের জন্য স্বয়ংক্রিয়ভাবে অনুসন্ধান করুন + সিনেমা + এনিমে এর ফিলার পর্ব দেখায় টিভি সিরিজ লিংক পাওয়া যায়নি চা খাওয়ানো হয়েছে +৩০ - মুভিসমূহ + সিনেমা ডিসকর্ডে যোগ দিন টরেন্টস - এটি স্থায়ীভাবে মুছে ফেলা হবে %s \nআপনি কি নিশ্চিত? + এটি স্থায়ীভাবে মুছে ফেলা হবে %s +\nআপনি কি নিশ্চিত? থামুন -৩০ গিটহাব @@ -173,8 +175,8 @@ যোগ করা রিপোজিটরি থেকে এখনও ইনস্টল করা হয়নি এমন সব প্লাগইন স্বয়ংক্রিয়ভাবে ইনস্টল করুন। ডিলিট শুরু - কার্টুনসমূহ - Some devices do not support the new package installer. Try the legacy option if updates do not install। + কার্টুন + কিছু ফোন নতুন প্যাকেজ ইনস্টলার সাপোর্ট করে না। যদি আপডেটগুলি ইনস্টল না হয় তবে পুরোনো পদ্ধতি ব্যবহার করে দেখুন। সাবটাইটেল নেই এই প্রোভাইডার ক্রোমকাস্ট সাপোর্ট করে না উন্নত অনুসন্ধান @@ -182,7 +184,8 @@ ব্যবহৃত লাইব্রেরী আমাদের তৈরি ছোট উপন্যাস পড়ার অ্যাপ্লিকেশন - %d মি \nবাকি + %d মি +\nবাকি অন্যান্য চলমান এশিয়ান নাটক @@ -204,14 +207,14 @@ আমাদের তৈরি Anime দেখার অ্যাপ্লিকেশন 18+ এনিমে - অনুসন্ধানের ফলে নির্বাচিত ভিডিওর মান লুকান + অনুসন্ধান ফলাফলে নির্বাচিত ভিডিও কুয়ালিটি লুকান অ্যাপ লাইভস্ট্রিম APK ইনস্টলার সময়কাল অ্যাপের ভাষা পাস করেছে - পর্বসমূহ + পর্ব %s ব্যাক আপ করতে সমস্যা হয়েছে সাইট সিজন @@ -220,10 +223,10 @@ আপনার বর্তমান পর্বের অগ্রগতি স্বয়ংক্রিয়ভাবে সিঙ্ক করুন প্লাগইন ডাউনলোড ফিল্টার করতে মোড নির্বাচন করুন লিঙ্ক পুনরায় লোড হয়েছে - অ্যাকাউন্ট পরিবর্তন + সুইচ অ্যাকাউন্ট দাবিত্যাগ এশিয়ান ড্রামা - উৎস + সোর্স এক্সটেনশন লিংকস এনএসএফডব্লিউ @@ -256,7 +259,7 @@ টিভি লেআউট ব্যবহারকারীর নাম %1$d-%2$d - নতুন সাইটের নাম + NewSiteName ডিফল্ট OVA ওভিয়ে @@ -302,12 +305,13 @@ নতুন এপিসোডের নোটিফিকেশন অন্য এক্সটেনশনের মধ্যে খুঁজুন কোন আপডেট পাওয়া যায়নি - অ আ ক খ ১২৩ + password123 আসছে %s সময়ের মধ্যে বাতিল করুন - %s \nঅবশিষ্ট + %s +\nঅবশিষ্ট লাইভ স্ট্রিম - উৎস ত্রুটি + সোর্স সমস্যা রিমোট সমস্যা রেন্ডারের সমস্যা ডাউনলোডের সমস্যা, স্ট্রোরেজদের পারমিশন চেক করুন @@ -322,7 +326,7 @@ আপডেট ওয়াইফাই তে যে কোয়ালিটিতে দেখতে চান মোবাইল ডাটায় যে কোয়ালিটিতে দেখতে চান - প্লেয়ারের তথ্য দেখুন + ভিডিও প্লেয়ারে রেজুলেশন ভিডিও বাফার সাইজ ভিডিও বাফার লেনথ যখন আইএসপি ব্লক করবে তখন কার্যকরী @@ -333,7 +337,7 @@ বড় করুন অ্যাপ আপডেট গ্যাসচার - ডিফল্ট সেটিংস + ডিফল্ট সাধারণ এলোমেলো বোতাম পছন্দের মিডিয়া @@ -350,419 +354,7 @@ %1$dঘন্টা %2$dমিনিট %3$dসেকেন্ড %1$dমিনিট %2$dসেকেন্ড %1$dসেকেন্ড - শুরু থেকে প্লে করুন + শুরু থেকে চালু করুন স্পিচ রিকগনিশন উপলব্ধ নেই কথা বলা শুরু করুন… - ডাউনলোড তালিকা - সম্পূর্ণ সিরিজ প্লে করুন - এই ভিডিওটি একটি টরেন্ট, অর্থাৎ আপনার ভিডিও অ্যাক্টিভিটি ট্র্যাক করা সম্ভব।\nএগোনোর আগে টরেন্টিং সম্পর্কে ভালোভাবে বুঝে নিন। - ডিলিট করার জন্য আইটেম সিলেক্ট করুন - বর্তমানে কোনো ডাউনলোড নেই। - বর্তমানে কোনো ডাউনলোড অপেক্ষায় নেই। - অফলাইনে দেখার জন্য উপলব্ধ - সব সিলেক্ট করুন - সব ডিসিলেক্ট করুন - লোকাল ভিডিও খুলুন - অতিরিক্ত ব্রাইটনেস - ডিসপ্লে ব্রাইটনেস ১০০% ছাড়িয়ে গেলে ব্রাইটনেস ফিল্টার চালু করুন - সার্চ সাজেশন - টাইপ করার সময় সার্চ সাজেশন দেখান - সাজেশন মুছে ফেলুন - প্লেয়ার মেটাডেটা ওভারলে প্রদর্শন - কাস্ট প্যানেল প্রদর্শন - প্রাক-মুক্তি সংস্করণ ইনস্টলেশন - প্রাক-মুক্তি সংস্করণ ইতোমধ্যে ইনস্টল রয়েছে। - প্রাক-মুক্তি সংস্করণ ইনস্টল করতে সমস্যা হয়েছে। - (%1$d | %2$s) মুছুন - সাবধান - আপনি কি নিম্নলিখিত আইটেমগুলো স্থায়ীভাবে মুছতে চান?\n\n%s - ফাইলগুলো মুছুন - আপনি কি নিশ্চিত যে আপনি %1$s-এ নিচের এপিসোডগুলো স্থায়ীভাবে মুছে ফেলতে চান?\n\n%2$s - আপনি নিচের সিরিজগুলোর সব এপিসোডও স্থায়ীভাবে মুছে ফেলবেন:\n\n%s - আপনি কি নিশ্চিত যে আপনি নিচের সিরিজটির সব এপিসোড স্থায়ীভাবে মুছে ফেলতে চান?\n\n%s - সঙ্গীত - অডিও বই - মিডিয়া - অডিও - পডকাস্ট - ভিডিও - এনকোডিং ত্রুটি - অসমর্থিত ত্রুটি - কাস্ট মিরর - প্লে মিরর" - রেটিং লেবেল - পর্বের বিবরণ - প্লেয়ারে প্রদর্শিত - স্কিপের পরিমাণ - প্লেয়ার দৃশ্যমান থাকলে যে সময় পরিমাণ স্কিপ করা হয় - GitHub-এর সাথে সংযোগ করা যায়নি। jsDelivr প্রক্সি চালু করা হচ্ছে… - jsDelivr ব্যবহার করে GitHub-এর সরাসরি লিংক ব্লক করা এড়িয়ে যান। এতে আপডেট কয়েকদিন বিলম্বিত হতে পারে। - নিরাপত্তা - অ্যাকাউন্টসমূহ - লাইভ ঘড়ি দেখান - পর্দার উপরের অংশে লাইভ ঘড়ি দেখায়। হোমপেজ ও প্লেয়ারে প্রযোজ্য - %1$s %2$s - অতিরিক্ত_উজ্জ্বলতা_সক্রিয় - স্থানীয়ভাবে ব্যবহারকারী যাচাই - ট্র্যাকিং যোগ করা - সমন্বয় - মূল্যায়ন - %s ব্যবহারকারী প্রমাণিত - %s যোগ করা হয়েছে - /%d - %s-এ লগইন করা যায়নি - কোনোটি নয় - স্বাভাবিক - সব - সর্বোচ্চ - সর্বনিম্ন - সীমারেখা - ছায়া - উত্থিত - সাবটাইটেল সিঙ্ক করুন - ১০০০ মিলিসেকেন্ড - সাবটাইটেল বিলম্ব - সাবটাইটেল যদি %d মিলিসেকেন্ড আগে দেখায় তবে এটি ব্যবহার করুন - প্রান্ত - Logcat 🐈 - ভিডিওগুলি বিরত অবস্থায় চালু করুন - উৎস নির্বাচন করার সময় চাপ কমান - %d / ১০ - /?? - নিষ্ক্রিয় করুন - অবদমিত - সাবটাইটেল %d মিলিসেকেন্ড দেরিতে প্রদর্শিত হলে এটি ব্যবহার করুন - সাবটাইটেলে কোনো বিলম্ব নেই - ঐ ভোরে ক্ষুদ্র কৃষক গৌরবী ঋষির সঙ্গে জ্ঞানী ছাত্রকে নিয়ে চাঁদ, ঝর্ণা, ঠাকুর, ডাল, ঢেঁকি, তীর, ধান, পাখি, ফুল, বাগান, ভ্রমর, মেঘ, যাত্রা, রথ, লতা, শঙ্খ, ষাঁড়, সাপ ও হরিণের গল্প শোনাল। অ আ ই ঈ উ ঊ ঋ এ ঐ ও ঔ ক খ গ ঘ ঙ চ ছ জ ঝ ঞ ট ঠ ড ঢ ণ ত থ দ ধ ন প ফ ব ভ ম য র ল শ ষ স হ ক্ষ জ্ঞ শ্র - প্রস্তাবিত - %s লোড হয়েছে - ফাইল থেকে লোড করুন - ইন্টারনেট থেকে লোড করুন - প্রথম উপলভ্যটি লোড করুন - ডাউনলোড করা ফাইল - প্রধান - পার্শ্ব চরিত্র - পটভূমি চরিত্র - উৎস - এলোমেলো - শীঘ্রই আসছে… - Cam - CamRip - CamHD - HQ - HD - TS - TC - Blu-ray - WP - DVD - 4K - SD - UHD - HDR - SDR - Web - পোস্টার ছবি - QR কোড ছবি - প্লেয়ার - রেজোলিউশন ও শিরোনাম - শিরোনাম - রেজোলিউশন - মিডিয়া তথ্য - অবৈধ আইডি - অবৈধ ডেটা - অবৈধ URL - ত্রুটি - সাবটাইটেল থেকে ক্লোজড ক্যাপশন সরান - সাবটাইটেল থেকে বিজ্ঞাপন সরান - পছন্দের মিডিয়া ভাষা অনুসারে ফিল্টার করুন - অতিরিক্ত - ট্রেলার - https://example.com/example.mp4 - রেফারার (ঐচ্ছিক) - পরবর্তী - এই ভাষাগুলোতে ভিডিও দেখুন - পূর্ববর্তী - সেটআপ এড়িয়ে যান - আপনার ডিভাইসের উপযোগী করতে অ্যাপের লেআউট পরিবর্তন করুন - আপনি কী দেখতে চান - সম্পন্ন - এক্সটেনশনসমূহ - রিপোজিটরি যোগ করুন - রিপোজিটরির নাম (ঐচ্ছিক) - রিপোজিটরির URL বা শর্টকোড - প্লাগইন লোড হয়েছে - প্লাগইন ডাউনলোড হয়েছে - প্লাগইন মুছে ফেলা হয়েছে - %s লোড করা যায়নি - ১৮+ - %1$d %2$s ডাউনলোড শুরু হয়েছে… - %1$d %2$s ডাউনলোড সম্পন্ন - সকল %s ইতোমধ্যে ডাউনলোড করা হয়েছে - রিপোজিটরিতে কোনো প্লাগইন পাওয়া যায়নি - রিপোজিটরি পাওয়া যায়নি, URL পরীক্ষা করুন এবং VPN ব্যবহার করে দেখুন - একসাথে ডাউনলোড - প্লাগইন - প্লাগইনসমূহ - এটি রিপোজিটরির সমস্ত প্লাগইনও মুছে ফেলবে - রিপোজিটরি মুছুন - প্লাগইন মুছুন - আপনি যে সাইটগুলো ব্যবহার করতে চান তার তালিকা ডাউনলোড করুন - ডাউনলোড করা হয়েছে: %d - নিষ্ক্রিয়: %d - ডাউনলোড করা হয়নি: %d - %dটি প্লাগইন আপডেট করা হয়েছে - CloudStream-এ ডিফল্টভাবে কোনো সাইট ইনস্টল করা থাকে না। আপনাকে রিপোজিটরি থেকে সাইটগুলো ইনস্টল করতে হবে।\n\nআমাদের Discord-এ যুক্ত হোন অথবা অনলাইনে অনুসন্ধান করুন। - কমিউনিটি রিপোজিটরিগুলো দেখুন - পাবলিক তালিকা - সব সাবটাইটেল বড় হাতের অক্ষরে দেখান - সতর্কতা: তৃতীয় পক্ষের এক্সটেনশন ব্যবহারের জন্য CloudStream কোনো দায়িত্ব নেয় না এবং সেগুলোর জন্য কোনো সহায়তা প্রদান করে না! - %s (নিষ্ক্রিয়) - ট্র্যাকসমূহ - কম্প্রেসর - ডায়নামিক রেঞ্জ কম্প্রেসর - প্লেয়ারে কম্প্রেসর নিয়ন্ত্রণ প্রদর্শন করুন (উচ্চ ও মৃদু অডিওর মধ্যে ভারসাম্য বজায় রাখতে সাহায্য করে) - চালু - চালু · %1$d dB / +%2$d dB - বন্ধ - হালকা - সংলাপ বৃদ্ধি - ভারী - থ্রেশহোল্ড: %d dB - মেকআপ গেইন: +%d dB - %d dB-এর চেয়ে উচ্চ শব্দগুলোর মাত্রা কমিয়ে দেওয়া হবে - সামগ্রিক ভলিউম +%d dB বৃদ্ধি করে - অডিও ট্র্যাক - ভিডিও ট্র্যাক - পরিবর্তন দেখতে অ্যাপটি রিস্টার্ট করুন। - রিস্টার্ট - থামান - সেফ মোড চালু - ক্র্যাশের কারণে সমস্ত এক্সটেনশন বন্ধ রাখা হয়েছে যাতে আপনি সমস্যা সৃষ্টিকারী এক্সটেনশনটি শনাক্ত করতে পারেন। - ক্র্যাশ সংক্রান্ত তথ্য দেখুন - রেটিং: %s - বিবরণ - সংস্করণ - অবস্থা - আকার - লেখকবৃন্দ - সমর্থিত - ভাষা - প্রথমে এক্সটেনশনটি ইনস্টল করুন - HLS প্লেলিস্ট - পছন্দের ভিডিও প্লেয়ার - অভ্যন্তরীণ প্লেয়ার - সর্বদা জিজ্ঞাসা করুন - কাস্ট ডিভাইস নির্বাচন করুন - অ্যাপটি পাওয়া যায়নি - সকল ভাষা - %s এড়িয়ে যান - ওপেনিং - এন্ডিং - রিল্যাপ - মিশ্র এন্ডিং - মিশ্র ওপেনিং - ক্রেডিট - প্রিভিউ - ইন্ট্রো - হিস্ট্রি পরিষ্কার করুন - ওপেনিং/এন্ডিং এড়িয়ে যাওয়ার পপআপ দেখান - অত্যধিক টেক্সট। ক্লিপবোর্ডে সংরক্ষণ করা সম্ভব নয়। - ক্লিপবোর্ড অ্যাক্সেস করতে ত্রুটি, অনুগ্রহ করে আবার চেষ্টা করুন। - কপি করতে ত্রুটি, অনুগ্রহ করে Logcat কপি করুন এবং অ্যাপ সাপোর্টে যোগাযোগ করুন। - দেখা হয়েছে হিসেবে চিহ্নিত করুন - দেখা হয়েছে তালিকা থেকে সরান - আপনি কি নিশ্চিতভাবে প্রস্থান করতে চান? - হ্যাঁ - না - ঠিক আছে - বাতিল - রিপোজিটরি খুলুন - ব্যাটারি অপ্টিমাইজেশন নিষ্ক্রিয় করুন - সাবস্ক্রাইব করা টিভি শোগুলোর নিরবচ্ছিন্ন ডাউনলোড এবং নোটিফিকেশন নিশ্চিত করতে CloudStream-এর ব্যাকগ্রাউন্ডে চলার অনুমতি প্রয়োজন। ঠিক আছে চাপলে একটি অনুমতির ডায়ালগ দেখানো হবে।\n\nঅনুগ্রহ করে \'অনুমতি দিন\' চাপুন।\n\nমনে রাখবেন, এই অনুমতির কারণে CS3 আপনার অতিরিক্ত ব্যাটারি খরচ করবে না। এটি কেবল প্রয়োজনের সময়ই ব্যাকগ্রাউন্ডে কাজ করবে, যেমন অফিসিয়াল এক্সটেনশন থেকে ভিডিও ডাউনলোড বা নোটিফিকেশন প্রাপ্তির সময়। - অ্যাপের ব্যাটারি ব্যবহার ইতোমধ্যে সীমাবদ্ধতাহীন হিসেবে সেট করা আছে - CloudStream-এর অ্যাপের তথ্য খোলা যাচ্ছে না। - অ্যাপ আপডেট ডাউনলোড হচ্ছে… - অ্যাপ আপডেট ইনস্টল হচ্ছে… - অ্যাপের নতুন সংস্করণ ইনস্টল করা যায়নি - লিগ্যাসি - প্যাকেজ ইনস্টলার - অ্যাপ থেকে প্রস্থান করার পর আপডেট করা হবে - সাজানোর ভিত্তি - সাজান - রেটিং (সর্বোচ্চ থেকে সর্বনিম্ন) - রেটিং (সর্বনিম্ন থেকে সর্বোচ্চ) - আপডেট (নতুন থেকে পুরোনো) - আপডেট (পুরোনো থেকে নতুন) - বর্ণানুক্রমিক (A থেকে Z) - বর্ণানুক্রমিক (Z থেকে A) - পর্ব (ক্রমবর্ধমান) - পর্ব (ক্রমহ্রাসমান) - রেটিং (সর্বোচ্চ) - রেটিং (সর্বনিম্ন) - মুক্তির তারিখ (নতুন) - মুক্তির তারিখ (পুরোনো) - পর্ব %s - রেটিং %s - তারিখ %s - লাইব্রেরি নির্বাচন করুন - এর মাধ্যমে খুলুন - আপনার লাইব্রেরি খালি :(\n\nএকটি লাইব্রেরি অ্যাকাউন্টে লগইন করুন অথবা আপনার স্থানীয় লাইব্রেরিতে শো যোগ করুন। - এই তালিকাটি খালি। অন্য একটিতে পরিবর্তন করে চেষ্টা করুন। - সেফ মোড ফাইল পাওয়া গেছে!\nফাইলটি না সরানো পর্যন্ত স্টার্টআপে কোনো এক্সটেনশন লোড করা হবে না। - পূর্বাবস্থায় ফেরান - সাবস্ক্রাইব করা শোগুলো আপডেট করা হচ্ছে - সাবস্ক্রাইব করা - %s সাবস্ক্রাইব করা হয়েছে - %s আনসাবস্ক্রাইব করা হয়েছে - পর্ব %d প্রকাশিত হয়েছে! - সাবস্ক্রাইব করুন - আনসাবস্ক্রাইব করুন - প্রোফাইল %d - প্রোফাইল সেটিংস - নেতিবাচক অগ্রাধিকারযুক্ত উৎসগুলো লুকান - ত্রুটিযুক্ত উৎসগুলো লুকান - - %dটি লুকানো লিঙ্ক - %dটি লুকানো লিঙ্ক - - Wi-Fi - মোবাইল ডেটা - ডিফল্ট হিসেবে সেট করুন - ব্যবহার করুন - সম্পাদনা - উৎসের অগ্রাধিকার - প্লেয়ারে ভিডিওর উৎসগুলো কীভাবে সাজানো হবে তা নির্ধারণ করুন - প্রোফাইলসমূহ - সাহায্য - এখানে আপনি উৎসগুলোর অগ্রাধিকার ক্রম পরিবর্তন করতে পারেন। কোনো ভিডিওর অগ্রাধিকার বেশি হলে সেটি উৎস নির্বাচনের তালিকায় সবার উপরে থাকবে।\n\nউৎসের অগ্রাধিকার এবং কোয়ালিটির অগ্রাধিকারের যোগফল হলো ভিডিওর অগ্রাধিকার।\n\nউৎস A: 3\nকোয়ালিটি B: 7\nসম্মিলিত ভিডিও অগ্রাধিকার হবে 10।\n\nমনে রাখবেন: যোগফল 10 বা তার বেশি হলে সেই লিঙ্ক লোড হওয়ার সময় প্লেয়ার স্বয়ংক্রিয়ভাবে লোডিং এড়িয়ে যাবে! - কোয়ালিটিসমূহ - প্রোফাইলের ব্যাকগ্রাউন্ড - UI সঠিকভাবে তৈরি করা যায়নি, এটি একটি গুরুতর ত্রুটি এবং অবিলম্বে রিপোর্ট করা উচিত %s - আপনি ইতোমধ্যে ভোট দিয়েছেন - প্রিয় তালিকা - %s প্রিয় তালিকায় যোগ করা হয়েছে - %s প্রিয় তালিকা থেকে সরানো হয়েছে - প্রিয় তালিকায় যোগ করুন - প্রিয় তালিকা থেকে সরান - সম্ভাব্য ডুপ্লিকেট পাওয়া গেছে - যোগ করুন - প্রতিস্থাপন করুন - সব প্রতিস্থাপন করুন - আপনার লাইব্রেরিতে ইতোমধ্যে একটি অনুরূপ আইটেম রয়েছে বলে মনে হচ্ছে: \'%s।\'\n\nআপনি কি তবুও এই আইটেমটি যোগ করতে চান, বিদ্যমান আইটেমটি প্রতিস্থাপন করতে চান, নাকি বাতিল করতে চান? - আপনার লাইব্রেরিতে সম্ভাব্য ডুপ্লিকেট আইটেম পাওয়া গেছে:\n\n%s\n\nআপনি কি তবুও এই আইটেমগুলো যোগ করতে চান, বিদ্যমানগুলো প্রতিস্থাপন করতে চান, নাকি বাতিল করতে চান? - PIN লিখুন - %s-এর জন্য PIN লিখুন - বর্তমান PIN লিখুন - প্রোফাইল লক করুন - PIN - ভুল PIN। অনুগ্রহ করে আবার চেষ্টা করুন। - PIN অবশ্যই ৪ অক্ষরের হতে হবে - একটি অ্যাকাউন্ট নির্বাচন করুন - কোনো অ্যাকাউন্ট নেই - অ্যাকাউন্ট পরিচালনা করুন - অ্যাকাউন্ট সম্পাদনা করুন - %s হিসেবে লগইন আছেন - স্টার্টআপে অ্যাকাউন্ট নির্বাচন এড়িয়ে যান - ডিফল্ট অ্যাকাউন্ট ব্যবহার করুন - ঘোরান - স্ক্রিন ওরিয়েন্টেশনের জন্য একটি টগল বাটন প্রদর্শন করুন - ভিডিওর অনুপাতের উপর ভিত্তি করে স্ক্রিন স্বয়ংক্রিয়ভাবে ঘোরানো সক্রিয় করুন - অটো রোটেট - প্রিয় - প্রিয় থেকে সরান - CloudStream আনলক করুন - বায়োমেট্রিক দিয়ে লক করুন - পাসওয়ার্ড/PIN প্রমাণীকরণ - এই ডিভাইসে বায়োমেট্রিক প্রমাণীকরণ সমর্থিত নয় - ফিঙ্গারপ্রিন্ট, Face ID, PIN, প্যাটার্ন ও পাসওয়ার্ড দিয়ে অ্যাপ আনলক করুন। - কয়েকবার ব্যর্থ প্রচেষ্টার পর প্রম্পটটি বন্ধ হয়ে যাবে। পুনরায় চেষ্টা করতে কেবল অ্যাপটি রিস্টার্ট করুন। - আপনার CloudStream ডেটা এখন ব্যাকআপ করা হয়েছে। এমনটি ঘটার সম্ভাবনা অত্যন্ত কম হলেও প্রতিটি ডিভাইস ভিন্নভাবে আচরণ করতে পারে। বিরল ক্ষেত্রে যদি অ্যাপ অ্যাক্সেস করা থেকে লক হয়ে যান, তবে সম্পূর্ণ অ্যাপ ডেটা পরিষ্কার করুন এবং ব্যাকআপ থেকে পুনরুদ্ধার করুন। এর ফলে উদ্ভূত যেকোনো অসুবিধার জন্য আমরা আন্তরিকভাবে দুঃখিত। - রিসেট - CloudStream উইকি - ভিজিট করুন - ডিভাইসের PIN কোড পাওয়া যাচ্ছে না, স্থানীয় প্রমাণীকরণ চেষ্টা করুন - PIN কোডের মেয়াদ শেষ হয়েছে! - কোডের মেয়াদ শেষ হতে বাকি: %1$dm %2$ds - মুক্তির তারিখ (নতুন থেকে পুরোনো) - মুক্তির তারিখ (পুরোনো থেকে নতুন) - প্লেয়ার কন্ট্রোলের নাম লুকান - সিকবার প্রিভিউ - সিকবারে থাম্বনেইল প্রিভিউ সক্রিয় করুন - এখনো কোনো সাবটাইটেল লোড হয়নি - ব্যাকআপ ফোল্ডারের অবস্থান - কাস্টম - প্রস্থানের আগে নিশ্চিত করুন - অ্যাপ বন্ধ করার আগে নিশ্চিতকরণ ডায়ালগ দেখান - দেখান - দেখাবেন না - এজের আকার - সেটিংস/প্রোভাইডার/পছন্দের মিডিয়া-তে গিয়ে টরেন্ট সক্রিয় করুন - এগিয়ে যেতে অ্যাপটি রিস্টার্ট করুন এবং স্ট্রিম টরেন্ট পপ-আপ গ্রহণ করুন। - সফটওয়্যার ডিকোডিং - সফটওয়্যার ডিকোডিংয়ের মাধ্যমে আপনার ডিভাইসে অসমর্থিত ভিডিও ফাইলগুলো প্লে করা সম্ভব, তবে উচ্চ রেজোলিউশনে প্লেব্যাক ল্যাগি বা অস্থিতিশীল হতে পারে। - ভলিউম ১০০% অতিক্রম করেছে - ১০০%-এর বেশি বাড়াতে আবার উপরে স্লাইড করুন - প্লাগইন আপডেট করুন - ম্যানুয়ালি প্লাগইন আপডেট করুন - প্লাগইন আপডেট প্রক্রিয়া শুরু হচ্ছে! - সফলভাবে %dটি প্লাগইন আপডেট করা হয়েছে - কোনো প্লাগইন আপডেট করা হয়নি। - প্লেয়ার বিজ্ঞপ্তি - ব্যাকগ্রাউন্ড থেকে প্লেব্যাক নিয়ন্ত্রণ করার জন্য প্লেয়ার নোটিফিকেশন - এমবেডেড - অনলাইন - সব সাবটাইটেল বোল্ড করুন - সব সাবটাইটেল ইটালিক করুন - ব্যাকগ্রাউন্ডের ব্যাসার্ধ - ব্যাকগ্রাউন্ড - একসাথে সমান্তরালভাবে কতগুলো আলাদা আইটেম ডাউনলোড করা যাবে - সমান্তরাল ডাউনলোড - একযোগে সংযোগ - ডাউনলোড করার সময় প্রতিটি ডাউনলোড একযোগে কতগুলো সংযোগ ব্যবহার করতে পারবে - ডাউনলোডে যান - কোনো ইন্টারনেট সংযোগ নেই।\n\nঅনুগ্রহ করে ইন্টারনেটে সংযুক্ত হয়ে আবার চেষ্টা করুন, অথবা অফলাইনে আপনার ডাউনলোড করা ফাইলগুলো দেখুন। - স্ক্রিনের সীমানা পরিবর্তন করে - ওভারস্ক্যান - পোস্টারের আকার পরিবর্তন করে - পোস্টারের আকার - চাপ দিয়ে গতি বাড়ানো - ২ গুণ গতি পেতে চেপে ধরে রাখুন - প্রোফাইল ছবি সম্পাদনা করুন - URL - ছবি নির্বাচক - ডিফল্ট ছবিসমূহ - প্রোফাইল ছবির URL লিখুন - কোনো URL পাওয়া যায়নি - অবৈধ URL বা ছবি - ছবি সফলভাবে আপডেট করা হয়েছে - এই পর্ব পর্যন্ত দেখা হয়েছে হিসেবে চিহ্নিত করুন - এই পর্ব পর্যন্ত দেখা তালিকা থেকে সরান - পুনরায় লোড করা হয়েছে - প্রোভাইডার পুনরায় লোড করুন - নাম - উৎসের নাম - রেজোলিউশন ও নাম - সব ডাউনলোড করুন - সব বাতিল করুন - আপনি কি %s নম্বর পর্ব ডাউনলোড করতে চান? - আপনি কি কিউতে থাকা সমস্ত ডাউনলোড বাতিল করতে চান? - সাবটাইটেল প্রান্তিককরণ - নিচে বামে - নিচে মাঝে - নিচে ডানে - মাঝে বামে - একেবারে মাঝে - মাঝে ডানে - উপরে বামে - উপরে মাঝে - উপরে ডানে - - %dটি সক্রিয় ডাউনলোড - %dটি সক্রিয় ডাউনলোড - - - %dটি ডাউনলোড কিউতে আছে - %dটি ডাউনলোড কিউতে আছে - - সরাসরি diff --git a/app/src/main/res/values-b+cs/strings.xml b/app/src/main/res/values-b+cs/strings.xml index 4747ecf52..2ee23c1c5 100644 --- a/app/src/main/res/values-b+cs/strings.xml +++ b/app/src/main/res/values-b+cs/strings.xml @@ -16,7 +16,8 @@ Rychlost (%.2fx) Hodnocení: %.1f - Nalezena nová aktualizace! \n%1$s -> %2$s + Nalezena nová aktualizace! +\n%1$s -> %2$s Výplň %d min CloudStream @@ -171,8 +172,10 @@ Pokračovat -30 +30 - Toto nevratně smaže %s \nJste si jisti? - %dm \nzbývá + Toto nevratně smaže %s +\nJste si jisti? + %dm +\nzbývá Probíhající Dokončena Stav @@ -322,8 +325,8 @@ Náhodný Již brzy… Kam - CamRip - CamHD + Kam + Kam HQ HD TS @@ -344,7 +347,7 @@ Rozlišení Neplatné ID Odstranit skryté titulky z titulků - Odstranit reklamy z titulků + Odstranit nadbytečné titulky Extra Označit jako zhlédnuté Historie @@ -413,7 +416,9 @@ Doplněk stažen 18+ Spuštěno stahování %1$d %2$s… - CloudStream nemá ve výchozím nastavení nainstalované žádné zdroje. Je třeba je nainstalovat z repozitářů. \n \nPřipojte se na náš Discord nebo hledejte na internetu. + CloudStream nemá ve výchozím nastavení nainstalované žádné zdroje. Je třeba je nainstalovat z repozitářů. +\n +\nPřipojte se na náš Discord nebo hledejte na internetu. Zakázáno: %d Aktualizováno %d doplňků Zobrazit informace o pádu @@ -441,7 +446,8 @@ Nepodařilo se nainstalovat novou verzi aplikace Původní Aplikace bude po ukončení aktualizována - Vaše knihovna je prázdná :( \nPřihlaste se k účtu v knihovně nebo přidejte pořady do místní knihovny. + Vaše knihovna je prázdná :( +\nPřihlaste se k účtu v knihovně nebo přidejte pořady do místní knihovny. Vybrat knihovnu Hodnocení (od nejvyššího) Hodnocení (od nejnižšího) @@ -449,7 +455,8 @@ Seřadit podle Řazení Tento seznam je prázdný. Zkuste přepnout na jiný. - Nalezen soubor bezpečného režimu! \nDo odebrání souboru nebudeme načítat žádná rozšíření. + Nalezen soubor bezpečného režimu! +\nDo odebrání souboru nebudeme načítat žádná rozšíření. Aktualizováno (od nejnovějšího) Aktualizováno (od nejstaršího) Abecedně (od A do Z) @@ -530,7 +537,13 @@ Nápověda Kvality Pozadí profilu - Zde můžete změnit pořadí zdrojů. Pokud má video vyšší prioritu, objeví se ve výběru zdrojů výše. Součet priority zdroje a priority kvality je priorita videa. \n \nZdroj A: 3 \nKvalita B: 7 \nBudou mít celkovou prioritu videa 10. \n \nPOZNÁMKA: Pokud je součet 10 nebo vyšší, přehrávač automaticky přeskočí načítání při načtení daného odkazu! + Zde můžete změnit pořadí zdrojů. Pokud má video vyšší prioritu, objeví se ve výběru zdrojů výše. Součet priority zdroje a priority kvality je priorita videa. +\n +\nZdroj A: 3 +\nKvalita B: 7 +\nBudou mít celkovou prioritu videa 10. +\n +\nPOZNÁMKA: Pokud je součet 10 nebo vyšší, přehrávač automaticky přeskočí načítání při načtení daného odkazu! Nepodařilo se správně vytvořit rozhraní. Toto je VÁŽNÁ CHYBA, kterou je potřeba ihned nahlásit %s Vypnout Výběr režimu pro filtrování stahování doplňků @@ -540,7 +553,11 @@ %s odebráno z oblíbených Oblíbené %s přidáno do oblíbených - Ve vaší knihovně byl nalezen potenciální duplikát: \n \n%s \n \nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci? + Ve vaší knihovně byl nalezen potenciální duplikát: +\n +\n%s +\n +\nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci? Frekvence záloh Nalezena potenciální duplicita Zamknout profil @@ -554,7 +571,9 @@ Odebírat Odebrat z oblíbených Vyberte účet - Vypadá to, že ve vaší knihovně již existuje potenciální duplikát: „%s“. \n \nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci? + Vypadá to, že ve vaší knihovně již existuje potenciální duplikát: „%s“. +\n +\nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci? Zadejte PIN PIN Zadejte současný PIN @@ -583,7 +602,8 @@ Po několika nezdařilých pokusech se okno zavře. Pro opětovný pokus restartujte aplikaci. Vaše data z aplikace CloudStream byla nyní zálohována. Ačkoli je tato možnost velmi malá, různá zařízení se mohou chovat různě. Ve výjimečném případě, že se vám přístup k aplikaci zablokuje, data aplikace zcela vymažte a obnovte je ze zálohy. Velmi se omlouváme za případné nepříjemnosti z toho plynoucí. Odebrat z oblíbených - %s \nzbývá + %s +\nzbývá Přidat do oblíbených Název a adresa repozitáře Chyba při kopírování, zkopírujte prosím protokol a kontaktujte podporu aplikace. @@ -624,12 +644,20 @@ Zvolte položky k odstranění Dostupné pro sledování offline Vybrat vše - Opravdu chcete trvale odstranit následující položky? \n \n%s - Opravdu chcete trvale odstranit následující epizody v %1$s? \n \n%2$s - Opravdu chcete trvale odstranit všechny epizody v následujících sériích? \n \n%s + Opravdu chcete trvale odstranit následující položky? +\n +\n%s + Opravdu chcete trvale odstranit následující epizody v %1$s? +\n +\n%2$s + Opravdu chcete trvale odstranit všechny epizody v následujících sériích? +\n +\n%s Zrušit výběr všeho Odstranit soubory - Také trvale odstraníte všechny epizody v následujících sériích: \n \n%s + Také trvale odstraníte všechny epizody v následujících sériích: +\n +\n%s Odstranit (%1$d | %2$s) Náhled v liště přehrávače Povolit náhled miniatur na liště přehrávače @@ -660,7 +688,7 @@ Spouštím proces aktualizace doplňků! Nebyly aktualizovány žádné doplňky. Ep %s - Úspěšně aktualizováno %d doplňků + Úspěšně aktualizováno %d doplňků! Epizoda (sestupně) Hodnocení (nejnižší) Ručně aktualizovat doplňky @@ -751,39 +779,4 @@ Priorita zdrojů Rozhodněte, jak mají být řazeny zdroje videí v přehrávači Zobrazit překrytí metadat v přehrávači - Video - Náhled - Živě - Nastavení profilu - Skrýt zdroje s negativní prioritou - Skrýt zdroje s chybami - - %d skrytý odkaz - %d skryté odkazy - %d skrytých odkazů - %d skrytých odkazů - - Zobrazit hodiny s reálným časem - Zobrazit hodiny s reálným časem v horní části obrazovky. Platí pro domovskou obrazovku a přehrávač - Logcat 🐈 - Spustit videa pozastavená - Snížit stres při výběru zdrojů - Hrana - Pozadí - Kompresor - Kompresor dynamického rozsahu - Zobrazit ovládání kompresoru v přehrávači (pomáhá s vyvážením hlasitého/tichého zvuku) - Zapnuto - Zapnuto · %1$d dB / +%2$d dB - Vypnuto - Lehký - Zvýraznění dialogů - Silný - Práh: %d dB - Zvýšení zisku: +%d dB - Zvuky hlasitější než %d dB budou ztlumeny - Zvýší celkovou hlasitost o +%d dB - Adresa - Výběr obrázků - Výchozí obrázky diff --git a/app/src/main/res/values-b+de/strings.xml b/app/src/main/res/values-b+de/strings.xml index f00db9ab4..eb939ec5e 100644 --- a/app/src/main/res/values-b+de/strings.xml +++ b/app/src/main/res/values-b+de/strings.xml @@ -27,7 +27,8 @@ Hintergrundbildvorschau Geschwindigkeit (%.2fx) Bewertung: %.1f - Neues Update gefunden! \n%1$s -> %2$s + Neues Update gefunden! +\n%1$s -> %2$s Füller %d Min CloudStream @@ -187,8 +188,10 @@ Fortsetzen -30 +30 - Dadurch wird %s permanent gelöscht \nBist du dir sicher? - %dm \nverbleibend + Dadurch wird %s permanent gelöscht +\nBist du dir sicher? + %dm +\nverbleibend Laufend Abgeschlossen Status @@ -252,7 +255,7 @@ Update Bevorzugte Videoqualität (WLAN) Videoplayertitel max. Zeichen - Zeige Playerinformationen + Playerinformationen anzeigen Videopuffergröße Videopufferlänge Video-Cache in Speicher @@ -398,7 +401,9 @@ Heruntergeladen: %d Deaktiviert: %d Nicht heruntergeladen: %d - CloudStream hat standardmäßig keine Websites installiert. Websites müssen aus Repositories installiert werden. \n \nTrete unserem Discord Server bei oder suche online. + CloudStream hat standardmäßig keine Websites installiert. Websites müssen aus Repositories installiert werden. +\n +\nTrete unserem Discord Server bei oder suche online. Community-Repositories anzeigen Öffentliche Liste Alle Untertitel in Großbuchstaben @@ -479,9 +484,11 @@ Alphabetisch (Z zu A) Bibliothek auswählen Öffnen mit - Deine Bibliothek ist leer :( \nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu. + Deine Bibliothek ist leer :( +\nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu. Diese Liste ist leer. Versuch zu einer anderen Liste zu wechseln. - Datei für den abgesicherten Modus gefunden! \nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird. + Datei für den abgesicherten Modus gefunden! +\nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird. Player ausgeblendet - Betrag zum vor- und zurückspulen Der Betrag, welcher verwendet wird, wenn der Player eingeblendet ist Der Betrag, welcher verwendet wird, wenn der Player ausgeblendet ist @@ -515,7 +522,13 @@ Hilfe Qualitäten Profil-Hintergrund - Hier kannst du verändern, wie die Quellen geordnet werden. Wenn ein Video eine höhere Priorität hat, wird es höher in der Quellenauswahl erscheinen. Die Summe der Quellenpriorität und der Qualitätspriorität ist die Videopriorität. \n \nQuelle A: 3 \nQualität B: 7 \nWerden eine kombinierte Videopriorität von 10 haben. \n \nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird! + Hier kannst du verändern, wie die Quellen geordnet werden. Wenn ein Video eine höhere Priorität hat, wird es höher in der Quellenauswahl erscheinen. Die Summe der Quellenpriorität und der Qualitätspriorität ist die Videopriorität. +\n +\nQuelle A: 3 +\nQualität B: 7 +\nWerden eine kombinierte Videopriorität von 10 haben. +\n +\nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird! Filtermodus für Plugin-Downloads auswählen Es wurde bereits abgestimmt Keine Plugins im Repository gefunden @@ -547,8 +560,14 @@ Kontoauswahl beim Starten überspringen Konten verwalten Konto bearbeiten - Es wurden potentielle Duplikate in deiner Bibliothek gefunden: \n \n%s \n \nMöchtest du dieses Element dennoch hinzufügen, das existierende ersetzen oder diese Aktion abbrechen? - Deine Bibliothek enthält möglicherweise schon ein Duplikat dieses Elements: \'%s\' \n \nMöchtest du dieses Element dennoch hinzufügen, das existierende ersetzen oder diese Aktion abbrechen? + Es wurden potentielle Duplikate in deiner Bibliothek gefunden: +\n +\n%s +\n +\nMöchtest du dieses Element dennoch hinzufügen, das existierende ersetzen oder diese Aktion abbrechen? + Deine Bibliothek enthält möglicherweise schon ein Duplikat dieses Elements: \'%s\' +\n +\nMöchtest du dieses Element dennoch hinzufügen, das existierende ersetzen oder diese Aktion abbrechen? Links wurden neu geladen Drehen Zeige einen Umschalter für Bildschirmorientierung an @@ -568,7 +587,8 @@ kein Favorit Dieser Bildschirm wurde nach einigen Fehlversuchen geschlossen. Starte die App neu. Ihre CloudStream-Daten wurden gesichert. Obwohl die Wahrscheinlichkeit dieses seltenen Falles sehr gering ist, verhalten sich alle Geräte unterschiedlich. Falls Sie im schlimmsten Fall den Zugriff zur App verlieren, löschen Sie die App-Daten vollständig und stellen Sie die Sicherung wieder her. Jegliche Unannehmlichkeiten, die Ihnen dadurch entstehen, bedauern wir sehr. - %s \nausstehend + %s +\nausstehend Favorit Kopiert! Beim kopieren ist ein Fehler aufgetreten, bitte kopieren sie logical und wenden sich an den Support. @@ -587,7 +607,7 @@ Sicherheit Konten Repository öffnen - Besuche %s auf dem Smartphone oder Computer und gebe den obenstehenden Code ein + Besuche%s auf dem Smartphone oder Computer und gebe den obenstehenden Code ein PIN-Code vom Gerät nicht abrufbar, versuche lokale Authentifizierung Zur Zeit sind keine Downloads verfügbar. Lokales Video öffnen @@ -610,10 +630,18 @@ Vom Beginn an spielen Elemente zum Löschen auswählen Zum Offline-Ansehen verfügbar - Bist du dir sicher, dass du die folgenden Elemente permanent löschen willst? \n \n%s - Bist du dir sicher, dass du die folgenden Episoden in %1$s permanent löschen willst? \n \n%2$s - Du wirst ebenfalls alle Episoden der folgenden Serien permanent löschen: \n \n%s - Bist du dir sicher, dass du alle Episoden der folgenden Serien permanent löschen willst? \n \n%s + Bist du dir sicher, dass du die folgenden Elemente permanent löschen willst? +\n +\n%s + Bist du dir sicher, dass du die folgenden Episoden in %1$s permanent löschen willst? +\n +\n%2$s + Du wirst ebenfalls alle Episoden der folgenden Serien permanent löschen: +\n +\n%s + Bist du dir sicher, dass du alle Episoden der folgenden Serien permanent löschen willst? +\n +\n%s Veröffentlichungsdatum (von neu nach alt) Veröffentlichungsdatum (von alt nach neu) Suchleisten Vorschau @@ -639,7 +667,7 @@ Softwaredekodierung ermöglicht es dem Player, Videodateien abzuspielen, die von deinem Gerät nicht unterstützt werden, könnte jedoch bei hoher Auflösung zu ruckelnder oder instabiler Wiedergabe führen. App neu starten und Stream Torrent Pop-Up akzeptieren, um fortzufahren. Plugins aktualisieren - %d Plugin(s) erfolgreich aktualisiert + %d Plugin(s) erfolgreich aktualisiert! Plugins manuell aktualisieren Podcast Nicht unterstützter Fehler @@ -712,8 +740,8 @@ Zusätzliche Helligkeit Aktiviere Helligkeitsfilter, wenn 100% Bildschirmhelligkeit überschritten ist Erhöhte Helligkeit aktiviert - Zeige Cast-Panel - Mediainfo + Cast-Panel zeigen + Medieninfo Quellname Alle herunterladen Möchtest du Episode %s herunter laden? @@ -731,17 +759,4 @@ Es befinden sich keine Downloads in der Warteschlange. Quellpriorität Entscheide, wie Videoquellen im Player sortiert werden sollen - Zeige Player-Metadaten - Video - Vorschau - Live - Echtzeituhr anzeigen - Echtzeituhr oben auf dem Bildschirm anzeigen. Gilt für Startseite und Player - Profil-Einstellungen - Quellen mit negativer Priorität verbergen - Quellen mit Fehlern ausblenden - - %d ausgeblendeter Link - %d ausgeblendete Links - diff --git a/app/src/main/res/values-b+el/strings.xml b/app/src/main/res/values-b+el/strings.xml index a38ee2682..4a53c4e58 100644 --- a/app/src/main/res/values-b+el/strings.xml +++ b/app/src/main/res/values-b+el/strings.xml @@ -110,7 +110,8 @@ Μπανάνα δόθηκε Ταχύτητα (%.2fx) Βαθμολογία: %.1f - Νέα διαθέσιμη ενημέρωση! \n%1$s -> %2$s + Νέα διαθέσιμη ενημέρωση! +\n%1$s -> %2$s Πατήστε δύο φορές στη μέση για παύση Χρήση φωτεινότητας συστήματος Χρήση φωτεινότητας συστήματος στο ενσωματωμένο πρόγραμμα αναπαραγωγής, αντί εφαρμογής προεπιλεγμένου σκούρου επικαλύμματος @@ -148,8 +149,10 @@ Ακύρωση Παύση Συνέχιση - Αυτό θα διαγράψει μόνιμα το %s \nΕίστε σίγουροι πως θέλετε να προχωρήσετε; - %dm \nαπομένουν + Αυτό θα διαγράψει μόνιμα το %s +\nΕίστε σίγουροι πως θέλετε να προχωρήσετε; + %dm +\nαπομένουν Σε εξέλιξη Κατάσταση Έτος @@ -320,7 +323,9 @@ Απενεργοποιήθηκε: %d Δεν κατέβηκε: %d Ενημερώθηκαν %d πρόσθετα - Το CloudStream δεν έχει προεγκατεστημένους ιστότοπους. Πρέπει να εγκαταστήσετε ιστότοπους μέσω ορισμένων αποθετηρίων. \n \nΒρείτε μας στο Discord ή ψάξτε στο διαδίκτυο. + Το CloudStream δεν έχει προεγκατεστημένους ιστότοπους. Πρέπει να εγκαταστήσετε ιστότοπους μέσω ορισμένων αποθετηρίων. +\n +\nΒρείτε μας στο Discord ή ψάξτε στο διαδίκτυο. Προβολή αποθετηρίων κοινότητας Δημόσια λίστα Κεφαλοποίηση υποτίτλων @@ -482,15 +487,23 @@ Αφαίρεση από παρακολουθημένα Περιηγητής Άνοιγμα με - Η βιβλιοθήκη σας είναι άδεια :( \nΣυνδεθείτε με έναν λογαριασμό βιβλιοθήκης ή προσθέστε σειρές στην τοπική βιβλιοθήκη σας. - Βρέθηκε αρχείο Ασφαλούς Λειτουργίας! \nΔεν πρόκειται να φορτωθούν extensions κατά το ξεκίνημα μέχρι να διαγραφεί το αρχείο. + Η βιβλιοθήκη σας είναι άδεια :( +\nΣυνδεθείτε με έναν λογαριασμό βιβλιοθήκης ή προσθέστε σειρές στην τοπική βιβλιοθήκη σας. + Βρέθηκε αρχείο Ασφαλούς Λειτουργίας! +\nΔεν πρόκειται να φορτωθούν extensions κατά το ξεκίνημα μέχρι να διαγραφεί το αρχείο. Αρχείο Καταγραφής Απέτυχε Πέτυχε Εκκίνηση Δε βρέθηκαν επεκτάσεις στο αποθετήριο Δε βρέθηκε αποθετήριο, ελέγξτε την URL και δοκιμάστε VPN - Από εδώ μπορείτε να αλλάξετε τον τρόπο σειράς των πηγών. Εάν ένα βίντεο έχει υψηλότερη προτεραιότητα, θα εμφανίζεται ψηλότερα στην επιλογή πηγής. Το άθροισμα της προτεραιότητας πηγής και της ποιότητας, είναι η προτεραιότητα του βίντεο. \n \nΠηγή Α: 3 \nΠοιότητα Β: 7 \nΘα έχει συνδυασμένη προτεραιότητα βίντεο 10. \n \nΣΗΜΕΙΩΣΗ: Εάν το άθροισμα είναι 10 ή περισσότερο, η συσκευή αναπαραγωγής θα παραλείψει αυτόματα τη φόρτωση όταν φορτωθεί αυτός ο σύνδεσμος! + Από εδώ μπορείτε να αλλάξετε τον τρόπο σειράς των πηγών. Εάν ένα βίντεο έχει υψηλότερη προτεραιότητα, θα εμφανίζεται ψηλότερα στην επιλογή πηγής. Το άθροισμα της προτεραιότητας πηγής και της ποιότητας, είναι η προτεραιότητα του βίντεο. +\n +\nΠηγή Α: 3 +\nΠοιότητα Β: 7 +\nΘα έχει συνδυασμένη προτεραιότητα βίντεο 10. +\n +\nΣΗΜΕΙΩΣΗ: Εάν το άθροισμα είναι 10 ή περισσότερο, η συσκευή αναπαραγωγής θα παραλείψει αυτόματα τη φόρτωση όταν φορτωθεί αυτός ο σύνδεσμος! Δοκιμή παρόχου Προτιμώμενη ποιότητας παρακολούθησης (Δεδομένα τηλεφώνου) Διακομιστής μεσολάβησης GitHub @@ -536,7 +549,9 @@ Εντάξει Απενεργοποιήση της εξοικονόμησης της μπαταρίας Έχετε ήδη ψηφίσει - Φαίνεται πως ένα πιθανό αντίγραφο βρίσκεται στη βιβλιοθήκη σας: \'%s.\' \n \nΘα επιθυμούσατε να το προσθέσετε, να το αντικαταστήσετε, ή να ακυρώσετε την ενέργεια; + Φαίνεται πως ένα πιθανό αντίγραφο βρίσκεται στη βιβλιοθήκη σας: \'%s.\' +\n +\nΘα επιθυμούσατε να το προσθέσετε, να το αντικαταστήσετε, ή να ακυρώσετε την ενέργεια; Εισαγωγή Τρέχον Κωδικού Κλείδωμα Προφίλ Ξεκλείδωμα Cloudstream @@ -563,7 +578,11 @@ Πιθανό αντίγραφο βρέθηκε Προσθήκη Αντικατάσταση - Πιθανά διπλά αρχεία βρέθηκαν στην βιβλιοθήκη: \n \n%s \n \nΘα επιθυμούσατε να προσθέσετε αυτό το αρχείο ούτως η άλλως, να αντικαταστήσετε τα ήδη υπάρχοντα, ή να ακυρώσετε την ενέργεια? + Πιθανά διπλά αρχεία βρέθηκαν στην βιβλιοθήκη: +\n +\n%s +\n +\nΘα επιθυμούσατε να προσθέσετε αυτό το αρχείο ούτως η άλλως, να αντικαταστήσετε τα ήδη υπάρχοντα, ή να ακυρώσετε την ενέργεια? Εισαγωγή Κωδικού για %s Κωδικός Εσφαλμένος Κωδικός. Προσπαθήστε ξανά. @@ -576,7 +595,8 @@ Παράκαμψη απαγόρευσης από raw github URLs χρησιμοποιώντας jsDelivr. Μπορεί να καθυστερήσει τις ενημερώσεις για μερικές μέρες. Εμφάνιση κουμπιού για περιστροφή οθόνης Αγαπημένο - %s \nαπομένουν + %s +\nαπομένουν Βιομετρική αυθεντικοποίηση δεν υποστηρίζεται από τη συσκευή Καστ ταινίας Για να εξασφαλιστούν αδιάκοπες λήψεις και ειδοποιήσεις για αναγραφόμενες τηλεοπτικές εκπομπές, το CloudStream χρειάζεται άδεια για να τρέξει στο παρασκήνιο. Πατώντας OK, θα εμφανιστεί ένας διάλογος αιτήματος. Παρακαλώ πατήστε \\\"Επιτρέπω\\\".\n\nΠαρακαλώ σημειώστε, αυτή η άδεια δεν σημαίνει ότι το CS3 θα αποστραγγίσει την μπαταρία σας. Θα λειτουργεί στο παρασκήνιο μόνο όταν είναι απαραίτητο, όπως κατά τη λήψη ειδοποιήσεων ή τη λήψη βίντεο από επίσημες επεκτάσεις. diff --git a/app/src/main/res/values-b+eo/strings.xml b/app/src/main/res/values-b+eo/strings.xml index 5dea7b24d..ccd18eae3 100644 --- a/app/src/main/res/values-b+eo/strings.xml +++ b/app/src/main/res/values-b+eo/strings.xml @@ -83,7 +83,8 @@ %1$dt %2$dh %3$dm %1$dh %2$dm %dm - Nova ĝisdatigo trovita! \n%1$s -> %2$s + Nova ĝisdatigo trovita! +\n%1$s -> %2$s Speciala epizodo CloudStream Elŝuto Komencite diff --git a/app/src/main/res/values-b+es/strings.xml b/app/src/main/res/values-b+es/strings.xml index df6b181f1..1801d39a2 100644 --- a/app/src/main/res/values-b+es/strings.xml +++ b/app/src/main/res/values-b+es/strings.xml @@ -7,8 +7,8 @@ Descargado %1$d %2$s Borrar repositorio El episodio %d se lanzará en - %1$d h %2$d m - %d m + %1$d h %2$d m + %d m Póster Extensiones Archivo descargado @@ -80,7 +80,8 @@ Recargar enlaces /?? /%d - Esto eliminará %s permanentemente \nEstá seguro? + Esto eliminará %s permanentemente +\nEstá seguro? ¿Seguro que quieres salir? Continuar Descarga Código de idioma (es_ES) @@ -103,7 +104,7 @@ Velocidad (%.2f×) Omitir carga %1$s Ep. %2$d - %1$d d %2$d h %3$d m + %1$d d %2$d h %3$d m Reparto: %s Relleno %d min @@ -248,7 +249,8 @@ Continuar -30 +30 - %dm \nfaltante + %dm +\nfaltante En curso Completado Estado @@ -480,8 +482,10 @@ Alfabéticamente (Z a A) Seleccionar biblioteca Abrir con - Tu biblioteca está vacía :( \nInicia sesión en una cuenta de biblioteca o añade series desde tu biblioteca local. - ¡Se encontró un archivo en modo seguro! \nNo cargar ninguna extensión al inicio hasta que se elimine el archivo. + Tu biblioteca está vacía :( +\nInicia sesión en una cuenta de biblioteca o añade series desde tu biblioteca local. + ¡Se encontró un archivo en modo seguro! +\nNo cargar ninguna extensión al inicio hasta que se elimine el archivo. Reproductor visible - buscar cantidad Reproductor oculto - buscar cantidad Android TV @@ -506,7 +510,13 @@ ISP Bypasses Calidad de visualización preferida (Datos móviles) Ayuda - Aquí puedes cambiar el orden de las fuentes. Si un vídeo tiene una prioridad más alta, aparecerá más arriba en la selección de las fuentes. La suma de la prioridad de la fuente y la prioridad de la calidad es la prioridad del vídeo. \n \nFuente A: 3 \nCalidad B: 7 \nTendrá una prioridad en el vídeo combinada de 10. \n \nNOTA: ¡Si la suma es 10 o más el reproductor saltará automáticamente la carga cuando se cargue ese enlace! + Aquí puedes cambiar el orden de las fuentes. Si un vídeo tiene una prioridad más alta, aparecerá más arriba en la selección de las fuentes. La suma de la prioridad de la fuente y la prioridad de la calidad es la prioridad del vídeo. +\n +\nFuente A: 3 +\nCalidad B: 7 +\nTendrá una prioridad en el vídeo combinada de 10. +\n +\nNOTA: ¡Si la suma es 10 o más el reproductor saltará automáticamente la carga cuando se cargue ese enlace! Perfil %d Wifi Editar @@ -526,7 +536,11 @@ %s eliminado de favoritos Favoritos %s añadido a favoritos - Se han encontrado posibles elementos duplicados en su biblioteca: \n \n%s \n \n¿Desea añadir este elemento de todos modos, sustituir los existentes o cancelar la acción? + Se han encontrado posibles elementos duplicados en su biblioteca: +\n +\n%s +\n +\n¿Desea añadir este elemento de todos modos, sustituir los existentes o cancelar la acción? Posible duplicado encontrado Bloquear perfil Añadido a favoritos @@ -569,7 +583,8 @@ Ahora se ha realizado una copia de seguridad de sus datos de CloudStream. Aunque la posibilidad de que esto ocurra es muy baja, todos los dispositivos pueden comportarse de forma diferente. En el raro caso de que no puedas acceder a la aplicación, borra completamente los datos de la aplicación y restaura desde una copia de seguridad. Sentimos mucho las molestias que esto pueda ocasionarte. Favorito Eliminar de favoritos - %s \nrestante + %s +\nrestante Nombre y URL del repositorio ¡Copiado! Error al copiar. Por favor, copie el logcat y comuníquese con el soporte de la aplicación. @@ -594,7 +609,7 @@ Imagen del código QR Descartar Abrir repositorio - Visita %s en tu smartphone o equipo e introduce el código anterior + Visita %s en tu smartphone o ordenador e introduce el código anterior ¡El código PIN ya ha caducado! El código caduca en %1$d mín y %2$d s No puedo obtener el código PIN del dispositivo; intente con la autenticación local @@ -606,16 +621,24 @@ Ocultar los nombres de los controles del reproductor Fecha de lanzamiento (antigua a nueva) Fecha de lanzamiento (de nueva a antigua) - ¿Estás seguro de que quieres borrar permanentemente todos los episodios de la serie? \n \n%s + ¿Estás seguro de que quieres borrar permanentemente todos los episodios de la serie? +\n +\n%s Seleccionar elementos para eliminar Disponible para visualizar sin conexión Seleccionar todo Deseleccionar todo Borrar archivos Borrar (%1$d | %2$s) - ¿Seguro que quieres borrar de forma permanente los siguientes elementos? \n \n%s - ¿Estás seguro de que deseas eliminar permanentemente los siguientes episodios en %1$s? \n \n%2$s - También borrará permanentemente todos los episodios de las siguientes series: \n \n%s + ¿Seguro que quieres borrar de forma permanente los siguientes elementos? +\n +\n%s + ¿Estás seguro de que deseas eliminar permanentemente los siguientes episodios en %1$s? +\n +\n%2$s + También borrará permanentemente todos los episodios de las siguientes series: +\n +\n%s Activar la previsualización para las miniaturas en la barra de búsqueda Previsualización de Seekbar Aún no hay subtítulos cargados @@ -647,7 +670,7 @@ Actualizar complementos Fecha de emisión (más antigua) Puntuar %s - %d complemento(s) actualizados correctamente + ¡%d complemento(s) actualizados correctamente! No se actualizó ningún complemento. Episodio (Ascendente) Reconocimiento de habla no disponible @@ -672,9 +695,9 @@ Sobreexploración Cambios en el tamaño de los pósteres Tamaño del póster - %1$d h %2$d m %3$d s - %1$d m %2$d s - %1$d s + %1$d h %2$d m %3$d s + %1$d m %2$d s + %1$d s Etiqueta de valoración Mantenga presionado para duplicar la velocidad Sin cuenta @@ -733,18 +756,4 @@ %d descargas encoladas %d descargas encoladas - Mostrar superposición de metadatos del jugador - Vídeo - Vista previa - En Vivo - Mostrar reloj en tiempo real - Muestra un reloj en tiempo real en la parte superior de la pantalla. Se aplica a la página de inicio y al reproductor - Configuración del perfil - Ocultar fuentes con prioridad negativa - Ocultar fuentes con errores - - %d enlace oculto - %d enlaces ocultos - %d enlaces ocultos - diff --git a/app/src/main/res/values-b+fa/strings.xml b/app/src/main/res/values-b+fa/strings.xml index 1018e9a82..5fa557f08 100644 --- a/app/src/main/res/values-b+fa/strings.xml +++ b/app/src/main/res/values-b+fa/strings.xml @@ -113,7 +113,8 @@ در حال تماشا بارگیری‌ها سرعت (%.2f برابر) - بروزرسانی جدید پیدا شد! \n%1$s -> %2$s + بروزرسانی جدید پیدا شد! +\n%1$s -> %2$s پخش فیلم مرورگر پخش قسمت @@ -129,7 +130,8 @@ برای بازنشانی به پیشفرض نگه‌دارید کتابخانه در ادامه - این فرآیند بطور کامل %s را حذف می‌کند \nآیا از این کار اطمینان دارید؟ + این فرآیند بطور کامل %s را حذف می‌کند +\nآیا از این کار اطمینان دارید؟ نام مخزن و نشانی کپی شد! درباره @@ -178,11 +180,13 @@ حذف پرونده نمایش تریلر ها قسمت‌ها - %dد \nباقی‌مانده + %dد +\nباقی‌مانده گیتهاب پنهان کردن ویدیو مشخص شده از نتایج جستجو لغو - %s \nباقی‌مانده + %s +\nباقی‌مانده پیش‌فرض کارتون تورنت diff --git a/app/src/main/res/values-b+fr/strings.xml b/app/src/main/res/values-b+fr/strings.xml index 39556afc4..34bf1393d 100644 --- a/app/src/main/res/values-b+fr/strings.xml +++ b/app/src/main/res/values-b+fr/strings.xml @@ -79,7 +79,8 @@ Annuler Pause Reprendre - Cela va supprimer définitivement %s \nÊtes-vous sûr ? + Cela va supprimer définitivement %s +\nÊtes-vous sûr ? En cours Terminé Statut @@ -121,7 +122,8 @@ Mettre à jour Utile pour contourner les bloquages des FAI Emplacement de téléchargement - Nouvelle mise à jour trouvée ! \n%1$s -> %2$s + Nouvelle mise à jour trouvée ! +\n%1$s -> %2$s Épisode spécial Qualité de visionnage préférée (WiFi) Taille de la mémoire cache @@ -135,7 +137,7 @@ Afficher les animés en Anglais (Dub) / sous-titrés Disposition en mode téléphone %1$s Ep %2$d - Note : %.1f + Note : %.1f Zoom Adapter à l\'écran Disposition de l\'application @@ -143,7 +145,7 @@ Langues des extensions Médias préférées Auto - Distribution : %s + Distribution : %s %d min Rechercher sur %s… À re-regarder @@ -289,8 +291,8 @@ Application Light Novel par les mêmes devs Anime app by the same devs Rejoignez le Discord - %1$d h %2$d min - %d min + %1$d h %2$d min + %d min Lire avec CloudStream Lire en direct Fin @@ -304,9 +306,9 @@ Intro Effacer l\'historique Oui - %1$d j %2$d h %3$d min + %1$d j %2$d h %3$d min Stream - Êtes-vous sûr·e de vouloir quitter ? + Êtes-vous sûr·e de vouloir quitter ? Non Téléchargement de la mise à jour… L\'épisode %d sera publié dans @@ -319,7 +321,8 @@ Nouveau Nom du site ID invalide Installer automatiquement les plugins qui sont dans les repository mais qui n\'ont pas encore été installés. - %dm \nrestant + %dm +\nrestant En direct Autres En direct @@ -360,13 +363,13 @@ NSFW 127.0.0.1 %d / 10 - / ?? + /?? /%d SD UHD HDR SDR - CamHD + Cam HQ HD 4K @@ -383,8 +386,8 @@ Installeur APK +30 Cam - Supprimer les pubs des sous-titres - CamRip + Supprimer les éléments superflus des sous-titres + Cam Trailer Regardez les vidéos dans ces langues Modifier le style de l\'application pour être assortie à votre appareil @@ -406,14 +409,14 @@ Téléchargé %1$d %2$s Tous les %s déjà téléchargés Télécharger la liste de sites que vous voulez utiliser - Téléchargé : %d + Téléchargé : %d Pistes vidéo Redémarrez l\'application pour voir les changements. Toutes les extensions ont été désactivé à cause d\'un crash pour vous aider à trouver l\'extension causant le problème. Mode sans échec activé Taille Version - Note : %s + Note : %s Description Status Installer l\'extension d\'abord @@ -429,10 +432,10 @@ Nom de dépôt (optionnel) plugin Supprimer le repository - Désactivé : %d - Non téléchargé : %d + Désactivé : %d + Non téléchargé : %d %d plugins mis-à-jour - Avertissement : CloudStream 3 décline toute responsabilité concernant l’utilisation d’extensions tierces et ne fournit aucun support pour celles-ci ! + Avertissement : CloudStream 3 décline toute responsabilité concernant l’utilisation d’extensions tierces et ne fournit aucun support pour celles-ci ! %s (Désactivé) Pistes Pistes audio @@ -445,7 +448,9 @@ Installateur de paquet plugins Cela supprimera également tous les plugins du repository - CloudStream n\'a aucun site installé par défaut. Vous devez installer les sites à partir de dépôts. \n \nRejoignez notre Discord ou cherchez en ligne. + CloudStream n\'a aucun site installé par défaut. Vous devez installer les sites à partir de dépôts. +\n +\nRejoignez notre Discord ou cherchez en ligne. Langage Afficher les popups skip pour les intro / fins Ancienne méthode d\'installation @@ -474,7 +479,8 @@ Note (basse à haute) Note (haut à bas) Alphabétique (A à Z) - Votre bibliothèque est vide :( \nConnectez-vous à un compte de bibliothèque ou ajoutez des séries à votre bibliothèque locale. + Votre bibliothèque est vide :( +\nConnectez-vous à un compte de bibliothèque ou ajoutez des séries à votre bibliothèque locale. Cette liste est vide. Essayez d\'en changer. Android TV Trier par @@ -483,7 +489,8 @@ Ouvrir avec Mis à jour (Nouveau vers ancien) Mis à jour (ancien vers nouveau) - Fichier du mode sans échec trouvé ! \nAucune extension ne sera chargée au démarrage avant que le fichier ne soit enlevé. + Fichier du mode sans échec trouvé ! +\nAucune extension ne sera chargée au démarrage avant que le fichier ne soit enlevé. Arrêter Annuler Enregistrer @@ -508,7 +515,13 @@ Impossible d\'atteindre GitHub. Activation du proxy jsDelivr… Vous avez déjà voté Désactivé - Ici, vous pouvez modifier la façon dont les sources sont ordonnées. Si une vidéo a une priorité plus élevée, elle apparaîtra plus haut dans la sélection de la source. La somme de la priorité source et de la priorité qualité est la priorité vidéo. \n \nSource A : 3 \nQualité B : 7 \nLa priorité vidéo combinée sera de 10. \n \nREMARQUE : Si la somme est de 10 ou plus, le joueur sautera automatiquement le chargement lorsque ce lien est chargé ! + Ici, vous pouvez modifier la façon dont les sources sont ordonnées. Si une vidéo a une priorité plus élevée, elle apparaîtra plus haut dans la sélection de la source. La somme de la priorité source et de la priorité qualité est la priorité vidéo. +\n +\nSource A : 3 +\nQualité B : 7 +\nLa priorité vidéo combinée sera de 10. +\n +\nREMARQUE : Si la somme est de 10 ou plus, le joueur sautera automatiquement le chargement lorsque ce lien est chargé ! Aucun plugin trouvé dans ce dossier Dépôt introuvable, vérifiez l\'URL et essayez avec un VPN Données mobiles @@ -541,14 +554,20 @@ PIN Favoris Connecté en tant que %s - Des doublons potentiels ont été trouvés dans votre bibliothèque : \n \n%s \n \nSouhaitez-vous ajouter cet élément, remplacer les éléments existants ou annuler l\'action ? + Des doublons potentiels ont été trouvés dans votre bibliothèque : +\n +\n%s +\n +\nSouhaitez-vous ajouter cet élément, remplacer les éléments existants ou annuler l\'action ? Saisir le code PIN pour %s Doublon potentiel trouvé Verrouiller le profil Ignorer la sélection de compte au démarrage Se désabonner S\'abonner - Il semble qu\'un élément potentiellement dupliqué existe déjà dans votre bibliothèque : \'%s. \n \nSouhaitez-vous ajouter cet élément, remplacer l\'élément existant ou annuler l\'action ? + Il semble qu\'un élément potentiellement dupliqué existe déjà dans votre bibliothèque : \'%s. +\n +\nSouhaitez-vous ajouter cet élément, remplacer l\'élément existant ou annuler l\'action ? Saisir le code PIN actuel Pivoter Les liens ont été rechargés @@ -561,7 +580,7 @@ Testez toutes les extensions Afficher les recommandations Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. - Copié ! + Copié ! Nom du dépôt et adresse internet Favori Vos données CloudStream viennent d\'être sauvegardées. Bien que cette éventualité soit très faible, tous les appareils peuvent se comporter différemment. Dans le rare cas où l\'accès à l\'application est bloqué, effacez complètement les données de l\'application et restaurez à partir d\'une sauvegarde. Nous sommes sincèrement désolés pour les désagréments occasionnés par cette situation. @@ -600,10 +619,10 @@ Ouvrir le dépôt Code expire dans %1$dm %2$ds Wiki de CloudStream - Le code PIN est maintenant expiré ! + Le code PIN est maintenant expiré ! Image du code QR Supprimer l\'extension - Êtes-vous sûr de vouloir supprimer définitivement les éléments suivants ? \n \n%s + Êtes-vous sûr de vouloir supprimer définitivement les éléments suivants ? \n \n%s Authentification locale Date de sortie (du plus ancien au plus récent) Date de sortie (du plus récent au plus ancien) @@ -619,9 +638,9 @@ Comptes Cette vidéo est un torrent, ce qui signifie que votre activité vidéo peut être suivie.\nAssurez-vous de comprendre le fonctionnement des torrents avant de continuer. Ignorer - Vous allez également supprimer définitivement tous les épisodes des séries suivantes :\n\n%s - Êtes-vous sûr de vouloir supprimer définitivement tous les épisodes des séries suivantes ?\n\n%s - Êtes-vous sûr de vouloir supprimer définitivement les épisodes suivants dans %1$s ?\n\n%2$s + Vous allez également supprimer définitivement tous les épisodes des séries suivantes :\n\n%s + Êtes-vous sûr de vouloir supprimer définitivement tous les épisodes des séries suivantes ?\n\n%s + Êtes-vous sûr de vouloir supprimer définitivement les épisodes suivants dans %1$s ?\n\n%2$s Recopier l’écran Aucun sous-titre n’a encore été chargé Emplacement du dossier de sauvegarde @@ -641,7 +660,7 @@ Note %s Date %s Mettre à jour les plugins - %d plugin(s) mis à jour avec succès + %d plugin(s) mis à jour avec succès ! Aucun plugin n\'a été mis à jour. Note (Plus Haute) Mettre à jour les plugins manuellement @@ -652,7 +671,7 @@ La notification du lecteur pour contrôler la lecture en arrière-plan Date (Plus Récent) Note (Plus Basse) - Démarrage du processus de mise à jour du plugin ! + Démarrage du processus de mise à jour du plugin ! Intégré En ligne La reconnaissance vocale n\'est pas disponible @@ -722,8 +741,8 @@ Déterminez comment les sources vidéo seront triées dans le lecteur Télécharger tout Tout annuler - Voulez-vous télécharger l\'épisode %s ? - Vous voulez annuler tous les téléchargements en file d\'attente ? + Voulez-vous télécharger l\'épisode %s ? + Vous voulez annuler tous les téléchargements en file d\'attente ? %d téléchargement actif %d téléchargements actifs @@ -735,36 +754,4 @@ %d téléchargements en attente Afficher les métadata de l\'overlay du lecteur vidéo - Vidéo - Prévisualisation - Live - Cacher les sources avec une priorité négative - Afficher l\'heure - Affiche l\'heure en haut de l\'écran. S\'applique à la page d\'Accueil et au lecteur - Paramètres du profil - Cacher les sources avec des erreurs - - %d lien caché - %d liens cachés - %d liens cachés - - Logcat 🐈 - Commencer des vidéos en pause - Réduire le stress lors du choix des sources - Bordure - Compresseur - Compresseur à portée dynamique - Montrer les contrôles du compresseur dans le lecteur (aide a équilibrer les audios fort des faibles) - Activé - Activé · %1$d dB / +%2$d dB - Désactivé - Légère - Booster les dialogues - Lourd - Décibels max: %d dB - Augmente le volume total de +%d dB - Arrière plan - URL - Choisir une image - Images par défaut diff --git a/app/src/main/res/values-b+gl/strings.xml b/app/src/main/res/values-b+gl/strings.xml index e3775b091..ede5fd0dc 100644 --- a/app/src/main/res/values-b+gl/strings.xml +++ b/app/src/main/res/values-b+gl/strings.xml @@ -13,7 +13,8 @@ Póster do Episodio Regresar Cambiar provedor - Nova actualización atopada! \n%1$s -> %2$s + Nova actualización atopada! +\n%1$s -> %2$s Recheo %d min Configuración diff --git a/app/src/main/res/values-b+hi/strings.xml b/app/src/main/res/values-b+hi/strings.xml index f5717d223..c9030a00a 100644 --- a/app/src/main/res/values-b+hi/strings.xml +++ b/app/src/main/res/values-b+hi/strings.xml @@ -2,7 +2,8 @@ स्पीड (%.2fx) - नया अपडेट आया है! \n%1$s -> %2$s + नया अपडेट आया है! +\n%1$s -> %2$s होम खोजें डाउनलोडस @@ -86,7 +87,8 @@ रद्द करें रोकें फिर से चलाएं - इससे %s स्थायी रूप से हट जाएगा \nक्या आपका निर्णय निश्चित है ? + इससे %s स्थायी रूप से हट जाएगा +\nक्या आपका निर्णय निश्चित है ? अभी चालू है मुकम्मल हुया स्थिति @@ -151,7 +153,11 @@ %d मिनट क्लाउडस्ट्रीम क्लाउडस्ट्रीम के साथ चलाएं - आपकी लाइब्रेरी में संभावित डुप्लिकेट आइटम पाए गए हैं: \n \n%s \n \nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे? + आपकी लाइब्रेरी में संभावित डुप्लिकेट आइटम पाए गए हैं: +\n +\n%s +\n +\nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे? %s के लिए पिन दर्ज करें संभावित डुप्लिकेट मिला अपडेट शुरू हुआ @@ -171,7 +177,9 @@ अकाउंट चुनिये लोडिंग स्किप करे लोडिंग… - ऐसा प्रतीत होता है कि संभावित रूप से डुप्लिकेट आइटम आपकी लाइब्रेरी में पहले से मौजूद है: \'%s.\' \n \nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे? + ऐसा प्रतीत होता है कि संभावित रूप से डुप्लिकेट आइटम आपकी लाइब्रेरी में पहले से मौजूद है: \'%s.\' +\n +\nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे? पिन दर्ज करें पिन लिंक पुन्ह खुली @@ -295,7 +303,7 @@ %1$d %2$s S E - %dm\nसमय बाकी + %dm\nबाकी वृत्तचित्र कोई नहीं जानता ये प्लेयर एरर क्यों आया उपशीर्षक डाउनलोड करें @@ -414,43 +422,4 @@ फिलहाल कोई डाउनलोड कतार में नहीं है। अतिरिक्त चमक डिस्प्ले की चमक 100% से अधिक होने पर ब्राइटनेस फ़िल्टर चालू करें - किनारा - लॉगकैट 🐈 - रुके हुए वीडियो को शुरू करें - स्रोत चुनते समय तनाव कम करें - अतिरिक्त चमक सक्षम - खोज के सुझाव - टाइप करते समय खोज सुझाव दिखाएं - कास्ट पैनल दिखाएँ - एपिसोड्स - वीडियो - रुको - क्रैश जानकारी देखें - लेखक लोग - विवरण - पहले एक्सटेंशन इंस्टॉल करे - पसंदीदा - भाषा - अमान्य डेटा - अमान्य यूआरएल - त्रुटि - छाया - सबटाइटल से विज्ञापन हटाएं - पसंदीदा मीडिया भाषा से फ़िल्टर करें - संस्करण - स्थिति - प्रिय - प्रयोग - - %d डाउनलोड कतार - %d डाउनलोडस कतार - - सीधा प्रसारण - सदस्यता लिया - %s सदस्यता लिया - %s सदस्यता छोड़ दी गई - एपिसोड %d रिलीज़ हुआ! - सदस्यता ले - सदस्यता रद्द - पिछला diff --git a/app/src/main/res/values-b+hr/strings.xml b/app/src/main/res/values-b+hr/strings.xml index a64e206bb..c2c0379f5 100644 --- a/app/src/main/res/values-b+hr/strings.xml +++ b/app/src/main/res/values-b+hr/strings.xml @@ -19,7 +19,8 @@ Brzina (%.2f×) Ocjena: %.1f - Pronađeno je novo ažuriranje! \n%1$s -> %2$s + Pronađeno je novo ažuriranje! +\n%1$s -> %2$s Umetak %d min CloudStream @@ -185,8 +186,10 @@ Nastavi −30 +30 - Ovo će trajno izbrisati %s \nJeste li sigurni? - %dmin \npreostalo + Ovo će trajno izbrisati %s +\nJeste li sigurni? + %dmin +\npreostalo U tijeku Završeno Status @@ -350,8 +353,8 @@ Slučajno Dolazi uskoro … Kamera - CamRip - CamHD + Kamera + Kamera HQ HD TS @@ -375,7 +378,7 @@ Nevažeći URL Pogreška Ukloni titlove za gluhe osobe iz titlova - Ukloni oglase iz titlova + Ukloni nepotrebne elemente iz titlova (npr. oglase) Filtriraj po preferiranom jeziku medija Dodatni sadržaji Trailer @@ -408,7 +411,9 @@ Preuzeto: %d Onemogućeno: %d Nepreuzeto: %d - CloudStream standardno nema instalirane web stranice. Stranice morate instalirati iz repozitorija. \n \nPridružite se našem Discordu ili tražite online. + CloudStream standardno nema instalirane web stranice. Stranice morate instalirati iz repozitorija. +\n +\nPridružite se našem Discordu ili tražite online. Prikaži repozitorije zajednice Javni popis Koristi velika slova za sve titlove @@ -493,9 +498,11 @@ Abecedno (Ž do A) Odaberite biblioteku Otvori sa - Vaša je biblioteka prazna :( \nPrijavite se na račun biblioteke ili dodajte filmove / serije u svoju lokalnu biblioteku. + Vaša je biblioteka prazna :( +\nPrijavite se na račun biblioteke ili dodajte filmove / serije u svoju lokalnu biblioteku. Ova je lista prazna. Pokušajte se prebaciti na jednu drugu listu. - Pronađena je datoteka sigurnog načina rada! \nProširenja se ne učitavaju tijekom pokretanja dok se datoteka ne ukloni. + Pronađena je datoteka sigurnog načina rada! +\nProširenja se ne učitavaju tijekom pokretanja dok se datoteka ne ukloni. Prikazan player – Količina pomicanja Količina pomicanja koja se koristi kada je player vidljiv Player skriven – Količina pomicanja @@ -534,13 +541,23 @@ Onemogući U repozitoriju nisu pronađeni dodaci Repozitorij nije pronađen. Provjeri URL i pokušaj VPN - Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je prioritet videa. \n \nIzvor A: 3 \nKvaliteta B: 7 \nImat će kombinirani prioritet videa od 10. \n \nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! + Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je prioritet videa. +\n +\nIzvor A: 3 +\nKvaliteta B: 7 +\nImat će kombinirani prioritet videa od 10. +\n +\nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! Već si glasao/la Učestalost spremanja sigurnosne kopije %s uklonjeno iz favorita Favoriti %s dodano u favorite - Potencijalni duplikati pronađeni su u vašoj biblioteci: \n \n%s \n \nŽelite li ipak dodati ovu stavku koja zamjenjuje postojeće ili poništiti radnju? + Potencijalni duplikati pronađeni su u vašoj biblioteci: +\n +\n%s +\n +\nŽelite li ipak dodati ovu stavku koja zamjenjuje postojeće ili poništiti radnju? Pronađen potencijalni duplikat Zaključaj profil Dodaj u favorite @@ -553,7 +570,9 @@ Pretplata Ukloni iz favorita Odaberite račun - Čini se da potencijalno duplicirana stavka već postoji u vašoj biblioteci: \'%s.\' \n \nŽelite li ipak dodati ovu stavku koja zamjenjuje postojeću ili poništiti radnju? + Čini se da potencijalno duplicirana stavka već postoji u vašoj biblioteci: \'%s.\' +\n +\nŽelite li ipak dodati ovu stavku koja zamjenjuje postojeću ili poništiti radnju? Unesite PIN PIN Unesite trenutni PIN @@ -577,7 +596,8 @@ Ime repozitorija i URL kopirano! Zaključaj s biometrijskim podatcima - %s \npreostalo + %s +\npreostalo Pogreška pri pristupanju međuspremnika. Pokušaj ponovo. Otključaj CloudStream Lozinka/PIN autentifikacija @@ -596,7 +616,7 @@ Otključaj aplikaciju pomoću otiska prsta, ID-a lica, PIN-a, uzorka i lozinke. Sljedeća u %s Pogreška pri kopiranju. Kopirajte zapisnik i kontaktirajte podršku aplikacije. - Da biste osigurali neprekinuta preuzimanja i obavijesti za pretplaćene TV emisije, CloudStream treba dopuštenje za rad u pozadini. Pritiskom na \"U redu\" prikazat će vam se dijaloški okvir s zahtjevom. Molimo vas da pritisnete \"Dopusti\". \n\nNapominjemo da ovo dopuštenje ne znači da će CS3 trošiti vašu bateriju. Aplikacija će raditi u pozadini samo kada je to potrebno, primjerice prilikom primanja obavijesti ili preuzimanja videozapisa iz službenih proširenja. + Da biste osigurali neprekinuta preuzimanja i obavijesti za pretplaćene TV emisije, CloudStream treba dopuštenje za rad u pozadini. Pritiskom na \"U redu\" prikazat će vam se dijaloški okvir s zahtjevom. Molimo vas da pritisnete \"Dopusti\". \n\nNapominjemo da ovo dopuštenje ne znači da će CS3 trošiti vašu bateriju. Aplikacija će raditi u pozadini samo kada je to potrebno, primjerice prilikom primanja obavijesti ili preuzimanja videozapisa iz službenih proširenja. Vaši CloudStream podaci su sada spremljeni u sigurnosnu kopiju. Iako je vjerojatnost mala, neki se uređaji mogu ponašati drugačije. Ako izgubite pristup aplikaciji, potpuno izbrišite podatke aplikacije i obnovite ih pomoću sigurnosne kopije. Ispričavamo se zbog mogućih neugodnosti. Sezona %1$d epizoda %2$d izlazi za Cast duplikat @@ -622,14 +642,22 @@ Izbriši dodatak Dostupno za gledanje offline Označi sve - Stvarno želite trajno izbrisati sljedeće stavke? \n \n%s - Stvarno želite trajno izbrisati sljedeće epizode u %1$s? \n \n%2$s - Trajno ćete izbrisati i sve epizode u sljedećim serijama: \n \n%s + Stvarno želite trajno izbrisati sljedeće stavke? +\n +\n%s + Stvarno želite trajno izbrisati sljedeće epizode u %1$s? +\n +\n%2$s + Trajno ćete izbrisati i sve epizode u sljedećim serijama: +\n +\n%s Odaberi stavke za brisanje Odznači sve Izbriši (%1$d | %2$s) Izbriši datoteke - Stvarno želite trajno izbrisati sve epizode u sljedećoj seriji? \n \n%s + Stvarno želite trajno izbrisati sve epizode u sljedećoj seriji? +\n +\n%s Još nije učitan nijedan titl Pretpregled trake za traženje Omogući minijaturu pregleda na traci za pretraživanje @@ -664,7 +692,7 @@ Ažuriraj podatke ručno Pokreće se postupak ažuriranja dodataka! Obavijesti reproduktora - Uspješno ažurirano %d dodataka + Uspješno ažuriranih dodataka: %d! Obavijest reproduktora za upravljanje reprodukcijom iz pozadine Ukosite sve titlove Podebljajte sve titlove @@ -721,7 +749,7 @@ Gore u sredini Gore desno Dodatna svjetlina - Uključi filtar svjetline kada se prekorači 100 % svjetline ekrana + Uključi filtar svjetline kada se prekorači 100 % svjetline ekrana dodatna_svjetlina_uključena Prijedlozi za pretraživanje Prikaži prijedloge za pretraživanje tijekom tipkanja @@ -747,23 +775,4 @@ Želiš li preuzeti epizodu %s? Želiš li otkazati sva preuzimanja u redu čekanja? Prikaži ploču glumačke postave - Video - Pregled - Uživo - Prikaži sloj metapodataka playera - Rub - Logmačka 🐈 - Pokreni videozapise pauzirane - Smanjite stres pri odabiru izvora - Prikaži sat u stvarnom vremenu - Prikaži sat stvarnog vremena na vrhu zaslona. Odnosi se na početnu stranicu i player - Postavke profila - Sakrij izvore s negativnim prioritetom - Sakrij izvore s pogreškama - - %d skrivena poveznica - %d skrivene poveznice - %d skrivenih poveznica - - Pozadina diff --git a/app/src/main/res/values-b+hu/strings.xml b/app/src/main/res/values-b+hu/strings.xml index b753eadb2..c50b18133 100644 --- a/app/src/main/res/values-b+hu/strings.xml +++ b/app/src/main/res/values-b+hu/strings.xml @@ -10,7 +10,8 @@ Szolgáltató Váltás Sebesség (%.2fx) Értékelés: %.1f - Új frissítés található! \n%1$s -> %2$s + Új frissítés található! +\n%1$s -> %2$s %d perc %1$sEp%2$d CloudStream @@ -148,7 +149,8 @@ Ep Nem található epizód Fájl törlése - %dp \nhátra + %dp +\nhátra Időtartam Elérhető Használatban @@ -204,7 +206,8 @@ %1$s %2$d%3$s Nincs évad +30 - Ezzel véglegesen törli a %s \nBiztosan törli? + Ezzel véglegesen törli a %s +\nBiztosan törli? Folyamatban levő Év Webhely @@ -320,7 +323,8 @@ Támogatott Alkalmazásfrissítés letöltése… Frissítve (újabbtól a régebbihez) - Úgy tűnik, a könyvtárad üres :( \nJelentkezz be egy könyvtár fiókba, vagy adj hozzá műsorokat a helyi könyvtárodhoz. + Úgy tűnik, a könyvtárad üres :( +\nJelentkezz be egy könyvtár fiókba, vagy adj hozzá műsorokat a helyi könyvtárodhoz. Úgy tűnik, ez a lista üres, próbálj meg egy másikra váltani. Max 4K @@ -412,7 +416,9 @@ Zárt feliratok eltávolítása a feliratokból 18+ Ez az összes tároló bővítményt is törli - A CloudStream alapértelmezés szerint nem telepített webhelyeket. A webhelyeket a tárolókból kell telepítenie. \n \nCsatlakozz a Discord-unkhoz vagy keress online. + A CloudStream alapértelmezés szerint nem telepített webhelyeket. A webhelyeket a tárolókból kell telepítenie. +\n +\nCsatlakozz a Discord-unkhoz vagy keress online. Verzió Megjelölés megtekintettként Eltávolítás a megnézettek közül @@ -466,7 +472,8 @@ Közreműködők Betűrendben (Z-től az A-ig) Könyvtár kiválasztása - Biztonságos módú fájlba ütköztünk! \nNem töltődik be semmilyen kiegészítő indításkor, amíg a fájl nem kerül törlésre. + Biztonságos módú fájlba ütköztünk! +\nNem töltődik be semmilyen kiegészítő indításkor, amíg a fájl nem kerül törlésre. Normál %s betöltve Beállítás kihagyása @@ -529,8 +536,18 @@ Profilok Eltávolítás kedvencekből Adja meg a jelenlegi PIN-t - Itt beállíthatja hogyan rendezze el a forrásokat. Ha egy videónak nagyobb a prioritása, előbb fog megjelenni a listában. A forrás prioritás és a minőség prioritás együttes értéke adja ki a videó prioritást. \n \nForrás A: 3 \nMinőség B: 7 \nEzek összértéke egy 10-es videó prioritást eredményez. \n \nFIGYELEM: Ha az összeg több mint 10, a lejátszó nem tölt be mást ha már a link betöltésre került! - Potenciálisan dupla elemek a könyvtárjában: \n \n%s \n \nSzeretné hozzáadni ezt az elemet mindenképpen, ezzel felülírva a jelenlegit, vagy visszavonja a műveletet? + Itt beállíthatja hogyan rendezze el a forrásokat. Ha egy videónak nagyobb a prioritása, előbb fog megjelenni a listában. A forrás prioritás és a minőség prioritás együttes értéke adja ki a videó prioritást. +\n +\nForrás A: 3 +\nMinőség B: 7 +\nEzek összértéke egy 10-es videó prioritást eredményez. +\n +\nFIGYELEM: Ha az összeg több mint 10, a lejátszó nem tölt be mást ha már a link betöltésre került! + Potenciálisan dupla elemek a könyvtárjában: +\n +\n%s +\n +\nSzeretné hozzáadni ezt az elemet mindenképpen, ezzel felülírva a jelenlegit, vagy visszavonja a műveletet? Fiók választás kihagyása belépéskor Használjon alapértelmezett fiókot Elforgatás @@ -539,7 +556,9 @@ %s hozzáadva a kedvencekhez %s eltávolítva a kedvencekből Hozzáadás a kedvencekhez - Úgy tűnik egy potenciálisan dupla elem már létezik a könyvtárjában: \'%s.\' \n \nMindenképpen hozzá akarja adni ezt az elemet, ezzel felülírva a régit, vagy visszavonja a műveletet? + Úgy tűnik egy potenciálisan dupla elem már létezik a könyvtárjában: \'%s.\' +\n +\nMindenképpen hozzá akarja adni ezt az elemet, ezzel felülírva a régit, vagy visszavonja a műveletet? Adja meg a PIN-t Profil Zárolása Válasszon egy fiókot diff --git a/app/src/main/res/values-b+in/strings.xml b/app/src/main/res/values-b+in/strings.xml index e9df06fc4..9c4c7fc11 100644 --- a/app/src/main/res/values-b+in/strings.xml +++ b/app/src/main/res/values-b+in/strings.xml @@ -169,8 +169,10 @@ Lanjutkan -30 +30 - Ini akan secara permanen menghapus %s \nApakah anda yakin? - %dm \ntersisa + Ini akan secara permanen menghapus %s +\nApakah anda yakin? + %dm +\ntersisa Masih Berlanjut Tamat Status @@ -313,8 +315,8 @@ Acak Segera hadir… Cam - CamRip - CamHD + Cam + Cam HQ HD TS @@ -388,7 +390,9 @@ %d plugin diperbarui Lihat repositori komunitas Daftar publik - CloudStream tidak memiliki situs yang terinstal secara default. Anda perlu menginstal situs-situs dari repositori. \n \nBergabunglah dengan Discord kami atau cari secara online. + CloudStream tidak memiliki situs yang terinstal secara default. Anda perlu menginstal situs-situs dari repositori. +\n +\nBergabunglah dengan Discord kami atau cari secara online. URL Repositori atau Kode Pendek Buat Akun Error @@ -444,7 +448,7 @@ Telah ditonton Data salah Hapus closed caption dari subtitlel - Hapus iklan dari subtitle + Hapus teks tidak perlu pada subtitel Audio Trek Video Trek Dukungan @@ -483,7 +487,8 @@ Hapus dari tontonan Peramban Pilih pustaka - Yahh daftar pustaka kamu kosong :( \nMasuk ke akun pustaka atau tambah perlihatkan ke lokal pustaka kamu. + Yahh daftar pustaka kamu kosong :( +\nMasuk ke akun pustaka atau tambah perlihatkan ke lokal pustaka kamu. Pustaka Urutkan berdasarkan Urutkan @@ -495,7 +500,8 @@ Abjad (Z ke A) Buka dengan Yahh daftar ini kosong. Coba ganti ke yang lain. - Mode aman file ditemukan! \nTidak memuat ekstensi pada startup sampai berkas dihapus. + Mode aman file ditemukan! +\nTidak memuat ekstensi pada startup sampai berkas dihapus. Sembunyikan Pemutaran - Geser Pemutar terlihat - Geser Geser untuk menghilangkan @@ -521,7 +527,13 @@ Kualitas nonton yang diinginkan (Data Seluler) Data seluler Bantuan - Di sini anda dapat mengubah sumber yang diurutkan. Jika video memiliki prioritas lebih tinggi, video tersebut akan muncul lebih tinggi dalam pemilihan sumber. Jumlah prioritas sumber dan prioritas kualitas adalah prioritas video. \n \nSumber A: 3 \nKualitas B: 7 \nAkan memiliki prioritas video yang digabung 10. \n \nCATATAN: Jika jumlahnya 10 atau lebih, pemain akan melewatkan pemuatan secara otomatis saat tautan dimuat! + Di sini anda dapat mengubah sumber yang diurutkan. Jika video memiliki prioritas lebih tinggi, video tersebut akan muncul lebih tinggi dalam pemilihan sumber. Jumlah prioritas sumber dan prioritas kualitas adalah prioritas video. +\n +\nSumber A: 3 +\nKualitas B: 7 +\nAkan memiliki prioritas video yang digabung 10. +\n +\nCATATAN: Jika jumlahnya 10 atau lebih, pemain akan melewatkan pemuatan secara otomatis saat tautan dimuat! Profil %d Wifi Pengaturan default @@ -581,7 +593,8 @@ Setelah beberapa kali gagal, perintah akan ditutup. Cukup mulai ulang aplikasi untuk mencoba lagi. Batalkan favorit Buka kunci CloudStream - %s \ntersisa + %s +\ntersisa Favorit Kunci dengan Biometrik Nama dan URL repositori @@ -625,10 +638,18 @@ Pilih Semua Batal Pilih Semua Hapus File - Apakah Anda yakin ingin menghapus item berikut secara permanen? \n \n%s - Apakah Anda yakin ingin menghapus episode berikut di %1$s secara permanen? \n \n%2$s - Anda juga akan menghapus semua episode dalam seri berikut secara permanen: \n \n%s - Apakah Anda yakin ingin menghapus semua episode dalam seri berikut secara permanen? \n \n%s + Apakah Anda yakin ingin menghapus item berikut secara permanen? +\n +\n%s + Apakah Anda yakin ingin menghapus episode berikut di %1$s secara permanen? +\n +\n%2$s + Anda juga akan menghapus semua episode dalam seri berikut secara permanen: +\n +\n%s + Apakah Anda yakin ingin menghapus semua episode dalam seri berikut secara permanen? +\n +\n%s Hapus (%1$d | %2$s) Hapus plugin Tidak bisa mendapatkan kode PIN perangkat, coba autentikasi lokal @@ -661,7 +682,7 @@ Rating %s Perbarui Plugin Mulai proses pembaruan plugin! - Berhasil memperbarui %d plugin(s) + Berhasil memperbarui %d plugin! Perbarui plugin secara manual Tidak ada plugin yang diperbarui. Pemberitahuan pemutar untuk mengontrol pemutaran dari latar belakang @@ -744,20 +765,4 @@ Apakah kamu ingin mengunduh episode %s? Apakah kamu ingin membatalkan semua unduhan dalam antrean? Tampilkan overlay metadata pemutar - Live - Video - Pratinjau - Tampilkan jam waktu nyata - Tampilkan jam waktu nyata di bagian atas layar. Berlaku untuk halaman rumah dan pemain - Pengaturan profil - Sembunyikan sumber dengan prioritas negatif - Sembunyikan sumber dengan kesalahan - - %d link tersembunyi - - Edge - Logcat 🐈 - Memulai video yang sedang dijeda - Kurangi stres saat memilih sumber informasi - Latar Belakang diff --git a/app/src/main/res/values-b+it/strings.xml b/app/src/main/res/values-b+it/strings.xml index db181baf5..55da49687 100644 --- a/app/src/main/res/values-b+it/strings.xml +++ b/app/src/main/res/values-b+it/strings.xml @@ -6,7 +6,7 @@ L\'episodio %d uscirà in %1$dd %2$dh %3$dm %1$dh %2$dm - %d min + %d min Poster Poster @@ -19,7 +19,8 @@ Velocità (%.2fx) Valutato: %.1f - Nuovo aggiornamento trovato! \n%1$s -> %2$s + Nuovo aggiornamento trovato! +\n%1$s -> %2$s Filler %d min @@ -185,8 +186,10 @@ Riprendi -30 +30 - Stai per eliminare permanentemente %s \nSei sicuro? - %dm \nrimanenti + Stai per eliminare permanentemente %s +\nSei sicuro? + %dm +\nrimanenti In corso Completato Stato @@ -225,7 +228,7 @@ Livestream NSFW Altro - Errore fonte + Errore sorgente Errore remoto Errore del renderer Errore imprevisto del lettore @@ -247,7 +250,7 @@ Controlla aggiornamenti Blocca Ridimensiona - Fonte + Sorgente Salta OP Non mostrare di nuovo Salta questo aggiornamento @@ -346,12 +349,12 @@ Protagonista Supporto Secondario - Fonte + Sorgente Casuale Prossimamente… Cam - CamRip - CamHD + Cam + Cam HQ HD TS @@ -409,15 +412,17 @@ Disabilitato: %d Non scaricato: %d Aggiornati %d plugin - CloudStream non ha siti installati per impostazione predefinita. È necessario installare i siti dai repository. \n \nUnisciti al nostro Discord o cerca online. + CloudStream non ha siti installati per impostazione predefinita. È necessario installare i siti dai repository. +\n +\nUnisciti al nostro Discord o cerca online. Visualizza i repository della comunità Lista pubblica Tutti i sottotitoli in maiuscolo Attenzione: CloudStream non si assume alcuna responsabilità per l\'utilizzo di estensioni di terze parti e non fornisce alcun supporto per esse! %s (Disabilitato) Tracce - Tracce audio - Tracce video + Traccia audio + Traccia video Riavvia app per visualizzare le modifiche. Safe mode attiva Tutte le estensioni sono state disabilitate a causa di un arresto anomalo per aiutarti a trovare l\'estensione che causa il problema. @@ -455,7 +460,7 @@ Opening misto Installazione aggiornamento dell\'app… Rimuovi le didascalie chiuse dai sottotitoli - Rimuovere gli annunci dai sottotitoli + Rimuovi il bloat dai sottotitoli Opening Ending Ending misto @@ -497,13 +502,15 @@ Aggiornato (Da vecchio a nuovo) Alfabetico (A - Z) Alfabetico (Z - A) - La tua libreria è vuota :( \nAccedi a un account di libreria o aggiungi degli show alla tua libreria locale. + La tua libreria è vuota :( +\nAccedi a un account di libreria o aggiungi degli show alla tua libreria locale. Seleziona libreria Apri con Libreria Ordina Questo elenco è vuoto. Prova a passare a un altro. - File \"safe mode\" trovato! \nAll\'avvio non sarà caricata alcuna estensione finchè il file non verrà rimosso. + File \"safe mode\" trovato! +\nAll\'avvio non sarà caricata alcuna estensione finchè il file non verrà rimosso. Intervallo di ricerca utilizzato quando il lettore è nascosto TV Android Intervallo di ricerca utilizzato quando il lettore è visibile @@ -527,7 +534,13 @@ Aggiornando shows a cui sei iscritto L\'episodio %d è stato rilasciato! Qualità di visualizzazione preferita (Dati mobili) - Qui puoi cambiare l\'ordine delle fonti. Se un video ha una priorità maggiore, apparirà più in alto nella selezione delle fonti. La somma tra la priorità della fonte e la priorità della qualità è la priorità video. \n \nFonte A: 3 \nQualità B: 7 \nAvranno una priorità video combinata di 10. \n \nNOTA: se la somma è pari o superiore a 10, il lettore salterà automaticamente il caricamento quando viene caricato quel link! + Qui puoi cambiare l\'ordine delle fonti. Se un video ha una priorità maggiore, apparirà più in alto nella selezione delle fonti. La somma tra la priorità della fonte e la priorità della qualità è la priorità video. +\n +\nFonte A: 3 +\nQualità B: 7 +\nAvranno una priorità video combinata di 10. +\n +\nNOTA: se la somma è pari o superiore a 10, il lettore salterà automaticamente il caricamento quando viene caricato quel link! Profilo %d Wi-Fi Imposta predefinito @@ -547,7 +560,11 @@ %s rimosso dai preferiti Preferiti %s aggiunto ai preferiti - Dei possibili duplicati sono stati trovati nella tua libreria: \n \n%s \n \nVorresti aggiungere l\'oggetto alla libreria comunque, rimpiazzare l\'esistente, o cancellare l\'azione? + Dei possibili duplicati sono stati trovati nella tua libreria: +\n +\n%s +\n +\nVorresti aggiungere l\'oggetto alla libreria comunque, rimpiazzare l\'esistente, o cancellare l\'azione? Frequenza di backup Trovato Possibile Duplicato Aggiungi ai preferiti @@ -560,7 +577,9 @@ Iscriviti Rimuovi dai preferiti Seleziona un account - Sembra che nella tua libreria esista già un elemento potenzialmente duplicato: \'%s.\' \n \nDesideri aggiungere comunque questo elemento, sostituire quello esistente o annullare l\'azione? + Sembra che nella tua libreria esista già un elemento potenzialmente duplicato: \'%s.\' +\n +\nDesideri aggiungere comunque questo elemento, sostituire quello esistente o annullare l\'azione? Inserisci PIN PIN Inserisci PIN corrente @@ -590,7 +609,8 @@ Dopo alcuni tentativi falliti, il prompt si chiuderà. Riavvia semplicemente l\'app per riprovare. È stato eseguito il backup dei tuoi dati CloudStream. Sebbene questa possibilità sia molto bassa, tutti i dispositivi possono comportarsi in modo diverso. Nel raro caso in cui ti venga bloccato l\'accesso all\'app, cancella completamente i dati dell\'app e ripristina da un backup. Siamo molto spiacenti per qualsiasi inconveniente derivanti da questo. Non preferito - %s \nresiduo + %s +\nresiduo Preferito Nome e URL del repository copiato! @@ -632,12 +652,20 @@ Seleziona tutto Deseleziona tutto Elimina (%1$d | %2$s) - Sei sicuro di voler eliminare definitivamente i seguenti episodi in %1$s? \n \n%2$s - Eliminerai definitivamente anche tutti gli episodi delle seguenti serie: \n \n%s - Sei sicuro di voler eliminare definitivamente tutti gli episodi delle seguenti serie? \n \n%s + Sei sicuro di voler eliminare definitivamente i seguenti episodi in %1$s? +\n +\n%2$s + Eliminerai definitivamente anche tutti gli episodi delle seguenti serie: +\n +\n%s + Sei sicuro di voler eliminare definitivamente tutti gli episodi delle seguenti serie? +\n +\n%s Data di rilascio (dal più vecchio) Elimina file - Sei sicuro di voler eliminare definitivamente i seguenti elementi? \n \n%s + Sei sicuro di voler eliminare definitivamente i seguenti elementi? +\n +\n%s Anteprima barra di avanzamento Abilita miniatura di anteprima sulla barra di avanzamento Nessun sottotitolo caricato @@ -670,7 +698,7 @@ Data di messa in onda (più vecchia) Valutazione %s Avvio del processo di aggiornamento plugin! - %d plugin aggiornati correttamente + %d plugin aggiornati con successo! Nessun plugin è stato aggiornato. Notifiche lettore La notifica del lettore per controllare la riproduzione in background @@ -733,7 +761,7 @@ Cancella suggerimenti Mostra pannello cast Info sui media - Nome fonte + Nome sorgente Luminosità extra Attiva il filtro di luminosità quando viene superato il 100% della luminosità dello schermo extra_brightness_enabled @@ -753,41 +781,7 @@ %d download in coda %d download in coda - Priorità fonti - Decidi come le fonti video devono essere ordinate nel lettore + Priorità sorgente + Decidi come le sorgenti video devono essere ordinate nel lettore Mostra sovrapposizione metadati lettore - Video - Anteprima - Live - Impostazioni profilo - Nascondi fonti con priorità negativa - Nascondi fonti con errori - - %d collegamento nascosto - %d collegamenti nascosti - %d collegamenti nascosti - - Mostra orologio in tempo reale - Mostra un orologio in tempo reale nella parte superiore dello schermo. Si applica alla homepage e al lettore - Logcat 🐈 - Avvia video in pausa - Riduci lo stress nella scelta delle fonti - Sfondo - Bordo - Compressore - Mostra i controlli del compressore nel player (aiuta a bilanciare l\'audio elevato/debole) - Attivo - Attivo · %1$d dB / +%2$d dB - Disattivato - Chiaro - Aumento dialoghi - Intenso - Soglia: %d dB - Compressore gamma dinamica - Guadagno compensazione: +%d dB - I suoni più forti di %d dB vengono abbassati - Aumenta il volume generale di +%d dB - URL - Selettore immagini - Immagini predefinite diff --git a/app/src/main/res/values-b+iw/strings.xml b/app/src/main/res/values-b+iw/strings.xml index aa53c35ef..0b0479679 100644 --- a/app/src/main/res/values-b+iw/strings.xml +++ b/app/src/main/res/values-b+iw/strings.xml @@ -6,7 +6,8 @@ לשנות ספק מהירות (%.2fx) דירוג: %.1f - נמצא עדכון חדש! \n%1$s -> %2$s + נמצא עדכון חדש! +\n%1$s -> %2$s סינון %d דקות קלאודסטרים @@ -145,8 +146,10 @@ המשך -30 +30 - %dדקות \nנותרו - ‬פעולה זאת תמחק לצמיתות את %s \nהאם אתם בטוחים? + %dדקות +\nנותרו + ‬פעולה זאת תמחק לצמיתות את %s +\nהאם אתם בטוחים? מתמשך משך זמן דירוג @@ -170,7 +173,7 @@ עדכון איכות צפייה מועדפת (WiFi) נגן וידאו כותרת מקסימום תווים - הצג מידע על הנגן + רזולוציית נגן וידאו הוסף מעקב צור חשבון הבא @@ -178,7 +181,7 @@ מטא-דאטה לא מסופק על ידי האתר, טעינת הסרטון תיכשל אם הוא לא קיים באתר. החלק בצד השמאלי או הימני כדי לשנות את הבהירות או עוצמת הקול שחזור הנתונים מהקובץ נכשל %s - עדכונים וגיבוי + עדכונים וגיבויים נותן לך את תוצאות החיפוש מופרדות לפי ספק הצג פרק פילר (לא הכרחי לעלילה) לאנימה הורדה אוטומטית של תוספים @@ -190,7 +193,7 @@ שגיאת מעבד הורד מראה הורד כתוביות - הצג כפתור אקראי בדף הבית ובספרייה + הצג כפתור אקראי בדף הבית תווית דיבוב גורם לקריסות אם מוגדר גבוה מדי במכשירים עם זיכרון נמוך, כגון Android TV. מקור @@ -232,21 +235,21 @@ השתמש בזה אם הכתוביות מוצגות %d מילישניות מוקדם יותר אקראי רזולוציה - CamHD + איכות מצלמה כתובת אתר לא מזוהה - https://example.com/example.mp4 - Referer (אופציונלי) + קישור לשידור + המפנה TC SD UHD - כתובת URL של המאגר או קוד מקוצר + כתובת אתר למאגר מדיה מועדפת /%d נגן את הפרק לא נמצאו קישורים קוד שפה (אנגלית) שגיאה - שפות ההרחבות + שפות ספק קישורים פריסת אפליקציה חיפוש מתקדם @@ -258,7 +261,7 @@ כמות הזזת הנגן (שניות) סרטון גיטהאב - CamRip + מצלמה החלק כדי לשנות הגדרות ‪דפדפן צבע חלון @@ -280,7 +283,7 @@ הצג פוסטרים מKitsu הסתר את איכות הסרטון שנבחרה בתוצאות החיפוש מתקין APK - חלק מהמכשירים אינם תומכים במתקין החבילות החדש. אם עדכונים לא מותקנים, נסה את האפשרות הישנה. + חלק מהטלפונים אינם תומכים במתקין החבילות החדש. נסו את האפשרות מדור קודם אם העדכונים לא מתקינים. אפליקצית רומנים קלים על ידי אותו מפתחים אפליקצית אנימה על ידי אותו מפתחים הצטרפות לדיסקורד @@ -347,7 +350,7 @@ תכונות כללי כפתור אקראי - אפשר תוכן למבוגרים (NSFW) בהרחבות נתמכות + הרשה NSFW בספקים נתמכים קידוד כתוביות פריסה פריסת טלפון @@ -355,11 +358,11 @@ צבע ראשוני שים את הכותרת מתחת לפוסטר סיסמה123 - שם משתמש + שםהמשתמשהמגניבשלי hello@world.com 127.0.0.1 - שם האתר החדש - https://example.com + האתרהמגניבשלי + דוגמה.com חשבון החלף חשבון הוסף חשבון @@ -398,7 +401,7 @@ לא מזוהה ID נתונים לא מזוהים הסר כיתובים סגורים מהכתוביות - הסר פרסומות מהכתוביות + הסר נפיחות מכתוביות תוספות טריילר צפה בסרטונים בשפות אלה @@ -407,7 +410,7 @@ סוים הרחבות הוסף מאגר - שם המאגר (אופציונלי) + שם מאגר תוסף הורד התוסף נמחק לא יכול לטעון %s @@ -422,12 +425,14 @@ קרדיטים מיין בחר ספרייה - נראה שהספרייה שלכם ריקה :( \nהתחברו לחשבון ספריה או הוסיפו סדרות לספרייה המקומית שלכם. - קובץ מצב בטוח נמצא! \nלא טוען שום תוספות בהפעלה עד להסרת הקובץ. + נראה שהספרייה שלכם ריקה :( +\nהתחברו לחשבון ספריה או הוסיפו סדרות לספרייה המקומית שלכם. + קובץ מצב בטוח נמצא! +\nלא טוען שום תוספות בהפעלה עד להסרת הקובץ. לא ניתן להתקין את הגרסה החדשה של האפליקציה הורדת אצווה תוסף - אזהרה: קלאודסטרים אינה לוקחת אחריות על השימוש בהרחבות צד שלישי ואינה מספקת להן תמיכה כלשהי! + הורד את כל התוספים ממאגר זה? רצועות שמע מסלולים כל התוספים נכבו עקב התרסקות כדי לעזור לך למצוא את האחד הגורם לצרות. @@ -439,13 +444,17 @@ הורד: %d מוגבל: %d לא מורד: %d - לקלאודסטרים אין אתרים מותקנים כברירת מחדל. עליך להתקין את האתרים ממאגרים. \n \nהצטרף לדיסקורד שלנו או חפש באינטרנט. + לקלאודסטרים אין אתרים מותקנים כברירת מחדל. עליכם להתקין את האתרים ממאגרים. +\n +\nבגלל הסרת DMCA על ידי Sky UK LImited 🤮 אנחנו לא יכולים לקשר את אתר המאגרים באפליקציה. +\n +\nתצטרפו לדיסקורד שלנו או תחפשו באינטרנט. הצג מאגרים קהילתיים רשימה ציבורית לשים את הכתוביות באותיות רישיות %s (מוגבל) רצועות וידאו - הפעל מחדש את האפליקציה כדי לראות את השינויים. + החל על הפעלה מחדש מצב בטוח מופעל הצג מידע על ההתרסקות דירוג: %s @@ -489,7 +498,7 @@ רשום חזור הרשמה ל-%s מבוטלת - מתווך GitHub + פרוקסי עבור raw.githubusercontent.com פרופילים הממשק לא נוצר נכון. זוהי שגיאה רצינית, נא לדווח עליה מיידית ל-%s עריכה @@ -503,7 +512,7 @@ הזמן שיזוז כשהנגן מוצג נגן מוצג - כמות הזזה אתחול - עקיפת חסימה של כתובות raw של GitHub באמצעות jsDelivr. עלול לגרום לעיכוב של כמה ימים בעדכונים. + עוקף את החסימה של GitHub באמצעות jsDelivr. עלול לגרום לעדכונים להתעכב בכמה ימים. בחר מצב כדי לסנן תוספים להורדה איכויות השתמש @@ -521,7 +530,13 @@ קביעה כברירת מחדל עבר מעקף ספק אינטרנט - כאן ניתן לשנות את סדר המקורות. אם לוידיאו יש עדיפות גבוהה יותר, הוא יופיע גבוה יותר בבחירת המקור. סכום עדיפות המקור ועדיפות האיכות היא עדיפות הוידיאו. \n \nמקור א: 3 \nאיכות ב: 7 \nיגרמו לעדיפות הסרטון להיות 10. \n \nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! + כאן ניתן לשנות את סדר המקורות. אם לוידיאו יש עדיפות גבוהה יותר, הוא יופיע גבוה יותר בבחירת המקור. סכום עדיפות המקור ועדיפות האיכות היא עדיפות הוידיאו. +\n +\nמקור א: 3 +\nאיכות ב: 7 +\nיגרמו לעדיפות הסרטון להיות 10. +\n +\nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! עונה %1$d פרק %2$d תשודר ב: %1$d שעות %2$d דקות %3$d שניות %1$d דקות %2$d שניות @@ -544,228 +559,4 @@ הראה המלצות מוסיף אפשרות מהירות בנגן תדירות גיבוי - מועדפים - %s נוסף למועדפים - %s הוסר מהמועדפים - הוסף למועדפים - הסר מהמועדפים - הירשם - בטל הרשמה - נמצא פריט כפול אפשרי - הוסף - החלף - החלף הכול - נראה שפריט שעשוי להיות כפול כבר קיים בספרייה שלך: \'%s.\' \n\nהאם להוסיף את הפריט בכל זאת, להחליף את הפריט הקיים או לבטל את הפעולה? - נמצאו בספרייה שלך פריטים שעשויים להיות כפולים: \n\n%s \n\nהאם להוסיף את הפריט בכל זאת, להחליף את הפריטים הקיימים או לבטל את הפעולה? - הזן קוד PIN - הזן את קוד ה־PIN הנוכחי - נעל פרופיל - PIN - קוד PIN שגוי. נא לנסות שוב. - קוד ה־PIN חייב להכיל 4 תווים - בחר חשבון - הזן קוד PIN עבור %s - ניהול חשבונות - ערוך חשבון - מחובר בתור %s - דלג על בחירת החשבון בהפעלה - השתמש בחשבון ברירת המחדל - סיבוב - הצג כפתור להחלפת כיוון המסך - אפשר החלפה אוטומטית של כיוון המסך בהתאם לכיוון הסרטון - סיבוב אוטומטי - בדוק את כל ההרחבות - בדיקה זו מיועדת למפתחים בלבד ואינה מאשרת או שוללת את תקינות פעולתה של הרחבה כלשהי. - בטל את נעילת קלאודסטרים - נעילה באמצעות זיהוי ביומטרי - אימות באמצעות סיסמה/PIN - אימות ביומטרי אינו נתמך במכשיר זה - בטל את נעילת האפליקציה באמצעות טביעת אצבע, זיהוי פנים, PIN, תבנית וסיסמה. - לאחר כמה ניסיונות כושלים, החלון ייסגר. פשוט הפעל מחדש את האפליקציה כדי לנסות שוב. - הנתונים שלך בקלאודסטרים גובו כעת. אף שהסיכוי לכך נמוך מאוד, כל מכשיר עשוי להתנהג אחרת. במקרה הנדיר שבו תינעל מחוץ לאפליקציה, נקה את נתוני האפליקציה לחלוטין ושחזר מגיבוי. אנו מתנצלים מאוד על כל אי נוחות שעלולה להיגרם מכך. - %s\nנותרו - מועדף - הסר ממועדפים - שגיאה בגישה ללוח העריכה, נא לנסות שוב. - שגיאה בהעתקה, נא להעתיק את ה־logcat ולפנות לתמיכה של האפליקציה. - אישור - השבת את מיטוב הסוללה - כדי להבטיח הורדות והתראות רציפות עבור סדרות טלוויזיה שנרשמת אליהן, קלאודסטרים זקוקה להרשאה לפעול ברקע. בלחיצה על אישור תוצג לך בקשת הרשאה. נא ללחוץ על \'אפשר\'.\n\nלתשומת לבך, הרשאה זו אינה אומרת ש־CS3 תרוקן את הסוללה שלך. היא תפעל ברקע רק כשיש בכך צורך, למשל בעת קבלת התראות או הורדת סרטונים מהרחבות רשמיות. - השימוש בסוללה של האפליקציה כבר מוגדר כבלתי מוגבל - לא ניתן לפתוח את פרטי האפליקציה של קלאודסטרים. - ספר שמע - איפוס - בקרוב בעוד %s - שדר מראה - בחר מכשיר שידור - הוויקי של קלאודסטרים - אבטחה - חשבונות - אימות מקומי - תמונת קוד QR - יש להיכנס אל %s בטלפון החכם או במחשב ולהזין את הקוד שלמעלה - לא ניתן לקבל את קוד ה־PIN של המכשיר, נסה אימות מקומי - תוקף קוד ה־PIN פג ! - תוקף הקוד יפוג בעוד %1$d דק׳ %2$d שנ׳ - סגור - פתח מאגר - אזהרה - מחק תוסף - הסתר את שמות הפקדים של הנגן - תאריך יציאה (מהחדש לישן) - תאריך יציאה (מהישן לחדש) - מחק קבצים - מחק (%1$d | %2$s) - האם אתה בטוח שברצונך למחוק לצמיתות את הפריטים הבאים?\n\n%s - האם אתה בטוח שברצונך למחוק לצמיתות את הפרקים הבאים ב־%1$s?\n\n%2$s - תמחק לצמיתות גם את כל הפרקים בסדרות הבאות:\n\n%s - האם אתה בטוח שברצונך למחוק לצמיתות את כל הפרקים בסדרות הבאות?\n\n%s - תצוגה מקדימה בסרגל ההזזה - אפשר תמונה ממוזערת לתצוגה מקדימה בסרגל ההזזה - עדיין לא נטענו כתוביות - אישור לפני יציאה - הצג חלון אישור לפני היציאה מהאפליקציה - הצג - אל תציג - מיקום תיקיית הגיבוי - מותאם אישית - גודל הקצה - שגיאת קידוד - שגיאה לא נתמכת - טען את הראשונה הזמינה - הפעל טורנט בהגדרות/ספקים/מדיה מועדפת - הפעל מחדש את האפליקציה ואשר את החלון הקופץ של שידור טורנט כדי להמשיך. - פענוח בתוכנה - פענוח בתוכנה מאפשר לנגן להפעיל קובצי וידאו שהמכשיר שלך אינו תומך בהם, אך עלול לגרום לניגון מקוטע או לא יציב ברזולוציה גבוהה. - פרק (סדר עולה) - פרק (סדר יורד) - דירוג (הגבוה ביותר) - דירוג (הנמוך ביותר) - תאריך שידור (החדש ביותר) - תאריך שידור (הישן ביותר) - פרק %s - דירוג %s - תאריך %s - עדכן תוספים - עדכן תוספים ידנית - תהליך עדכון התוספים מתחיל! - התוספים עודכנו בהצלחה: %d - לא עודכנו תוספים. - התראות הנגן - התראת הנגן לשליטה בניגון מהרקע - מוטמעות - מקוונות - הצג את כל הכתוביות בגופן מודגש - הצג את כל הכתוביות בגופן נטוי - רדיוס הרקע - עוצמת הקול חרגה מ־100% - החלק שוב למעלה כדי לחרוג מ־100% - כמה פריטים שונים ניתן להוריד במקביל - הורדות מקבילות - חיבורים בו־זמניים - בכמה חיבורים בו־זמניים כל הורדה יכולה להשתמש במהלך ההורדה - עבור להורדות - אין חיבור לאינטרנט. \n\nנא להתחבר לאינטרנט ולנסות שוב, או לצפות בהורדות שלך במצב לא מקוון. - משנה את גבולות המסך - Overscan - משנה את גודל הפוסטרים - גודל הפוסטרים - הגברת מהירות בלחיצה ארוכה - החזק כדי לקבל מהירות פי 2 - שאל תמיד - תווית דירוג - אין חשבון - ערוך את תמונת הפרופיל - הזן כתובת URL של תמונת הפרופיל - לא נמצאה כתובת URL - כתובת URL או תמונה לא תקינה - התמונה עודכנה בהצלחה - סמן כנצפה עד פרק זה - הסר סימון נצפה עד פרק זה - נטען מחדש - טען מחדש את הספק - נגן מראה" - שם - רזולוציה ושם - יישור הכתוביות - למטה משמאל - למטה במרכז - למטה מימין - באמצע משמאל - באמצע במרכז - באמצע מימין - למעלה משמאל - למעלה במרכז - למעלה מימין - נגן את כל הסדרה - התקן גרסת טרום־הפצה - גרסת טרום־ההפצה כבר מותקנת. - התקנת גרסת טרום־ההפצה נכשלה. - טקסט הפרק - הצעות חיפוש - הצג הצעות חיפוש בזמן ההקלדה - נקה הצעות - הצג את חלונית צוות השחקנים - פרטי המדיה - שם המקור - בהירות נוספת - אפשר מסנן בהירות כאשר חורגים מ־100% בהירות תצוגה - extra_brightness_enabled - תור הורדות - אין כרגע הורדות בתור. - הורד הכול - בטל הכול - האם להוריד את פרק %s? - האם לבטל את כל ההורדות שבתור? - - %d הורדה פעילה - %d הורדות פעילות - %d הורדות פעילות - - - %d הורדה בתור - %d הורדות בתור - %d הורדות בתור - - עדיפות המקורות - קבע כיצד ימוינו מקורות הווידאו בנגן - הצג שכבת מטא־נתונים בנגן - מוזיקה - מדיה - שמע - פודקאסט - וידאו - תצוגה מקדימה - חי - הגדרות פרופיל - הסתר מקורות בעדיפות שלילית - הסתר מקורות עם שגיאות - - %d קישור מוסתר - %d קישורים מוסתרים - %d קישורים מוסתרים - - הצג שעון בזמן אמת - הצג שעון בזמן אמת בראש המסך. חל על דף הבית ועל הנגן - Logcat 🐈 - התחל סרטונים במצב מושהה - הפחתת לחץ בעת בחירת מקורות - קצה - רקע - מדחס - מדחס טווח דינמי - הצגת פקדי המדחס בנגן (מסייע לאזן בין שמע חזק לשקט) - פועל - פועל · %1$d dB / +%2$d dB - כבוי - קל - הגברת דיבור - כבד - סף: %d dB - הגבר פיצוי: +%d dB - צלילים חזקים מ־%d dB מוחלשים - מגביר את עוצמת הקול הכוללת ב־%d dB - כתובת URL - בורר תמונות - תמונות ברירת מחדל diff --git a/app/src/main/res/values-b+ja/strings.xml b/app/src/main/res/values-b+ja/strings.xml index c7bdb4ef0..16cfb49c6 100644 --- a/app/src/main/res/values-b+ja/strings.xml +++ b/app/src/main/res/values-b+ja/strings.xml @@ -62,7 +62,8 @@ ローディング… ブラウザで開く シーズン - 残り \n%d分 + 残り +\n%d分 再生エピソード ダウンロード済 バックアップ @@ -81,7 +82,8 @@ 次のランダム 戻り 評価: %.1f - 新しいアップデートを発見! \n%1$s -> %2$s + 新しいアップデートを発見! +\n%1$s -> %2$s %d分 %sを検索… ソース @@ -383,7 +385,7 @@ お気に入り 生体認証は、このデバイスではサポートされていません 指紋、顔ID、PIN、パターン、パスワードでアプリをロック解除します. - スマホやパソコンで %s にアクセスし、上記のコードを入力してください + スマホやPCで%sにアクセスし、上記のコードを入力してください PINコードが期限切れになりました! ライブラリが空です :(\nライブラリアカウントにログインするか、ローカルライブラリに表示を追加してください。 このリストは空です。 別のものに切り替えてみてください。 @@ -645,7 +647,7 @@ アプリを再起動し、ポップアップに同意して続行します。 ソフトウェアデコーディング ソフトウェアデコーディングにより、機器でサポートされていないビデオファイルを再生できますが、高解像度で遅延や不安定な再生を引き起こす可能性があります。 - %d 個のプラグインを更新しました + %d 個のプラグインを正常に更新しました! 更新されたプラグインはありません。 プレーヤー通知 バックグラウンドから再生を制御するためのプレーヤー通知 @@ -729,16 +731,4 @@ ソースの優先順位 プレイヤーでのビデオソースの並び順を設定します - プレイヤーメタデータオーバーレイを表示 - 動画 - プレビュー - ライブ - 現地時刻を表示 - 画面上部に現地時刻を表示します。ホーム画面とプレイヤーに適用されます - プロフィール設定 - 優先度がマイナスのソースを非表示 - エラーが発生したソースを非表示 - - %d件の非表示リンク - diff --git a/app/src/main/res/values-b+kn/strings.xml b/app/src/main/res/values-b+kn/strings.xml index ba6da787c..22a45b906 100644 --- a/app/src/main/res/values-b+kn/strings.xml +++ b/app/src/main/res/values-b+kn/strings.xml @@ -83,7 +83,8 @@ ಶೇರ್ ಫೈಲ್ ಅಳಿಸಿ ಹೆಚ್ಚಿನ ಮಾಹಿತಿ - ಹೊಸ ಅಪ್ಡೇಟ್ ಬಂದಿದೆ \n%1$s-%2$s + ಹೊಸ ಅಪ್ಡೇಟ್ ಬಂದಿದೆ +\n%1$s-%2$s ಲೋಡಿಂಗ್… ಡೌನ್‌ಲೋಡ್ ಭಾಷೆಗಳನ್ನು ಮಾಡಿ ಲೈವ್‌ಸ್ಟ್ರೀಮ್ ಪ್ಲೇ ಮಾಡಿ diff --git a/app/src/main/res/values-b+ko/strings.xml b/app/src/main/res/values-b+ko/strings.xml index 966d13020..9b07d259e 100644 --- a/app/src/main/res/values-b+ko/strings.xml +++ b/app/src/main/res/values-b+ko/strings.xml @@ -11,7 +11,8 @@ 배경 미리보기 속도 (%.2fx) 평점: %.1f - 새로운 업데이트! \n%1$s -> %2$s + 새로운 업데이트! +\n%1$s -> %2$s %d분 CloudStream CloudStream으로 재생 @@ -27,7 +28,7 @@ 공유 브라우저로 열기 브라우저 - 로딩 스킵 + 로딩 건너뛰기 로딩중… 시청 보류 @@ -56,8 +57,8 @@ 재생 속도 글자 색상 윤곽선 색상 - 자막 배경 색상 - 자막 창 색상 + 배경 색상 + 배경 색상 윤곽선 유형 자막 높이 폰트 @@ -123,7 +124,7 @@ 어두운 오버레이 대신 앱 플레이어의 시스템 밝기를 사용합니다 시청 진행 상황 업데이트 현재 에피소드 진행 상황을 자동으로 동기화합니다 - 데이터 복원 + 백업에서 데이터 복원 데이터 백업 파일에서 데이터를 복원하지 못했습니다 %s 저장된 데이터 @@ -160,8 +161,10 @@ 년 평점 +30 - %s이(가) 영구적으로 삭제됩니다 \n정말 삭제하시겠습니까? - %d분 \n남음 + %s가 영구 삭제됩니다 +\n정말 삭제하시겠습니까? + %d분 +\n남음 사이트 시간 개요 @@ -181,7 +184,7 @@ %s로 재생 자동 다운로드 다운로드 소스 목록 - 링크 초기화 + 링크 새로고침 자막 다운로드 화질 라벨 더빙 라벨 @@ -192,7 +195,7 @@ 크기 조정 소스 오프닝 스킵 - 다음에 업데이트 + 이 업데이트 건너뛰기 선호하는 화질 (WiFi) 선호하는 화질 (모바일 데이터) 플레이어 내 표시 정보 @@ -262,7 +265,7 @@ 포스터 이미지 제목 Cam - CamRip + Cam Blu-ray WP DVD @@ -294,7 +297,11 @@ 저장소 삭제 사용하려는 사이트 목록 다운로드 다운로드됨: %d - CloudStream에는 기본적으로 설치된 사이트가 없습니다. 저장소에서 사이트를 설치해야 합니다. \n \nDiscord에 가입하거나 온라인으로 검색해 보세요. + CloudStream에는 기본적으로 설치된 사이트가 없습니다. 저장소에서 사이트를 설치해야 합니다. +\n +\nSky UK Limited의 무분별한 DMCA 게시 중단으로 인해 앱에서 저장소 사이트를 연결 할 수 없습니다. +\n +\nDiscord에 가입하거나 온라인으로 검색해 보세요. 커뮤니티 저장소 보기 공개 목록 자막 대문자화 표시 @@ -315,7 +322,7 @@ 충돌 정보 보기 언어 에피소드 %d 공개! - Picture-in-picture 모드 + PIP 모드 플레이어 크기 조정 버튼 미니플레이어를 통해 다른 앱 상단에서 계속 재생합니다 레터박스 제거 @@ -324,7 +331,7 @@ 백업 파일을 성공적으로 로드하였습니다 정보 고급 검색 - 설정 프로세스 재실행 + 설정 프로세스 다시 실행 APK 인스톨러 Github 소스 오류 @@ -405,7 +412,7 @@ 인터넷에서 불러오기 로드됨 %s 공개 예정… - CamHD + Cam HD TS 플레이어 @@ -431,16 +438,16 @@ 먼저 확장 프로그램을 설치하세요 앱을 찾을 수 없음 모든 언어 - %s 스킵 + 건너뛰기 %s 오프닝 엔딩 혼합 엔딩 혼합 오프닝 크레딧 - 인트로 + 소개 기록 삭제 기록 - 오프닝/엔딩 시 스킵 팝업 표시 + 오프닝/엔딩 시 건너뛰기 팝업 표시 텍스트가 너무 많습니다. 클립보드에 저장할 수 없습니다. 시청에서 삭제 정말 종료하시겠습니까? @@ -459,8 +466,10 @@ 알파벳순 (A에서 Z) 알파벳순 (Z에서 A) 다음으로 열기 - 라이브러리가 비어 있습니다 :( \n라이브러리 계정으로 로그인하거나 로컬 라이브러리에 프로그램을 추가하세요. - 안전 모드 파일을 찾았습니다! \n파일이 제거될 때까지 시작 시 확장 프로그램을 로드하지 않습니다. + 라이브러리가 비어 있습니다 :( +\n라이브러리 계정으로 로그인하거나 로컬 라이브러리에 프로그램을 추가하세요. + 안전 모드 파일을 찾았습니다! +\n파일이 제거될 때까지 시작 시 확장 프로그램을 로드하지 않습니다. HLS 재생목록 내부 플레이어 선호하는 동영상 플레이어 @@ -476,7 +485,7 @@ @string/home_play 플롯을 찾을 수 없음 설명을 찾을 수 없음 - 로그캣 🐈 보기 + Logcat 🐈 표시 애니메이션용 필러 에피소드 표시 통과 계속 @@ -493,7 +502,7 @@ 음각 양각 다람쥐 헌 쳇바퀴에 타고파 - 자막에서 광고 제거 + 불필요한 요소/코드 제거 엑스트라 https://example.com/example.mp4 트랙 @@ -508,11 +517,11 @@ 보안 계정 리포지토리에서 플러그인을 찾을 수 없습니다 - 복사 완료! + 복사됨! 레포지토리 이름 및 URL 본 테스트는 개발자만을 대상으로 하며, 확장자의 작업을 확인하거나 거부하지 않습니다. CloudStream 위키 - 링크 초기화 완료 + 링크 새로고침 완료 백업 빈도 즐겨찾기 QR 이미지 @@ -563,11 +572,17 @@ 기본값 설정 구독 사용 - 당신의 라이브러리에 이미 잠재적으로 중복된 항목이 존재합니다: \'%s\'. \n \n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? + 당신의 라이브러리에 이미 잠재적으로 중복된 항목이 존재합니다: \'%s\'. +\n +\n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? 전부 대체 추가 즐겨찾기에서 %s 제거 - 당신의 라이브러리에 잠재적으로 중복된 항목이 발견되었습니다: \n \n%s \n \n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? + 당신의 라이브러리에 잠재적으로 중복된 항목이 발견되었습니다: +\n +\n%s +\n +\n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? 계정 선택 기본 계정 사용 회전 @@ -584,7 +599,7 @@ 재설정 플러그인 다운로드를 필터링할 모드 선택 CloudStream 데이터 백업이 완료되었습니다. 드문 경우지만, 기기에 따라 앱 접속이 안 되는 오류가 발생할 수 있습니다. 만약 앱이 열리지 않는다면, 앱 데이터를 완전히 삭제(초기화)한 후 이 백업 파일로 복구해 주시기 바랍니다. 이용에 불편을 드려 대단히 죄송합니다. - 스마트폰이나 컴퓨터에서 %s 위의 코드를 입력하세요 + 스마트폰이나 컴퓨터에서 %s를 방문하여 위의 코드를 입력하세요 구독 중인 TV 쇼의 알림을 받고 다운로드를 끊김 없이 완료하려면, CloudStream의 백그라운드 실행 권한이 필요합니다. \'확인\'을 누른 후 나타나는 요청 창에서 \'허용\'을 선택해 주세요.\n\n참고로, 이 권한을 허용한다고 해서 배터리가 계속 소모되는 것은 아닙니다. 알림을 받거나 공식 확장 프로그램에서 영상을 다운로드할 때처럼 꼭 필요한 상황에서만 백그라운드 작업을 수행합니다. 여기서 소스의 순서를 변경할 수 있습니다. 비디오의 우선 순위가 높은 경우에는 소스 선택 화면에 더 높게 나타납니다. 소스 우선 순위와 품질 우선 순위의 합이 비디오 우선 순위입니다. \n \n참고 A: 3 \n품질 B: 7 \n총 비디오 우선 순위는 10입니다. \n \n참고: 합이 10 이상이면 해당 링크가 로드되면 플레이어는 자동으로 로드를 건너뜁니다! 시즌 %1$d 에피소드 %2$d 공개 예정 @@ -593,7 +608,8 @@ 추천목록 보기 플레이어에 속도 옵션을 추가합니다 %s 후 공개 예정 - %s \n남음 + %s +\n남음 잠재적 중복 발견 %s의 PIN 입력 즐겨찾기에서 제거 @@ -611,10 +627,18 @@ 로컬 비디오 열기 파일 삭제 삭제 (%1$d | %2$s) - 다음 항목을 영구적으로 삭제 하시겠습니까? \n \n%s - 다음 에피소드를 영구적으로 삭제 하시겠습니까? %1$s? \n \n%2$s - 또한 다음 시리즈의 모든 에피소드를 영구적으로 삭제합니다: \n \n%s - 다음 시리즈의 모든 에피소드를 영구적으로 삭제 하시겠습니까? \n \n%s + 다음 항목을 영구적으로 삭제 하시겠습니까?? +\n +\n%s + 다음 에피소드를 영구적으로 삭제 하시겠습니까? %1$s? +\n +\n%2$s + 또한 다음 시리즈의 모든 에피소드를 영구적으로 삭제합니다: +\n +\n%s + 다음 시리즈의 모든 에피소드를 영구적으로 삭제 하시겠습니까?? +\n +\n%s 공개일 (최신순) 공개일 (오래된순) 플레이어 내 버튼명 숨기기 @@ -664,7 +688,7 @@ 플러그인 업데이트하기 수동으로 플러그인 업데이트 하기 플러그인 업데이트 절차를 시작합니다! - 플러그인 %d개를 성공적으로 업데이트했습니다 + 성공적으로 %d 플러그인을 업데이트 하였습니다! 업데이트 된 플러그인이 없습니다. 플레이어 알림 백그라운드에서 재생을 제어하기 위한 플레이어 알림 @@ -715,7 +739,7 @@ 입력하는 동안 검색어 제안 표시 출연진 정보 표시 - 자막 배경 테두리 곡률 + 배경 테두리 곡률 공급자 새로고침 검색어 제안 제안 삭제 @@ -730,36 +754,4 @@ 새로고침 최대 밝기 확장 활성화 플레이어에 메타데이터 오버레이 표시 - 비디오 - 프리뷰 - 라이브 - 프로필 설정 - 오류가 있는 소스 숨기기 - 우선순위가 음수인 소스 숨기기 - - 숨겨진 링크 %d개 - - 실시간 시계 표시 - 화면 상단에 실시간 시계를 표시합니다. 홈페이지와 플레이어에 적용됩니다 - Logcat 🐈 - 동영상을 정지한 채 시작 - 소스를 선택할 때의 부하 줄이기 - 배경 - 윤곽선 - 다이내믹 레인지 컴프레서 - 컴프레서 - 플레이어에 컴프레서 제어 표시 (시끄러운/조용한 오디오 균형 조정) - 켬 · %1$d dB / +%2$d dB - 끔 - 약하게 - 대화 증폭 - 강하게 - 임곗값: %d dB - 메이크업 게인: +%d dB - %d dB보다 큰 소리를 줄입니다 - 전체 음량을 %d dB 키웁니다 - 켬 - URL - 이미지 선택기 - 기본 이미지 diff --git a/app/src/main/res/values-b+lt/strings.xml b/app/src/main/res/values-b+lt/strings.xml index ff92ac093..cb2d816f3 100644 --- a/app/src/main/res/values-b+lt/strings.xml +++ b/app/src/main/res/values-b+lt/strings.xml @@ -25,18 +25,19 @@ Kopijuoti Duoti bananai Daugiau informacijos - Atsisiuntimai + Atsiuntimai Automatiškai pasirinkti kalbą Klaida kraunant nuorodas +30 - Atsisiuntimas baigtas + Atsiuntimas baigtas Tęsti žiūrėjimą - Rastas atnaujinimas!\n%1$s -> %2$s + Rastas atnaujinimas! +\n%1$s -> %2$s Atsisiųsti kalbas Ieškoti naudojant tiekėjus Grįžti atgal Siunčiama - Daugiau parinkčių + Daugiau pasirinkčiu Paleisti seriją Grotuvo greitis %d Bananai duoti kūrėjams @@ -45,7 +46,7 @@ Žanrai -30 Serijos plakatas - Gali reikėti VPN šiam tiekėjui, kad veiktų teisingai + Gali reikėti VPN šitam tiekėjui, kad veiktų teisingai Ieškoti %s… Github Nėra duotu bananų @@ -66,7 +67,7 @@ Atšaukti Pradėti Filmukas - Atsisiuntimas atšauktas + Atsiuntimas atšauktas Išplėstinė paieška Tuščias sąrašas. Pabandykite pasirinkti kitą sąrašą. Chromecast subtitrai @@ -87,16 +88,17 @@ Pratęsti siuntimą Azijietiškos dramos Serija - Jūsų biblioteka tuščia :( \nPrisijunkite prie bibliotekos paskyros arba pridėkite laidas prie vietinės bibliotekos. + Jūsų biblioteka tuščia :( +\nPrisijunkite prie bibliotekos paskyros arba pridėkite laidas prie vietinės bibliotekos. Pradėti sekančia seriją, kai dabartinė baigsis Teksto spalva Užbaigta Naudoti sistemos ryškumą programos grotuve vietoj tamsumo Nepavyko atstatyti duomenis iš failo %s - Paleisti anonsą - Paleisti gyvą transliaciją + Paleisti anonsa + Paleisti gyva transliacija Nerasta serijų - Šis tiekėjas yra iš Torrent\'ų, rekomenduojamas VPN + Šis tiekėjas yra iš Torrentų, VPN rekomenduojama naudoti Nepavyko Plakatas Paleisti failą @@ -108,21 +110,21 @@ Perdaryti nustatymo procesą %1$d-%2$d Duoti bananą kūrėjams - Sugrįšti + Sugryšti Nuoroda nukopijuota į iškarpinę Paieška Informacija - Praleisti įkėlimą + Praleisti įkėlima Informacija Serija %d bus išleista Išsaugoti Perdaug teksto. Nepavyko išsaugoti i iškarpynę. - Atsisiuntimas nepavyko + Atsiuntimas nepavyko Pasidalinti Pagrindinis Plakatas Šaltiniai Nustatymai - Paieška + Ieškoti Kraunama… Pašalinti Daugiau informacijos @@ -138,7 +140,7 @@ Uždaryti Nustatyti žiūrėjimo statusą Paleisti su CloudStream - Subtitrų lygis + Subtitrų iškėlimas Serijos Skleisti: %s Sezonas @@ -179,7 +181,11 @@ 127.0.0.1 Atsiųsta %1$d %2$s Praleisti %s - Pagal numatytuosius nustatymus „CloudStream“ neturi įdiegtų svetainių. Turite įdiegti svetaines iš saugyklų. \n \nDėl beprotiško DMCA reikalavimų, kurios atliko Sky UK Limited 🤮, negalime susieti saugyklos svetainės programoje. \n \nPrisijunkite prie mūsų Discord arba ieškokite internete. + Pagal numatytuosius nustatymus „CloudStream“ neturi įdiegtų svetainių. Turite įdiegti svetaines iš saugyklų. +\n +\nDėl beprotiško DMCA reikalavimų, kurios atliko Sky UK Limited 🤮, negalime susieti saugyklos svetainės programoje. +\n +\nPrisijunkite prie mūsų Discord arba ieškokite internete. Mobilūs duomenys šaunusPrisijungimoVardas Autoriai @@ -248,51 +254,4 @@ Ar tikrai norite išeiti? Pašalinti iš žiūrimų Garso takelis - Užpildymas - %d min - Namai - Atsisiuntimų eilė - Balso atpažinimas nepasiekiamas - Pradėkite kalbėti… - Nežiūrimas - Paleisti visą seriją - Šis įrašas yra Torrent\'e, tai reiškia, kad tavo įrašo veikla gali būti sekama. Įsitikinkite, kad suprantate Torrenting prieš tęsiant. - Atkurti ryšį… - Pasirinkite elementus, kuriuos norite pašalinti - Šiuo metu atsisiuntimų nėra. - Šiuo metu atsisiuntimų eilėje nėra. - Pasiekiama žiūrėti neprisijungus - Pasirinkti viską - Panaikinti visus pasirinkimus - Tinklo srautas - Atidaryti vietinį video - Nuorodos perkrautos - Subtitrai - Repozitorijos pavadinimas ir adresas - nukopijuota! - Naujos serijos pranešimas - Ieškoti kituose plėtiniuose - Rodyti rekomendacijas - Apvado spalva - Rėmelio tipas - Ieškoti naudojant tipus - Laikyti ilgai, kad sugražinti pradinius nustatymus - Įkelti šriftus sudedant juos į %s - Metaduomenys nesuteikiamos svetainės, vaizdo įkėlimas nepavyks, jeigu jo nebus svetainėje. - Aprašymas - Nėra siužeto aprašymo - Aprašymas nerastas - Rodyti Logcat - Vaizdas vaizde - Tęsia atkūrimą mažame grotuve virš kitų programų - Grotuvo dydžio pakeitimo mygtukas - Pašalinti juodas paraštes - Subtitrai - Chromecast subtitrų nustatymai - Atkūrimo sparta - Prideda greičio pasirinkimą grotuve - Braukite, kas ieškotumėte - Braukite į šonus, kad kontroliuotumėte padėtį vaizdo įraše - Braukite, kad pakeistumėte nustatymus - Braukite aukštyn arba žemyn ekrano kairėje arba dešinėje, jei norite pakeisti ryškumą arba garsą diff --git a/app/src/main/res/values-b+lv/strings.xml b/app/src/main/res/values-b+lv/strings.xml index a8ecaa68a..7d74354ea 100644 --- a/app/src/main/res/values-b+lv/strings.xml +++ b/app/src/main/res/values-b+lv/strings.xml @@ -8,11 +8,12 @@ Galvenais plakāts Nākamais nejaušais Iet atpakaļ - Nomainīt pakalpojumu sniedzēju + Nomainīt dvēju Apskatīt background Ātrums (%.2fx) Lidzīgi: %.1f - Jauns atjauninājums atrasts! \n%1$s -> %2$s + Jauns atjauninājums atrasts! +\n%1$s -> %2$s %d galvenais CloudStream Atskaņo ar cloudstream @@ -24,7 +25,7 @@ Nākamā epizode Pārlūks Izlaist ladešanos - Ielādē… + Lādējas… Skaties Aizturēts Pabeigts @@ -42,7 +43,7 @@ Palaist epizodi Ieladēt Lejupielāde iepauzēta - Lejupielāde sākta + Lādēšana sakās Lejupielāde neizdevās Lejupielāde atcelta Lejupielāde pabeigta @@ -50,7 +51,7 @@ Tīkla plūsma Kļūda, ielādējot saites Iekšējā atmiņa - Dublēts + Dub Dzēst datni Atskaņot datni Atsākt ielādi @@ -74,8 +75,8 @@ Stūra tips Fonts Fonta lielums - Meklēt pēc pakalpojumu sniedzējiem - Meklēt pēc tipiem + Meklēt izmantojot devējus + Meklēt izmantojot tipus %d Banāni iedoti veidotājiem %d. epizode būs pieejama Filtrs @@ -105,9 +106,9 @@ Noņemt Vairāk informācijas @string/home_play - Lai šis pakalpojumu sniedzējs darbotos pareizi, var būt nepieciešams VPN - Šis pakalpojumu sniedzējs piedāvā torrentus, tāpēc ļoti ieteicams izmantot VPN - Vietne nenodrošina metadatus. Ja tie nebūs pieejami vietnē, video lejupielāde neizdosies. + VPNs varētu būt vajadzīgs lai šis devējs strādātu pareizi + Šis devējs ir Torrents vpn ir rekomendēts + Dati nav doti no saites, video lādēšanas neizdosies ja neiksestē saitē. Apraksts Nav apraksts atrasts Apraksts nav atrasts @@ -150,7 +151,7 @@ Atjauninājumi un dublēšana Informācija Advancēta meklēšana - Sniedz meklēšanas rezultātus, kas sagrupēti pēc pakalpojumu sniedzēja + Dod tev meklēšanas rezultātus citus no devēja Rādīt anime papilda epizodi Rādīt reklāmkadrus Rādīt Kitsu vietnes plakātus @@ -168,7 +169,7 @@ Iedot banānu izstrādātājiem Iedotie banāni Lietotnes valoda - Šis pakalpojumu sniedzējs neatbalsta Chromecast + Šim devējam nav Chromecast pieņemšana Nav linku strastu Links kopēts cliobordā Restartēt uz parasto value @@ -192,8 +193,10 @@ Atsākt -30 +30 - Šis pilnibā dzesīs %s \nEsat parliecināts? - %dmin.\natlikusi(-šas) + Šis pilnibā dzesīs %s +\nEsat parliecināts? + %dm +\natlikušas Pabeigts Statuss Gads @@ -287,16 +290,16 @@ Parasts Izskats Funkcijas - Vispārīgie + Ģenerāls Randomā poga Rādīt izlases pogu Sākums un Bibliotēka sadaļās Papildinājuma valodas Lietotnes izkārtojums - Vēlamie multimediji + Izvēlētā media Iespējot nepiedienīgu, izaicinošu saturu (NSFW) atbalstītajos papildinājumos Subtitru kodējums - Pakalpojumu sniedzēji - Pakalpojumu sniedzēja tests + Devēji + Devēju tests Izskats Automātiski Televizora izskats @@ -372,17 +375,17 @@ Kļūda Noņemt slēgtos parakstus no subtitriem Noņemt lieko no subtitriem (piemēram, reklāmu) - Atlasīt pēc vēlamās multimediju valodas + Filtrēt pēc vēlamās multivides valodas Ekstras Treileris https://piemērs.com/piemērs.mp4 Referents Nākamais - Skatīties video šajās valodās + Skatieties videoklipus šajās valodās Iepriekšējais Izlaist uzstādīšanu Mainiet lietotnes izskatu, lai tā atbilstu savai ierīcei - Ko jūs vēlētos skatīt + Ko tu vēlies redzēt Pabeigts Papildinājumi Pievienot repozitoriju @@ -455,7 +458,8 @@ Alfabētiskā secībā (Z līdz A) Atlasiet Bibliotēka Atvērt ar - Šķiet, ka jūsu bibliotēka ir tukša :( \nPiesakieties bibliotēkas kontā vai pievienojiet pārraides savai vietējai bibliotēkai. + Šķiet, ka jūsu bibliotēka ir tukša :( +\nPiesakieties bibliotēkas kontā vai pievienojiet pārraides savai vietējai bibliotēkai. Atgriest Anulēts %s abonements %d sērija izlaista! @@ -570,7 +574,7 @@ Atjaunināt papildinājumus Atjaunināt papildinājumus manuāli Sāk papildinājuma atjaunināšanas procesu! - Veiksmīgi atjaunināts(-i) %d papildinājums(-i) + Veiksmīgi atjaunināts(-i) %d papildinājums(-i)! Neviens papildinājums netika atjaunināts. Meklēt citos papildinājumos Pārbaudīt visus papildinājumus @@ -607,15 +611,4 @@ Vai tiešām vēlaties neatgriezeniski dzēst visas šī seriāla, raidījuma epizodes?\n\n%s Pašlaik nav nevienas rindā ievietotas lejupielādes. Atvērt vietējo video - Multimedija - Multimediju informācija - Iespējojiet torrentu Iestatījumi/Pakalpojumu sniedzēji/Vēlamie multimediji sadaļā - Paziņojums par jaunu epizodi - Papildu spilgtums - Papildu spilgtums iespējots - Meklēšanas ieteikumi - Pārlādēt pakalpojumu sniedzēju - Tiešraide - Video - Saites atkārtoti ielādētas diff --git a/app/src/main/res/values-b+mk/strings.xml b/app/src/main/res/values-b+mk/strings.xml index 60cf274bf..99ee9c2ac 100644 --- a/app/src/main/res/values-b+mk/strings.xml +++ b/app/src/main/res/values-b+mk/strings.xml @@ -126,7 +126,8 @@ Откажи Паузирај Продолжи - Ова трајно ќе го избрише %s \nДали си сигурен? + Ова трајно ќе го избрише %s +\nДали си сигурен? Во тек Изгледанo Статус @@ -244,7 +245,7 @@ TC Претплатен на %s Преводи - Предупредување: CloudStream не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив! + Предупредување: CloudStream 3 не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив! Недостасуваат дозволи за складирање. Обиди се повторно. Зачувај Вчитај од датотека @@ -310,7 +311,7 @@ Видео резолуција Азиски драми Приклучокот е вчитан - Отстрани реклами од преводите + Отстрани непотребен текст од преводите Не е преземено: %d Remove closed captions from subtitles Аудио јазик @@ -416,7 +417,8 @@ Почна да презема %1$d %2$s… Автоматски ажурирања на приклучоци -30 - %dm \nпреостанува + %dm +\nпреостанува Видео кеш на дискот https://example.com/example.mp4 Готово @@ -429,8 +431,8 @@ Изглед Додај тракинг Оценет - CamRip - CamHD + Камера + Камера SDR Апликацијата не е пронајдена Корисничко име @@ -445,7 +447,7 @@ Грешка при правење резервна копија на %s Сокриј го избраниот квалитет на видеото во резултатите од пребарувањето Некои уреди не го поддржуваат новиот инсталатор на пакети. Испробај ја легаси(старата) опција, ако ажурирањата не се инсталираат. - Прикажи информации за плеерот + Резолуција на видео плеер Големина на видео баферот Распоред Стандардно @@ -585,7 +587,8 @@ По неколку неуспешни обиди, известувањето ќе се затвори. Едноставно вклучи ја апликацијата повторно за да се обидеш повторно. Медиуми Претстои во %s - %s \nпреостанати + %s +\nпреостанати За да се обезбедат непрекинати преземања и известувања за претплатените ТВ серии, CloudStream треба дозвола за работа во позадина. Со притискање на „ОК“, ќе ви биде прикажан дијалог за барање дозвола. Ве молиме, притиснете „Дозволи“.\n\nИмајте предвид дека оваа дозвола не значи дека CS3 ќе ја троши вашата батерија. Ќе работи во позадина само кога е потребно, како на пример при примање известувања или преземање видеа од официјални екстензии. Грешка при пристапот до таблата со исечоци, обиди се повторно. Грешка при копирање, молам копирај го логот и контактирај ја поддршката на апликацијата. @@ -642,7 +645,7 @@ Започнување на процесот на ажурирање на приклучоци! Ажурирај приклучоци Ниту еден приклучок не е ажуриран. - Успешно ажурирани %d приклучоци + Успешно ажурирани %d приклучоци! Епизода (Растечки) Епизода (Опаѓачки) Рејтинг (Највисок) @@ -705,64 +708,4 @@ Горе во центар Горе на десно Пушти ја целата серија - Редица за преземање - Моментално нема преземања во редицата. - Дополнителна осветленост - Овозможи филтер за осветленост кога ќе се надмине 100% осветленост на екранот - овозможена_дополнителна_осветленост - Предлози за пребарување - Прикажувај предлози за пребарување додека пишуваш - Исчисти предлози - Прикажи преклоп со метаподатоци на плеерот - Прикажи панел за емитување - Инсталирај предиздавачка верзија - Предиздавачката верзија е веќе инсталирана. - Неуспешна инсталација на предиздавачката верзија. - Видео - Текст на епизода - Информации за медиумот - Преглед - Приоритет на извор - Одреди како ќе се подредуваат видео изворите во плеерот - Име на изворот - Преземи сѐ - Откажи сѐ - Дали сакате да ја преземете епизодата %s? - Дали сакате да ги откажете сите преземања во редицата? - - %d активно преземање - %d активни преземања - - - %d преземање во редицата - %d преземања во редицата - - Во живо - Започни паузирани видеа - Поставки на профил - Сокриј извори со негативен приоритет - Сокриј извори со грешки - Позадина - Edge - Logcat 🐈 - Намалете го оптоварувањето при избирање извори - Прикажи часовник во реално време - Прикажи часовник во реално време на врвот на екранот. Се применува на почетната страница и плеерот - - %d скриена врска - %d скриени врски - - Компресор - Компресор со динамички опсег - Прикажи ги контролите на компресорот во плеерот (помага во балансирање на гласен/тивок звук) - Вклучено - Вклучено · %1$d dB / +%2$d dB - Исклучено - Лесно - Засилување на дијалог - Тешко - Праг: %d dB - Компензациско засилување: +%d dB - Звуци погласни од %d dB се стишуваат - Ја зголемува вкупната јачина на звукот за +%d dB diff --git a/app/src/main/res/values-b+ml/strings.xml b/app/src/main/res/values-b+ml/strings.xml index 6c5dd3d89..d1c9409a3 100644 --- a/app/src/main/res/values-b+ml/strings.xml +++ b/app/src/main/res/values-b+ml/strings.xml @@ -3,7 +3,8 @@ വേഗം (%.2fx) റേറ്റിംഗ്: %.1f - പുതിയ അപ്ഡേറ്റ്! \n%1$s -> %2$s + പുതിയ അപ്ഡേറ്റ്! +\n%1$s -> %2$s ക്ലൗഡ് സ്ട്രീം ഹോം തിരയുക @@ -83,9 +84,9 @@ പ്ലേയർ സബ്‌ടൈറ്റിലുകളുടെ സെറ്റിങ്‌സ് - വീഡിയോയിൽ ഇരുവശങ്ങളിലേക്കും സ്വൈപ്പ് ചെയ്താൽ സ്ഥാനത്തെ നിയന്ത്രിക്കാം + വീഡിയോപ്ലേയറിൽ സമയം നിയന്ത്രിക്കാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ സ്വൈപ്പുചെയ്യുക - തെളിച്ചം അല്ലെങ്കിൽ വോളിയം മാറ്റാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ നീക്കുക + തെളിച്ചം അല്ലെങ്കിൽ വോളിയം മാറ്റാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ സ്വൈപ്പുചെയ്യുക മുന്നോട്ട് അല്ലെങ്കിൽ പിന്നിലേക്ക് നീങ്ങാൻ വലത്തോട്ടോ ഇടത്തോട്ടോ രണ്ടുതവണ ടാപ്പുചെയ്യുക തിരയുക @@ -114,7 +115,8 @@ റദ്ദാക്കുക നിർത്തുക തുടരുക - സ്ഥിരമായി %sനെ ഡിലീറ്റ് ചെയ്യുക \nഉറപ്പാണോ? + സ്ഥിരമായി %sനെ ഡിലീറ്റ് ചെയ്യുക +\nഉറപ്പാണോ? തുടരുന്നു പൂർത്തിയായി അവസ്ഥ @@ -186,11 +188,13 @@ ലോഡിംഗ്… ബ്രൗസർ വീണ്ടും കാണുക - നെറ്റ്‌വർക്ക് സ്ട്രീം + സ്ട്രീം %s ൽ ഫോൻ്റ്‌സ് വെച്ചു കൊണ്ട് ഇംപോർട്ട് ചെയ്യുക പ്രശ്‌നമുണ്ടാക്കുന്ന ഒന്ന് കണ്ടെത്താൻ നിങ്ങളെ സഹായിക്കുന്നതിന് ഒരു ക്രാഷ് കാരണം എല്ലാ വിപുലീകരണങ്ങളും ഓഫാക്കി. പൊതു പട്ടിക - CloudStream-ന് സ്ഥിരസ്ഥിതിയായി സൈറ്റുകളൊന്നും ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. നിങ്ങൾ റിപ്പോസിറ്ററികളിൽ നിന്ന് സൈറ്റുകൾ ഇൻസ്റ്റാൾ ചെയ്യേണ്ടതുണ്ട്. \n \nഞങ്ങളുടെ ഡിസ്കോർഡിൽ ചേരുക അല്ലെങ്കിൽ ഓൺലൈനിൽ തിരയുക. + CloudStream-ന് സ്ഥിരസ്ഥിതിയായി സൈറ്റുകളൊന്നും ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. നിങ്ങൾ റിപ്പോസിറ്ററികളിൽ നിന്ന് സൈറ്റുകൾ ഇൻസ്റ്റാൾ ചെയ്യേണ്ടതുണ്ട്. +\n +\nഞങ്ങളുടെ ഡിസ്കോർഡിൽ ചേരുക അല്ലെങ്കിൽ ഓൺലൈനിൽ തിരയുക. പകർത്തുക എല്ലാ സബ്‌ടൈറ്റിലുകളും വലിയക്ഷരമാക്കുക റെൻഡറർ പിശക് @@ -257,7 +261,8 @@ ഈ ലിസ്റ്റ് ശൂന്യമാണ്. മറ്റൊന്നിലേക്ക് മാറാൻ ശ്രമിക്കുക. ചരിത്രം മായ്ക്കുക ലോഗ്കാറ്റ് കാണിക്കുക 🐈 - നിങ്ങളുടെ ലൈബ്രറി ശൂന്യമാണ് :( \nഒരു ലൈബ്രറി അക്കൗണ്ടിൽ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ നിങ്ങളുടെ പ്രാദേശിക ലൈബ്രറിയിലേക്ക് ഷോകൾ ചേർക്കുക. + നിങ്ങളുടെ ലൈബ്രറി ശൂന്യമാണ് :( +\nഒരു ലൈബ്രറി അക്കൗണ്ടിൽ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ നിങ്ങളുടെ പ്രാദേശിക ലൈബ്രറിയിലേക്ക് ഷോകൾ ചേർക്കുക. വീഡിയോ റിപ്പോസിറ്ററി നാമവും URL ഉം പകർത്തി! @@ -267,50 +272,4 @@ എഡ്ജ് തരം ഔട്ട്ലൈൻ നിറം പശ്ചാത്തല നിറം - സീസൺ %1$d എപ്പിസോഡ് %2$d റിലീസ് ചെയ്യുന്ന സമയം - %1$dh %2$dm %3$ds - %1$dm %2$ds - %1$ds - തുടക്കം മുതൽ പ്ലേ ചെയ്യുക - ഡൗൺലോഡ് ക്യൂ - സ്പീച്ച് റെക്കഗ്നിഷൻ ലഭ്യമല്ല - സംസാരിക്കാൻ തുടങ്ങുക… - മുഴുനീള പരമ്പരയും പ്ലേ ചെയ്യുക - ഈ വീഡിയോ ഒരു ടോറന്റ് ആണ്, അതായത് നിങ്ങളുടെ വീഡിയോ ആക്റ്റിവിറ്റി ട്രാക്ക് ചെയ്യപ്പെടാൻ സാധ്യതയുണ്ട്.\nതുടരുന്നതിന് മുൻപ് ടോറന്റിംഗിനെക്കുറിച്ച് നിങ്ങൾക്ക് കൃത്യമായി അറിയാമെന്ന് ഉറപ്പാക്കുക. - ഡിലീറ്റ് ചെയ്യേണ്ടവ തിരഞ്ഞെടുക്കുക - നിലവിൽ ഡൗൺലോഡുകൾ ഒന്നും തന്നെയില്ല. - നിലവിൽ ക്യൂവിലുള്ള ഡൗൺലോഡുകൾ ഒന്നും തന്നെയില്ല. - ഓഫ്‌ലൈനായി കാണാൻ ലഭ്യമാണ് - എല്ലാം തിരഞ്ഞെടുക്കുക - തിരഞ്ഞെടുത്തവയെല്ലാം ഒഴിവാക്കു - ലോക്കൽ വീഡിയോ തുറക്കുക - സേവ് ചെയ്യുക - സബ്‌ടൈറ്റിൽ ഉയരം - ഫോണ്ട് - ഫോണ്ട് വലുപ്പം - സ്വയം തിരഞ്ഞെടുത്ത ഭാഷ - ഭാഷകൾ ഡൗൺലോഡ് ചെയ്യുക - സബ്ടൈറ്റിൽ ഭാഷ - സൈറ്റിൽ വിവരങ്ങൾ ലഭ്യമല്ല, ആയതിനാൽ വീഡിയോ ലോഡ് ആവുകയില്ല. - പ്ലേയറിന്റെ വലുപ്പം മാറ്റുന്ന ബട്ടൻ - സബ്ടൈട്ടിൽസ് - ക്രോംകാസ്റ്റ് സബ്ടൈട്ടിൽസ് - ക്രോംകാസ്റ്റ് സബ്ടൈട്ടിൽസ് ക്രമീകരണങ്ങൾ - പ്ലേ ചെയ്യുന്ന വേഗത - പ്ലേയറിൽ വേഗതയ്ക്കുള്ള ഓപ്ഷൻ ചേർക്കുന്നു - മുന്നോട്ടും പിന്നോട്ടും പോകുവാൻ സ്വൈപ്പ് ചെയ്യുക - ക്രമീകരണങ്ങൾ മാറ്റുവാൻ സ്വൈപ്പ് ചെയ്യുക - നാമം - ഉറവിട നാമം - ദൃശ്യമേന്മയും നാമവും - എല്ലാം ഡൗൺലോഡ് ചെയ്യുക - അടുത്ത ഭാഗം സ്വയം പ്ലേ ചെയ്യുക - ഇപ്പോളത്തെ ഭാഗം കഴിഞ്ഞയുടനെ അടുത്തത് ആരംഭിക്കുക - തൽസമയം - @string/home_play - രണ്ടുവട്ടം ടാപ്പ് ചെയ്താൽ സ്ഥാനം നിയന്ത്രിക്കാം - രണ്ടുവട്ടം ടാപ്പ് ചെയ്താൽ താൽകാലികമായി നിർത്തിവെക്കാം - പ്ലേയറിലെ സ്ഥാനനിയന്ത്രണ അളവ് (സെക്കണ്ടസ്) - നടുവിൽ രണ്ട് തവണ ടാപ്പ് ചെയ്താൽ താൽകാലികമായി നിർത്തിവെക്കാം - സിസ്റ്റത്തിന്റെ വെളിച്ചക്രമീകരണം ഉപയോഗിക്കുക diff --git a/app/src/main/res/values-b+ms/strings.xml b/app/src/main/res/values-b+ms/strings.xml index 8843b252e..dc1cd3ee6 100644 --- a/app/src/main/res/values-b+ms/strings.xml +++ b/app/src/main/res/values-b+ms/strings.xml @@ -25,8 +25,8 @@ Episod %d akan disiarkan dalam Pelakon:%s Mod Selamat Hidup - %1$dh %2$dj %3$dm - %1$dj %2$dm + %1$dd %2$dh %3$dm + %1$dh %2$dm %dm Poster Episod Poster Utama @@ -56,7 +56,8 @@ Tutup Pratonton Resensi:%.1f - Kemas kini baru dijumpai! \n%1$s -> %2$s + Kemas kini baru dijumpai! +\n%1$s -> %2$s %d min Main dari mula Musim %1$d Episod %2$d akan dikeluarkan di @@ -69,7 +70,7 @@ Buka video tempatan Tetapan Sari Kata Pemberitahuan episod baru - Cari dalam sambungan lain + Carian dalam sambungan lain Paparkan cadangan Penanda halaman Pasang @@ -105,7 +106,7 @@ Carian mengikut sumber %d Pisang yang diberikan kepada pemaju Saiz Teks - Simpan + Mohon Warna teks Warna latar belakang Tiada Benenes diberikan @@ -116,7 +117,7 @@ Buang Lebih Maklumat Sumber ini adalah torrent, penggunaan VPN digalakkan - Metadata tidak diberikan oleh laman web, video lalai akan gagal sekiranya tidak wujud di laman web. + Metadata tidak diberikan oleh laman web, video lalai akan gagala sekiranya tidak wujud di laman web. Penerangan Papar Logcat🐈 Saiz butang pemain @@ -485,7 +486,7 @@ Kemaskini dan sandaran Ketik dua kali untuk mencari Gunakan kecerahan sistem dalam pemain apl dan bukannya tindanan gelap - %1$dj %2$dm %3$ds + %1$dh %2$dm %3$ds %1$dm %2$ds %1$ds Pengecaman pertuturan tidak tersedia @@ -533,10 +534,4 @@ Siaran Langsung Audio Podcast - Giliran muat turun - Main Siri Penuh - Pada masa ini tiada muat turun digilirkan. - Logcat 🐈 - Mulakan video dijeda - Kurangkan tekanan apabila memilih simber diff --git a/app/src/main/res/values-b+mt/strings.xml b/app/src/main/res/values-b+mt/strings.xml index 8f8bf6cd7..ea859ee29 100644 --- a/app/src/main/res/values-b+mt/strings.xml +++ b/app/src/main/res/values-b+mt/strings.xml @@ -18,7 +18,8 @@ Ibdel Il-fornitur veloċità (%.2fx) Klassifikazzjoni: %.1f - Aġġornament ġdid misjub! \n%1$s -> %2$s + Aġġornament ġdid misjub! +\n%1$s -> %2$s %d min CloudStream Ara bil-CloudStream diff --git a/app/src/main/res/values-b+my/strings.xml b/app/src/main/res/values-b+my/strings.xml index d360d095d..0938e4f98 100644 --- a/app/src/main/res/values-b+my/strings.xml +++ b/app/src/main/res/values-b+my/strings.xml @@ -11,7 +11,8 @@ နောက်သို့ နောက်ခံပုံရိပ်ကို အကြိုကြည့်ရန် အဆင့်: %.1f - အပ်ဒိတ်အသစ်! \n%1$s -> %2$s + အပ်ဒိတ်အသစ်! +\n%1$s -> %2$s စစ်ထုတ်မှု %d မိနစ် CloudStream @@ -110,8 +111,10 @@ အပိုင်း အပိုင်းများ %1$d-%2$d - ဒါကအပြီးဖျက်ခြင်းဖြစ်ပါသည် %s \nသင်သေချာပါသလား။ - %dမိနစ် \nကျန်ရိှသည် + ဒါကအပြီးဖျက်ခြင်းဖြစ်ပါသည် %s +\nသင်သေချာပါသလား။ + %dမိနစ် +\nကျန်ရိှသည် ထုတ်လွှင့်နေဆဲ ထုတ်လွှင့်မှုပြီးဆုံး အခြေအနေ @@ -339,7 +342,11 @@ သမားရိုးကျ ထည့်သွင်းသူ ထွက်ချိန်တွင် အက်ပ်ကို အပ်ဒိတ်လုပ်ပါမည် - CloudStream တွင် မူရင်းအတိုင်း ထည့်သွင်းထားသည့်ဆိုက်များ မရှိပါ။ ရီပိုစစ်ထရီများ မှ ဆိုဒ် များကို ထည့်သွင်းရန်လိုအပ်သည်။ \n \nSky UK Limited မှ ဦးနှောက်မဲ့ DMCA ကို ဖယ်ရှားလိုက်ခြင်းကြောင့် 🤮 ကျွန်ုပ်တို့သည် ရီပိုစစ်ထရီဆိုဒ်ကို အက်ပ်တွင် ချိတ်ဆက်၍မရပါ။ \n \nကျွန်ုပ်တို့၏ Discord တွင်ပါဝင်ပါ သို့မဟုတ် အွန်လိုင်းတွင်ရှာဖွေပါ။ + CloudStream တွင် မူရင်းအတိုင်း ထည့်သွင်းထားသည့်ဆိုက်များ မရှိပါ။ ရီပိုစစ်ထရီများ မှ ဆိုဒ် များကို ထည့်သွင်းရန်လိုအပ်သည်။ +\n +\nSky UK Limited မှ ဦးနှောက်မဲ့ DMCA ကို ဖယ်ရှားလိုက်ခြင်းကြောင့် 🤮 ကျွန်ုပ်တို့သည် ရီပိုစစ်ထရီဆိုဒ်ကို အက်ပ်တွင် ချိတ်ဆက်၍မရပါ။ +\n +\nကျွန်ုပ်တို့၏ Discord တွင်ပါဝင်ပါ သို့မဟုတ် အွန်လိုင်းတွင်ရှာဖွေပါ။ အခြားသူများ၏ရီပိုစစ်ထရီများကိုရှာဖွေမည် အသံများ အသံဖိုင်များ @@ -415,7 +422,8 @@ အက်ပ်အပ်ဒိတ်အားဒေါင်းလုဒ်လုပ်နေသည်… အက်ပ်အပ်ဒိတ်အားသွင်းနေသည်… အပ်ဒိတ်ဖြစ်မှု (အသစ် မှ အဟောင်း) - သင့်လိုက်ဘရီသည် ဗလာဖြစ်နေသည် :( \nအကောင့်ဝင်ပါ သို့မဟုတ် သင့်ဖုန်းလိုက်ဘရီတွင် ကြည့်စရာများထည့်ပါ။ + သင့်လိုက်ဘရီသည် ဗလာဖြစ်နေသည် :( +\nအကောင့်ဝင်ပါ သို့မဟုတ် သင့်ဖုန်းလိုက်ဘရီတွင် ကြည့်စရာများထည့်ပါ။ သုံးရန် တည်းဖြတ်ရန် အရည်အသွေးများ @@ -508,7 +516,8 @@ စာရင်းသွင်းပြီး %s စာရင်းသွင်းမှုပယ်ဖျက်ပြီး %s ဤစာရင်းသည် ဗလာဖြစ်နေသည်။ အခြားတစ်ခုသို့ ပြောင်းကြည့်ပါ။ - Safe mode ဖိုင်ကို တွေ့ရှိခဲ့သည်။ \nဖိုင်ကိုမဖယ်ရှားမချင်း စတင်ဖွင့်စတွင် မည်သည့် extension များကိုမျှ မတင်ပါ။ + Safe mode ဖိုင်ကို တွေ့ရှိခဲ့သည်။ +\nဖိုင်ကိုမဖယ်ရှားမချင်း စတင်ဖွင့်စတွင် မည်သည့် extension များကိုမျှ မတင်ပါ။ အပိုင်းသစ် %d ထွက်ပြီ ပရိုဖိုင် %d ဝိုင်ဖိုင် @@ -516,7 +525,13 @@ ပုံသေထားရန် ပရိုဖိုင်များ အကူအညီ - ဤနေရာတွင် သင်သည် အရင်းအမြစ်များကို မည်ကဲ့သို့ အစီအစဥ်ချမည်ကို ပြောင်းလဲနိုင်သည်။ ဗီဒီယိုတစ်ခုတွင် ပိုမိုဦးစားပေးပါက ရင်းမြစ်ရွေးချယ်မှုတွင် ပိုမိုမြင့်မားလာမည်ဖြစ်သည်။ အရင်းအမြစ် ဦးစားပေးနှင့် အရည်အသွေး ဦးစားပေး၏ ပေါင်းစုသည် ဗီဒီယို ဦးစားပေးဖြစ်သည်။ \n \nအရင်းအမြစ် A: 3 \nအရည်အသွေး B: 7 \nပေါင်းစပ်ဗီဒီယို ဦးစားပေး 10 ခု ရှိပါမည်။ \n \nမှတ်ချက်- ပေါင်းလဒ်သည် 10 သို့မဟုတ် ထို့ထက်ပိုပါက ထိုလင့်ခ်ကို တင်သည့်အခါ ဗီဒီယိုဖွင့်စက်သည် အလိုအလျောက် ဒေါင်းလုဒ်ကို ကျော်သွားမည်ဖြစ်သည် + ဤနေရာတွင် သင်သည် အရင်းအမြစ်များကို မည်ကဲ့သို့ အစီအစဥ်ချမည်ကို ပြောင်းလဲနိုင်သည်။ ဗီဒီယိုတစ်ခုတွင် ပိုမိုဦးစားပေးပါက ရင်းမြစ်ရွေးချယ်မှုတွင် ပိုမိုမြင့်မားလာမည်ဖြစ်သည်။ အရင်းအမြစ် ဦးစားပေးနှင့် အရည်အသွေး ဦးစားပေး၏ ပေါင်းစုသည် ဗီဒီယို ဦးစားပေးဖြစ်သည်။ +\n +\nအရင်းအမြစ် A: 3 +\nအရည်အသွေး B: 7 +\nပေါင်းစပ်ဗီဒီယို ဦးစားပေး 10 ခု ရှိပါမည်။ +\n +\nမှတ်ချက်- ပေါင်းလဒ်သည် 10 သို့မဟုတ် ထို့ထက်ပိုပါက ထိုလင့်ခ်ကို တင်သည့်အခါ ဗီဒီယိုဖွင့်စက်သည် အလိုအလျောက် ဒေါင်းလုဒ်ကို ကျော်သွားမည်ဖြစ်သည် ပရိုဖိုင်နောက်ခံ UI ကို မှန်ကန်စွာ ဖန်တီး၍မရပါ၊ ၎င်းသည် အဓိက ချို့ယွင်းချက်တစ်ခုဖြစ်ပြီး ချက်ချင်းသတင်းပို့သင့်သည်။ %s သင်နဂိုတည်းကသတ်မှတ်ပြီး diff --git a/app/src/main/res/values-b+ne/strings.xml b/app/src/main/res/values-b+ne/strings.xml index 49e5a9350..8a432a505 100644 --- a/app/src/main/res/values-b+ne/strings.xml +++ b/app/src/main/res/values-b+ne/strings.xml @@ -15,7 +15,8 @@ मुख्य पोस्टर %1$s Ep %2$d अभिनेता:%s - नयाँ अपडेट भेटियो! \n%1$s -> %2$s + नयाँ अपडेट भेटियो! +\n%1$s -> %2$s फिलर %d मिनेट क्लाउडस्ट्रीम diff --git a/app/src/main/res/values-b+nl/strings.xml b/app/src/main/res/values-b+nl/strings.xml index feec84263..e3bbee121 100644 --- a/app/src/main/res/values-b+nl/strings.xml +++ b/app/src/main/res/values-b+nl/strings.xml @@ -19,7 +19,8 @@ Snelheid (%.2fx) Beoordeeld: %.1fAls - Nieuwe update gevonden! \n%1$s -> %2$s + Nieuwe update gevonden! +\n%1$s -> %2$s Filler %d min CloudStream @@ -181,8 +182,10 @@ Hervatten -30 +30 - Dit wordt zeker permanent verwijderd %s \nWeet u het zeker? - %dm \nremaining + Dit wordt zeker permanent verwijderd %s +\nWeet u het zeker? + %dm +\nremaining Voortdurende Voltooid Status @@ -481,7 +484,8 @@ Verwijderen uit bekeken App wordt bijgewerkt bij afsluiten Gesorteerd - Je bibliotheek is leeg :( \nLog in op een bibliotheekaccount of voeg voorstellingen toe aan uw lokale bibliotheek. + Je bibliotheek is leeg :( +\nLog in op een bibliotheekaccount of voeg voorstellingen toe aan uw lokale bibliotheek. Uitgeschakeld: %d Stop Niet gedownload: %d @@ -503,7 +507,8 @@ %s ( Uitgeschakeld) Herstart de app om veranderingen te zien. Gedownload: %d - Veilige mode bestand gevonden! \nGeen extensies laden bij het opstarten totdat het bestand is verwijderd. + Veilige mode bestand gevonden! +\nGeen extensies laden bij het opstarten totdat het bestand is verwijderd. Nee Beoordeling ( Hoog naar Laag) Veilige mode aan @@ -515,7 +520,11 @@ Wis repository Uitgeschreven bij %s Terugkeren - CloudStream heeft standaard geen sites geïnstalleerd. U moet de sites uit repositories installeren. \n \nVanwege een hersenloze DMCA verwijdering door Sky UK Limited 🤮 kunnen we de repository site niet linken in de app. \n \nWord lid van onze Discord of zoek online. + CloudStream heeft standaard geen sites geïnstalleerd. U moet de sites uit repositories installeren. +\n +\nVanwege een hersenloze DMCA verwijdering door Sky UK Limited 🤮 kunnen we de repository site niet linken in de app. +\n +\nWord lid van onze Discord of zoek online. Audiosporen Gesorteerd op Wifi @@ -527,7 +536,13 @@ Kwaliteiten Profiel achtergrond Gebruik - Hier kan je de volgorde van de bronnen veranderen. Als een video een hogere prioriteit heeft zal het hoger in de bronnenlijst staan. De som van de prioriteit van de bron en de prioriteit van de kwaliteit is de prioriteit van de video. \n \nBron A: 3 \nKwaliteit B: 7 \nHeeft een totale prioriteit van de video van 10. \n \nNOTITIE: Als de som 10 of hoger is zal de speler automatisch het laden overslaan wanneer die link is geladen! + Hier kan je de volgorde van de bronnen veranderen. Als een video een hogere prioriteit heeft zal het hoger in de bronnenlijst staan. De som van de prioriteit van de bron en de prioriteit van de kwaliteit is de prioriteit van de video. +\n +\nBron A: 3 +\nKwaliteit B: 7 +\nHeeft een totale prioriteit van de video van 10. +\n +\nNOTITIE: Als de som 10 of hoger is zal de speler automatisch het laden overslaan wanneer die link is geladen! Profiel %d Repository niet gevonden, controleer de URL en probeer een VPN Geen plug-ins gevonden in de repository @@ -539,7 +554,11 @@ Favorieten %s toegevoegd aan favorieten Aangemeld als %s - Er zijn mogelijk dubbele items gevonden in uw bibliotheek: \n \n%s \n \nWilt u dit item toch toevoegen, de bestaande vervangen of de actie annuleren? + Er zijn mogelijk dubbele items gevonden in uw bibliotheek: +\n +\n%s +\n +\nWilt u dit item toch toevoegen, de bestaande vervangen of de actie annuleren? Voer PIN in voor %s Backupfrequentie Mogelijk Duplicaat Gevonden @@ -558,7 +577,9 @@ Abonneer Verwijder uit favorieten Selecteer een Account - Het lijkt erop dat er al een mogelijk duplicaat bestaat in uw bibliotheek: \'%s.\' \n \nWilt u dit item toch toevoegen, het bestaande item vervangen of de actie annuleren? + Het lijkt erop dat er al een mogelijk duplicaat bestaat in uw bibliotheek: \'%s.\' +\n +\nWilt u dit item toch toevoegen, het bestaande item vervangen of de actie annuleren? PIN invoeren PIN Huidige PIN invoeren @@ -661,38 +682,4 @@ Fout bij toegang tot het Klembord, Probeer het opnieuw. Fout bij het kopiëren. Kopieer alsjeblieft de logcat en neem contact op met de app-ondersteuning. Afwijzen - Video - Voorbeeld - Bron Prioriteit - Bepaal hoe de videobronnen worden gesorteerd in de speler - Ontgrendel CloudStream - Versleutel met Biometrie - Reset - verschijningsdatum (Nieuw naar Oud) - verschijningsdatum (Oud naar Nieuw) - Verberg de namen van de besturingselementen van de speler - Ondertiteling nog niet geladen - Back-up folder locatie - Aangepast - Bevestig voor afsluiten - Toon dialoogvenster voordat de app wordt afgesloten - Randgrote - Laat Speler Metadata Overlay zien - Kon de apparaat PIN niet vinden, probeer locale authenticatie - Zet torrent aan in Instellingen/Providers/Media voorkeur - Herstart de app en accepteer de Stream Torrent pop-up om verder te gaan. - Handmatige Update Knop - Open de app met Vingerafdruk, Face ID, PIN, Pattern en Wachtwoord. - Seekbar preview - Zet preview thumbnail aan op seekbar - Software decodering - Software decodering staat de mogelijkheid toe om de speler videobestanden af te laten spelen op jouw apparaat, maar het kan een haperige of onstabiele kijkervaring zorgen op hoge resolutie. - Volume heeft de 100% overschreden - Update Plugins - Geen plugins zijn geupdate. - Speler meldingen - De speler melding om de kijkervaring van de achtergrond de besturen - Online - Hoeveel verschillende items kunnen in parallel gedownload worden - Parallel downloads diff --git a/app/src/main/res/values-b+nn/strings.xml b/app/src/main/res/values-b+nn/strings.xml index b29e142b0..245bf6618 100644 --- a/app/src/main/res/values-b+nn/strings.xml +++ b/app/src/main/res/values-b+nn/strings.xml @@ -15,7 +15,8 @@ Fyllstoff Forhandsvis bakgrunnsbilete Vurdert: %.1f - Ny oppdatering tilgjengeleg! \n%1$s -> %2$s + Ny oppdatering tilgjengeleg! +\n%1$s -> %2$s %d minutt Miniatyrbilete Episode %d vil bli sleppt om @@ -114,8 +115,10 @@ Gjenoppta -30 +30 - Dette vil slette %s permanent. \nEr du sikker på dette? - %dm \ngjenstår + Dette vil slette %s permanent. +\nEr du sikker på dette? + %dm +\ngjenstår Pågåande Fullført År @@ -188,14 +191,4 @@ Bilde i bilde Fortsett å sjå Prøv tilkopling på nytt… - Sesong %1$d Episode %2$d blir sleppt om - Spel av frå start - Nedlastingskø - Semmegjenkjenning er ikkje tilgjengeleg - Snakk no… - Nettlesar - Fjerna - Strøm Torrent - Spel heile serien - Denne filmen er ein Torrent, som betyr at bruken din kan bli spora\nSett deg inn i bruk av Torrent-resursar før du fortsetter. diff --git a/app/src/main/res/values-b+no/strings.xml b/app/src/main/res/values-b+no/strings.xml index 7d309a2f8..55b5303eb 100644 --- a/app/src/main/res/values-b+no/strings.xml +++ b/app/src/main/res/values-b+no/strings.xml @@ -12,7 +12,8 @@ Avspillingshastighet (%.2fx) Vurdert: %.1f - Ny oppdatering funnet! \n%1$s -> %2$s + Ny oppdatering funnet! +\n%1$s -> %2$s CloudStream Hjem Søk @@ -134,7 +135,8 @@ Avbryt Stopp Gjenoppta - Dette vil slette %s \nEr du sikker? + Dette vil slette %s +\nEr du sikker? Pågående Fullført Posisjon @@ -351,7 +353,8 @@ %1$d-%2$d Offentlig liste programtillegg - %dm \nigjen + %dm +\nigjen Videooppløsning Synkroniser undertekster Undertekstforsinkelse @@ -370,7 +373,11 @@ Ugyldig nettadresse Knippe-nedlasting Slett pakkebrønn - CloudStream har ingen sider installert som forvalg. Du må installere sidene fra pakkebrønner. \n \nSom følge av en hjernedød DMCA-forespørsel fra Sky UK Limited 🤮 kan vi ikke lenke til pakkebrønnssiden i programmet. \n \nTa del i vår Discord, eller søk på nett. + CloudStream har ingen sider installert som forvalg. Du må installere sidene fra pakkebrønner. +\n +\nSom følge av en hjernedød DMCA-forespørsel fra Sky UK Limited 🤮 kan vi ikke lenke til pakkebrønnssiden i programmet. +\n +\nTa del i vår Discord, eller søk på nett. Bruk dette hvis undertekster vises %d ms for sent Programtillegg innlastet Lydspor @@ -505,8 +512,10 @@ ISP-omgåelser Denne listen er tom. Prøv å bytte til en annen. Sorter - Fant fil for trygt modus. \nLaster ikke inn noen utvidelser ved oppstart til filen er fjernet. - Biblioteket ditt er tomt :( \nLogg inn på en bibliotekkonto eller legg til programmer i ditt lokale bibliotek. + Fant fil for trygt modus. +\nLaster ikke inn noen utvidelser ved oppstart til filen er fjernet. + Biblioteket ditt er tomt :( +\nLogg inn på en bibliotekkonto eller legg til programmer i ditt lokale bibliotek. Rediger Profiler Favoritter diff --git a/app/src/main/res/values-b+or/strings.xml b/app/src/main/res/values-b+or/strings.xml index ce7f74290..40a2915fd 100644 --- a/app/src/main/res/values-b+or/strings.xml +++ b/app/src/main/res/values-b+or/strings.xml @@ -84,7 +84,8 @@ ବ୍ୟାକଅପ୍ ଆଣ୍ଡ୍ରଏଡ୍ ଟିଵି ଅଙ୍ଗଭଙ୍ଗୀ - ନୂଆ ଅଦ୍ୟତନ ମିଳିଲା! \n%1$s -> %2$s + ନୂଆ ଅଦ୍ୟତନ ମିଳିଲା! +\n%1$s -> %2$s ଅଵଧି ଆପ୍ ବ୍ୟାକଅପ୍ ଫାଇଲ୍ ଧାରଣ ହେଲା diff --git a/app/src/main/res/values-b+pl/strings.xml b/app/src/main/res/values-b+pl/strings.xml index 2b5414f4a..d1b1cf1c5 100644 --- a/app/src/main/res/values-b+pl/strings.xml +++ b/app/src/main/res/values-b+pl/strings.xml @@ -2,7 +2,8 @@ Prędkość (%.2fx) Ocena: %.1f - Znaleziono nową aktualizację! \n%1$s -> %2$s + Znaleziono nową aktualizację! +\n%1$s -> %2$s Filler %d min %1$s Odc. %2$d @@ -176,8 +177,10 @@ Odtwórz -30 +30 - Spowoduje to trwałe usunięcie %s \nCzy jesteś pewien? - %dm \npozostało + Spowoduje to trwałe usunięcie %s +\nCzy jesteś pewien? + %dm +\npozostało Bieżący Zakończone Status @@ -243,7 +246,7 @@ Aktualizacja Domyślna jakość (WiFi) Maksymalna liczba znaków w tytule odtwarzacza - Pokaż informacje o odtwarzaczu + Pokaż informacje o odtwarzaczu Rozmiar bufora wideo Długość bufora wideo Pamięć podręczna wideo na dysku @@ -324,8 +327,8 @@ Losowy Już wkrótce… Cam - CamRip - CamHD + Cam + Cam HQ HD TS @@ -349,7 +352,7 @@ Niepoprawny URL Błąd Usuń CC z napisów - Usuń reklamy z napisów + Usuń nadmiarowe informacje z napisów Filtrowanie wg preferowanego języka Dodatki Zwiastun @@ -382,7 +385,9 @@ Wyłączono: %d Nie pobrano: %d Zaaktualizowano %d rozszerzeń - CloudStream nie ma domyślnie zainstalowanych żadnych witryn. Musisz zainstalować witryny z repozytoriów. \n \nDołącz do naszego Discorda lub poszukaj online. + CloudStream nie ma domyślnie zainstalowanych żadnych witryn. Musisz zainstalować witryny z repozytoriów. +\n +\nDołącz do naszego Discorda lub poszukaj online. Zobacz repozytoria społeczności Publiczna lista Wszystkie napisy wielką literą @@ -482,9 +487,11 @@ Alfabetycznie (od Z do A) Wybierz bibliotekę Biblioteka - Twoja biblioteka jest pusta :( \nZaloguj się na swoje konto lub dodaj programy do swojej lokalnej biblioteki. + Twoja biblioteka jest pusta :( +\nZaloguj się na swoje konto lub dodaj programy do swojej lokalnej biblioteki. Ta lista jest pusta. Spróbuj przełączyć się na inną. - Znaleziono plik trybu bezpiecznego. \nRozszerzenia nie zostaną wczytane, dopóki plik nie zostanie usunięty. + Znaleziono plik trybu bezpiecznego. +\nRozszerzenia nie zostaną wczytane, dopóki plik nie zostanie usunięty. Używana ilość przewijania, gdy widoczny jest odtwarzacz Ukryty odtwarzacz - ilość przewijania Android TV @@ -508,7 +515,13 @@ Obchodzi blokadę surowych adresów URL GitHuba za pomocą jsDelivr. Może powodować opóźnienie aktualizacji o kilka dni. Nie udało się połączyć z GitHubem. Włączono serwer pośredniczący jsDelivr… Domyślna jakość (dane mobilne) - W tym miejscu można zmienić kolejność źródeł. Jeśli wideo ma wyższy priorytet, pojawi się wyżej w wyborze źródła. Priorytet wideo jest sumą priorytetu źródła i priorytetu jakości. \n \nŹródło A: 3 \nJakość B: 7 \nŁączny priorytet wideo będzie wynosił 10. \n \nUWAGA: Jeśli suma wynosi 10 lub więcej, odtwarzacz automatycznie pominie ładowanie po załadowaniu tego łącza! + W tym miejscu można zmienić kolejność źródeł. Jeśli wideo ma wyższy priorytet, pojawi się wyżej w wyborze źródła. Priorytet wideo jest sumą priorytetu źródła i priorytetu jakości. +\n +\nŹródło A: 3 +\nJakość B: 7 +\nŁączny priorytet wideo będzie wynosił 10. +\n +\nUWAGA: Jeśli suma wynosi 10 lub więcej, odtwarzacz automatycznie pominie ładowanie po załadowaniu tego łącza! Profil %d Wi-Fi Dane mobilne @@ -528,7 +541,11 @@ Usunięto %s z ulubionych Ulubione Dodano %s do ulubionych - W swojej bibliotece znaleziono potencjalne duplikaty: \n \n%s \n \nCzy chcesz dodać ten element, zastąpić istniejące, czy anulować operację? + W swojej bibliotece znaleziono potencjalne duplikaty: +\n +\n%s +\n +\nCzy chcesz dodać ten element, zastąpić istniejące, czy anulować operację? Wprowadź PIN dla %s Częstotliwość tworzenia kopii zapasowych Znaleziono potencjalny duplikat @@ -545,7 +562,9 @@ Zasubskrybuj Usuń z ulubionych Wybierz konto - Wygląda się, że potencjalny duplikat już znajduje się w bibliotece: \'%s\'. \' \n \nCzy chciałbyś dodać ten element, zastąpić istniejący, czy anulować akcję? + Wygląda się, że potencjalny duplikat już znajduje się w bibliotece: \'%s\'. \' +\n +\nCzy chciałbyś dodać ten element, zastąpić istniejący, czy anulować akcję? Wprowadź PIN PIN Linki załadowane ponownie @@ -571,7 +590,8 @@ Odblokuj aplikację za pomocą odcisku palca, identyfikatora twarzy, kodu PIN, wzoru i hasła. Kopia zapasowa Twoich danych CloudStream została teraz utworzona. Chociaż prawdopodobieństwo tego jest bardzo niskie, wszystkie urządzenia mogą zachowywać się inaczej. W rzadkich przypadkach, gdy dostęp do aplikacji zostanie zablokowany, należy całkowicie wyczyścić dane aplikacji i przywrócić je z kopii zapasowej. Bardzo nam przykro z powodu wszelkich niedogodności z tym związanych. Usuń z ulubionych - %s \npozostało + %s +\npozostało Dodaj do ulubionych Nazwa repozytorium i adres URL Błąd dostępu do schowka. Spróbuj ponownie. @@ -613,11 +633,19 @@ Zaznacz wszystkie Zaznacz elementy do usunięcia Odznacz wszystkie - Czy na pewno chcesz na stałe usunąć następujące elementy? \n \n%s - Usuniesz na stale wszystkie odcinki następującego serialu: \n \n%s - Czy na pewno chcesz na stałe usunąć wszystkie odcinki następującego serialu? \n \n%s + Czy na pewno chcesz na stałe usunąć następujące elementy? +\n +\n%s + Usuniesz na stale wszystkie odcinki następującego serialu: +\n +\n%s + Czy na pewno chcesz na stałe usunąć wszystkie odcinki następującego serialu? +\n +\n%s Usuń pliki - Czy na pewno chcesz na stałe usunąć następujące odcinki %1$s? \n \n%2$s + Czy na pewno chcesz na stałe usunąć następujące odcinki %1$s? +\n +\n%2$s Usuń (%1$d | %2$s) Podgląd paska przewijania Włącz podgląd miniatury na pasku wyszukiwania @@ -649,7 +677,7 @@ Odcinek (malejąco) Ocena (najwyższa) Zaktualizuj wtyczki - Pomyślnie zaktualizowano wtyczki: %d + Pomyślnie zaktualizowano wtyczki: %d! Nie zaktualizowano żadnych wtyczek. Zaktualizuj wtyczki ręcznie Rozpoczęcie procesu aktualizacji wtyczek! @@ -693,7 +721,7 @@ Przeładuj dostawcę Odtwarzaj inne źródło" Nazwa - Rozdzielczość i nazwa + Rozdzielczość i nazwa Dolne lewe Wyrównanie napisów Dolne środkowe @@ -714,16 +742,16 @@ Wyczyść sugestie Pokaż panel obsady Nazwa źródła - Informacje o multimediach + Informacje o multimediach Dodatkowa jasność Włącz filtr jasności, gdy jasność wyświetlacza przekroczy 100% Włączono dodatkową jasność Kolejka pobierania - Obecnie nie ma żadnych plików do pobrania w kolejce. + Obecnie nie ma żadnych plików do pobrania w kolejce. Pobierz wszystkie Anuluj wszystkie Czy chcesz pobrać odcinek %s? - Czy chcesz anulować wszystkie pliki do pobrania z kolejki? + Czy chcesz anulować wszystkie pliki do pobrania z kolejki? %d aktywne pobieranie %d aktywne pobierania @@ -731,47 +759,12 @@ %d aktywnych pobierań - %d pobieranie w kolejce - %d pobierania w kolejce - %d pobierań w kolejce - %d pobierań w kolejce + %d pobieranie w kolejce + %d pobierania w kolejce + %d pobierań w kolejce + %d pobierań w kolejce Priorytet źródła - Zdecyduj, jak mają być sortowane źródła wideo w odtwarzaczu + Zdecyduj, jak mają być sortowane źródła wideo w odtwarzaczu Pokaż nakładkę metadanych odtwarzacza - Wideo - Zapowiedź - Na żywo - Ustawienia profilu - Ukryj źródła z negatywnym priorytetem - Ukryj źródła z błędami - - %d ukryty odnośnik - %d ukryte odnośniki - %d ukrytych odnośników - %d ukrytych odnośników - - Pokaż zegar czasu rzeczywistego - Wyświetl zegar czasu rzeczywistego u góry ekranu. Dotyczy strony głównej i odtwarzacza - Logcat 🐈 - Uruchamiaj filmy wstrzymane - Ogranicz obciążenie podczas wyboru źródeł - Tło - Krawędź - Kompresor - Kompresor zakresu dynamicznego - Wyświetlaj w odtwarzaczu elementy sterujące kompresorem (pomaga wyrównać głośność dźwięku) - Włączony - Włączony · %1$d dB / +%2$d dB - Wyłączony - Lekki - Wzmocnienie dialogów - Silny - Próg: %d dB - Wzmocnienie kompensujące: +%d dB - Dźwięki głośniejsze niż %d dB są ściszane - Zwiększa ogólną głośność o +%d dB - Domyślne obrazy - Adres URL - Wybór obrazu diff --git a/app/src/main/res/values-b+pt+BR/strings.xml b/app/src/main/res/values-b+pt+BR/strings.xml index e5882d7d8..b856246e8 100644 --- a/app/src/main/res/values-b+pt+BR/strings.xml +++ b/app/src/main/res/values-b+pt+BR/strings.xml @@ -180,7 +180,8 @@ Continuar -30 +30 - Isso apagará %s permanentemente \nVocê tem certeza? + Isso apagará %s permanentemente +\nVocê tem certeza? %dm\nrestantes Em andamento Concluído @@ -340,8 +341,8 @@ Aleatório Em breve… Cam - CamRip - Cam HD + Cam + Cam HQ HD TS @@ -365,7 +366,7 @@ URL inválida Erro Remover legendas ocultas(CC) das legendas - Remover anúncios das legendas + Remover bloat das legendas Filtrar por linguagem preferida Extras Trailer @@ -395,7 +396,9 @@ Transferido: %d Desativado: %d Não transferido: %d - CloudStream não tem fontes instaladas por padrão. Você precisa instalar um site de repositórios. \n \nEntre no nosso Discord ou pesquise online. + CloudStream não tem fontes instaladas por padrão. Você precisa instalar um site de repositórios. +\n +\nEntre no nosso Discord ou pesquise online. Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas @@ -438,7 +441,8 @@ Abrir com Selecionar Biblioteca Passou nos testes - Sua biblioteca está vazia :0 \nEntre numa conta de biblioteca ou adicione Midias para sua biblioteca local. + Sua biblioteca está vazia :0 +\nEntre numa conta de biblioteca ou adicione Midias para sua biblioteca local. Qualidade preferida de reprodução (Dados Móveis) Legado Biblioteca @@ -456,8 +460,15 @@ Alfabética(Z => A) Qualidade Perfil de plano de fundo - Aqui você pode alterar como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais alta, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte e da prioridade da qualidade é a prioridade do vídeo. \n \nFonte A: 3 \nQualidade B: 7 \nTerá uma prioridade de vídeo combinada de 10. \n \nNOTA: Se a soma for 10 ou mais, o Player pulará automaticamente o carregamento quando o link for carregado! - Arquivo de modo de segurança encontrado! \nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido. + Aqui você pode alterar como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais alta, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte e da prioridade da qualidade é a prioridade do vídeo. +\n +\nFonte A: 3 +\nQualidade B: 7 +\nTerá uma prioridade de vídeo combinada de 10. +\n +\nNOTA: Se a soma for 10 ou mais, o Player pulará automaticamente o carregamento quando o link for carregado! + Arquivo de modo de segurança encontrado! +\nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido. Inscrito em %s Episódio %d lançado! Selecionar padrão @@ -542,7 +553,11 @@ Duplicata em potencial encontrada Adicionar Substituir - Possíveis itens duplicados foram encontrados em sua biblioteca: \n \n %s \n \nGostaria de adicionar este item mesmo assim, substituir os existentes ou cancelar a ação? + Possíveis itens duplicados foram encontrados em sua biblioteca: +\n +\n %s +\n +\nGostaria de adicionar este item mesmo assim, substituir os existentes ou cancelar a ação? Insira o PIN Insira o PIN para %s Insira o PIN atual @@ -561,7 +576,9 @@ Links recarregados Frequência de backup Substitua tudo - Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' \n \nGostaria de adicionar este item mesmo assim, substituir o existente ou cancelar a ação? + Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' +\n +\nGostaria de adicionar este item mesmo assim, substituir o existente ou cancelar a ação? Inscrever-se Cancelar inscrição Usar conta padrão @@ -582,7 +599,8 @@ A autenticação biométrica não é compatível com este dispositivo Desbloquear o aplicativo com impressão digital, ID facial, PIN, padrão e senha. Após algumas tentativas fracassadas, o prompt será fechado. Basta reiniciar o aplicativo para tentar novamente. - %s \nrestante(s) + %s +\nrestante(s) Favorito Não favorito copiado! @@ -616,7 +634,7 @@ Reproduzir do começo Reprovou alguns testes Excluir plugin - Atualmente não há downloads disponíveis. + Você não baixou nada :/ Ocultar os nomes dos controles do player Abrir arquivo de vídeo Data de lançamento (do novo ao antigo) @@ -662,7 +680,7 @@ Data %s Atualizar plugins Iniciando o processo de atualização do plugin! - Atualização bem-sucedida para %d plugin(s) + Atualização bem-sucedida para %d plugin(s)! Nenhum plugin foi atualizado. Atualizar plugins manualmente Notificações de reprodução @@ -736,7 +754,7 @@ Nome da fonte Baixar tudo Cancelar tudo - Você deseja baixar o episódio%s? + Você deseja baixar o episódio%s Você gostaria de cancelar todos os downloads da fila? %ddownload ativo @@ -748,25 +766,4 @@ %d downloads na sequência %d downloads na sequência - Mostrar sobreposição de metadados do reprodutor - Vídeo - Visualização - Ao vivo - Mostrar relógio em tempo real - Exiba um relógio em tempo real na parte superior da tela. Aplica-se à página inicial e ao player - Configurações de perfil - Ocultar fontes com prioridade negativa - Ocultar fontes com erros - - %d link oculto - %d link ocultos - %d link ocultos - - Borda - Logcat 🐈 - Iniciar vídeos pausados - Reduza o estresse ao selecionar fontes - Fundo - Compressor - Compressor de faixa dinâmica diff --git a/app/src/main/res/values-b+pt/strings.xml b/app/src/main/res/values-b+pt/strings.xml index 960a91d0d..f9dd3083b 100644 --- a/app/src/main/res/values-b+pt/strings.xml +++ b/app/src/main/res/values-b+pt/strings.xml @@ -18,7 +18,8 @@ Visualizar plano de fundo Velocidade (%.2fx) Classificado: %.1f - Nova atualização encontrada! \n%1$s -> %2$s + Nova atualização encontrada! +\n%1$s -> %2$s Preenchimento CloudStream Assistir com o CloudStream @@ -61,7 +62,7 @@ Transmitir Erro a Carregar Links Armazenamento Interno - Dub + Dob Leg Eliminar Ficheiro Reproduzir Ficheiro @@ -100,7 +101,7 @@ Importar fontes colocando em %s Continuar a Assistir Remover - Mais informações + Mais info Uma VPN pode ser necessária para que este fornecedor funcione corretamente Este fornecedor é um torrent, uma VPN é recomendada Metadados não são oferecidos pelo site, o carregamento do vídeo irá falhar se ele não existir no site. @@ -142,7 +143,7 @@ Procurar Contas e segurança Atualizações e cópias de segurança - Informações + Info Procura Avançada Mostra resultados separados por fornecedor Mostrar episódios de enchimento para anime @@ -175,8 +176,10 @@ Cancelar Pôr em Pausa Retomar - Isto apagará %s permanentemente \nTem a certeza? - %dm \nem falta + Isto apagará %s permanentemente +\nTem a certeza? + %dm +\nem falta Em Curso Concluído Estado @@ -318,9 +321,9 @@ Carregar de arquivo Carregar da Internet Arquivo baixado - Principal - Suporte - Plano de fundo + Protagonista + Coadjuvante + Figurante Aleatório Em breve… Imagem de Poster @@ -361,7 +364,9 @@ Transferido: %d Desativado: %d Não transferido: %d - O CloudStream não tem sites instalados por padrão. É necessário instalar os sites a partir de repositórios. \n \nJunte-se ao nosso Discord ou pesquise online. + O CloudStream não tem sites instalados por padrão. É necessário instalar os sites a partir de repositórios. +\n +\nJunte-se ao nosso Discord ou pesquise online. Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas @@ -469,8 +474,10 @@ Atualizando shows inscritos Alfabético (A a Z) Avaliações (Crescente) - A sua biblioteca está vazia :( \nEntre numa conta da biblioteca ou adicione espectáculos à sua biblioteca local. - Arquivo de modo de segurança encontrado! \nNenhuma extensão será carregada na inicialização do app até que o arquivo seja removido. + A sua biblioteca está vazia :( +\nEntre numa conta da biblioteca ou adicione espectáculos à sua biblioteca local. + Arquivo de modo de segurança encontrado! +\nNenhuma extensão será carregada na inicialização do app até que o arquivo seja removido. Contorno do provedor de serviço de internet (ISP) Links Recursos do Player @@ -515,7 +522,13 @@ Ajuda Qualidades Perfil de fundo - Aqui pode alterar a forma como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais elevada, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte com a prioridade da qualidade é a prioridade do vídeo. \n \nFonte A: 3 \nQualidade B: 7 \nTerá uma prioridade de vídeo combinada de 10. \n \nNOTA: Se a soma for 10 ou mais, o leitor saltará automaticamente o carregamento quando essa ligação for carregada! + Aqui pode alterar a forma como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais elevada, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte com a prioridade da qualidade é a prioridade do vídeo. +\n +\nFonte A: 3 +\nQualidade B: 7 +\nTerá uma prioridade de vídeo combinada de 10. +\n +\nNOTA: Se a soma for 10 ou mais, o leitor saltará automaticamente o carregamento quando essa ligação for carregada! Selecionar o modo para filtrar a transferência de plug-ins Não foi possível criar corretamente a interface do utilizador, trata-se de um GRANDE BUG e deve ser comunicado imediatamente %s Desativar @@ -523,7 +536,7 @@ Repositório não encontrado, verifique o URL e tente a VPN Você já votou Cancelar Inscrição - Inscrever-se + Subscrever Favoritos A recarregar links Frequência de Backup @@ -547,8 +560,14 @@ Selecione uma conta Gerenciar contas Usar conta padrão - Potenciais itens duplicados foram encontrados na sua biblioteca: \n \n%s \n \nDeseja adicionar esse item mesmo assim, subtituir os existentes, ou cancelar a ação? - Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' \n \nDeseja adicionar esse item mesmo assim, subtituir o existente, ou cancelar a ação? + Potenciais itens duplicados foram encontrados na sua biblioteca: +\n +\n%s +\n +\nDeseja adicionar esse item mesmo assim, subtituir os existentes, ou cancelar a ação? + Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' +\n +\nDeseja adicionar esse item mesmo assim, subtituir o existente, ou cancelar a ação? Mostrar recomendações Adiciona uma opção de velocidade no leitor Testar todas as extensões @@ -565,7 +584,8 @@ Desfavorito Bloqueio com biometria copiado! - %s \nrestante + %s +\nrestante Erro ao aceder à área de transferência, tente novamente. Erro ao copiar, copie o logcat e contacte o suporte da aplicação. Desbloquear o CloudStream @@ -591,7 +611,9 @@ Pré-visualização na barra de progresso Ativar a miniatura de pré-visualização na barra de progresso Autenticação Local - Irá também eliminar permanentemente todos os episódios da seguinte série: \n \n%s + Irá também eliminar permanentemente todos os episódios da seguinte série: +\n +\n%s Eliminar (%1$d | %2$s) Visite %s no seu smartphone ou computador e introduza o código acima Recomeçar @@ -604,9 +626,15 @@ Próximo em %s Eliminar Ficheiros Aviso - Tem a certeza que pretende eliminar permanentemente os seguintes items? \n \n%s - Tem a certeza que pretende eliminar permanentemente os seguintes episódios em %1$s? \n \n%2$s - Tem a certeza que pretende eliminar permanentemente todos os episódios da seguinte série? \n \n%s + Tem a certeza que pretende eliminar permanentemente os seguintes items? +\n +\n%s + Tem a certeza que pretende eliminar permanentemente os seguintes episódios em %1$s? +\n +\n%2$s + Tem a certeza que pretende eliminar permanentemente todos os episódios da seguinte série? +\n +\n%s Segurança Contas QR Code @@ -686,7 +714,7 @@ Imagem Atualizada com Sucesso Marcar como assistido o episódio Removar marcação de assistido até esse episódio - Recarregar + Recarregado Provedor de Recarregamento Reproduzir do servidor alternativo" Nome @@ -733,8 +761,4 @@ %d downloads na fila %d downloads na fila - Mostrar sobreposição de metadados do player - Vídeo - Pré-visualização - Ao Vivo diff --git a/app/src/main/res/values-b+qt/strings.xml b/app/src/main/res/values-b+qt/strings.xml index 6bbb0ddba..bbe145842 100644 --- a/app/src/main/res/values-b+qt/strings.xml +++ b/app/src/main/res/values-b+qt/strings.xml @@ -185,7 +185,8 @@ u ooah uo ahauao huhuu hauu h a ou oh ouhuouhoaaha aaooohhouhhha hauauuu - aaaaaaa uuuuuu \n%1$s -> %2$s + aaaaaaa uuuuuu +\n%1$s -> %2$s %1$s aaou %2$d oouaaahh %s aaaaaaugh ouh %d uuoogahaaah ooua-h-ha @@ -227,7 +228,8 @@ aaaaaaaaaaahhhgh-aooohoooo aau aooooghaao aagh aaaaaaaaaaaa oooh, aaough, ooga oguuu aaaaaaaaaaa ooooooohghh a-a-aaauo - %dmmmmmm.. \naaaaooughugh + %dmmmmmm.. +\naaaaooughugh aooohuohaaaa ooooagh oooooogh-aaaaaogh guuuaaaahhhhhhhaaa @@ -276,7 +278,9 @@ aaaagg uug oooogg oooogg - oooohhhoogg uuh uh uuuhh aaaaggguh og ooooggg uug aagg ek aaaaggg oog aaahh aagg uuuugggooohh \n \nJoin uuh uuuuggg ag uuuuhh eeeeek + oooohhhoogg uuh uh uuuhh aaaaggguh og ooooggg uug aagg ek aaaaggg oog aaahh aagg uuuugggooohh +\n +\nJoin uuh uuuuggg ag uuuuhh eeeeek uuugg aaaagg oogg uugg uh aaagg @@ -309,7 +313,13 @@ ooooggg %d aahh oooogggk - eeek aag uug uuuuhh ooh aak uuuuggg ooh uuuuhhh ug g eeeek oog h uuuuhh oooogggh ag aahh oooohh aaaagg uh oog uuuugg uuuugggog uug oog uh uuh aaaagg uuuuuukg uug aah uuuuuuk uuuuuukg ak ooh uuuhh aaaagggk \n \nSource A: 3 \nQuality B: 7 \nWill uuhh k uuuuhhhk ooogg uuuuhhhh uk 10 \n \nNOTE: ah uug oog ug 10 og oogg uug aaaahh uuhh uuuugggaaaahh oogg aaaahhh oogg uuhh aahh uh loaded! + eeek aag uug uuuuhh ooh aak uuuuggg ooh uuuuhhh ug g eeeek oog h uuuuhh oooogggh ag aahh oooohh aaaagg uh oog uuuugg uuuugggog uug oog uh uuh aaaagg uuuuuukg uug aah uuuuuuk uuuuuukg ak ooh uuuhh aaaagggk +\n +\nSource A: 3 +\nQuality B: 7 +\nWill uuhh k uuuuhhhk ooogg uuuuhhhh uk 10 +\n +\nNOTE: ah uug oog ug 10 og oogg uug aaaahh uuhh uuuugggaaaahh oogg aaaahhh oogg uuhh aahh uh loaded! uuuuhhhug %s aaaaggg uuhh aaaagggug oog @@ -478,7 +488,10 @@ uuh uuhh uuuuggg uuuhh %s ooogg oh oooohhhog aaaahhh uuh - oh ooooggg oogg k uuuuuukaahh uuuugggag uuhh ooooggg oooogg ah oogg library: \'%s\' \n \n \nWould uug uugg uh aak oogg oohh anyway, oooohhh uuh oooohhhg one, oh aaaagg eek action? + oh ooooggg oogg k uuuuuukaahh uuuugggag uuhh ooooggg oooogg ah oogg library: \'%s\' +\n +\n +\nWould uug uugg uh aak oogg oohh anyway, oooohhh uuh oooohhhg one, oh aaaagg eek action? ooogg uuh uuhh g ooogg oooogg oh aah uuuugg uugg oogg ak aaagg ooh oooohhhuug aagg uug aahh uug oooohhhg ak oooohh uuuuuuk oh uuh aaaagggag @@ -510,7 +523,8 @@ uuuugg oogg oh eeeeek eeeeeek uuuugggg aagg uuugg uuuuggg +30 - %s \nremaining + %s +\nremaining aagg uuuuhhh uuuuhh aag aah oogg Fingerprint, eeek ID, PIN, aaaaggg uuh aaaagggh eeeeek uuuhh oh aaaagg @@ -530,13 +544,21 @@ aaaagg aaahh aaaahhhg uuuhh oooohhh uh %s oohh uuuuggg uuhh uuugg - uuh oohh oogg aaaahhhuugg uuuugg oog aaaagggh ak uuh uuuuhhhog series: \n \n%s + uuh oohh oogg aaaahhhuugg uuuugg oog aaaagggh ak uuh uuuuhhhog series: +\n +\n%s aak aaaagggah oooohh uuuhh oooogg (%1$d | %2$s) - uuh uug oohh uuk aagg oh uuuuggguugg aaaahh uuh aaaagggah items? \n \n%s - uug ooh aagg ooh uuuk ag aaaahhhuuhh oooohh ooh oooohhhug oooohhhh ah %1$s? \n \n%2$s - ooh aag oogg uuh aagg ug eeeeeekoohh aaaahh aah uuuugggg uk aag oooohhhek series? \n \n%s + uuh uug oohh uuk aagg oh uuuuggguugg aaaahh uuh aaaagggah items? +\n +\n%s + uug ooh aagg ooh uuuk ag aaaahhhuuhh oooohh ooh oooohhhug oooohhhh ah %1$s? +\n +\n%2$s + ooh aag oogg uuh aagg ug eeeeeekoohh aaaahh aah uuuugggg uk aag oooohhhek series? +\n +\n%s %1$s %2$s aaaahhh uuuhh eeeeek oooohhhg ah uuk uuh aahh ug aaaaggg aaak ooh oooohhh space, aahh ug oooohhh ek @@ -590,8 +612,10 @@ uuuuggg (Old og New) uuuuggguuuhh (Z ak A) uuuugg uuuuggg - uuhh eeeeeek ek uuugg :( \nLog oh ag h aaaaggg uuuuggg ah uuk ooogg ag uugg uuugg oooohhh - oohh aagg uugg found! \nNot aaaaggg aak uuuugggaag ah uuuuhhh aaaak oogg ah eeeeeek + uuhh eeeeeek ek uuugg :( +\nLog oh ag h aaaaggg uuuuggg ah uuk ooogg ag uugg uuugg oooohhh + oohh aagg uugg found! +\nNot aaaaggg aak uuuugggaag ah uuuuhhh aaaak oogg ah eeeeeek uuuuggguuugg oohh %s ooooggg %d released! uugg @@ -599,7 +623,11 @@ ooh ooooggg oooohhh uuuuhhhoog uh ooh oooogg ag ek oooohhh correctly, uuhh uh g ooogg aah uug aaaagg uh uuuugggk uuuuhhhaahh %s - uuuuggguk uuuuhhhoh uuugg aahh aaak uuugg ag oohh library: \n \n%s \n \nWould aag aagg og uuk uugg oogg anyway, aaaahhh ooh uuuugggh ones, ek uuuuhh aah action? + uuuuggguk uuuuhhhoh uuugg aahh aaak uuugg ag oohh library: +\n +\n%s +\n +\nWould aag aagg og uuk uugg oogg anyway, aaaahhh ooh uuuugggh ones, ek uuuuhh aah action? oooohhhag uug oooohh uuh eeeek aaaaggg h oooogg aaaaak uug uuuugg aaaaaakaagg uuuuhh uuuuhhhuh oooogggag ag aaaahh uuuuggguugg aaagg ek aaagg aaaagggaagg diff --git a/app/src/main/res/values-b+ro/strings.xml b/app/src/main/res/values-b+ro/strings.xml index b1852640c..5f612c640 100644 --- a/app/src/main/res/values-b+ro/strings.xml +++ b/app/src/main/res/values-b+ro/strings.xml @@ -19,7 +19,8 @@ Viteză (%.2fx) Evaluare: %.1f - Actualizare nouă găsită! \n%1$s -> %2$s + Actualizare nouă găsită! +\n%1$s -> %2$s Filler %d min @@ -176,8 +177,10 @@ Continuă -30 +30 - Sunteți pe cale să ștergeți definitiv %s \nSunteți sigur? - %dm \nrămas + Sunteți pe cale să ștergeți definitiv %s +\nSunteți sigur? + %dm +\nrămas În curs de desfășurare Finalizat Status @@ -427,7 +430,8 @@ Funcții Autori Adaugă depozit - Biblioteca ta este goală :( \nConectați-vă într-un cont de bibliotecă sau adăugați emisiuni la biblioteca locală. + Biblioteca ta este goală :( +\nConectați-vă într-un cont de bibliotecă sau adăugați emisiuni la biblioteca locală. Eliminați subtitrările închise din subtitrări Descărcați lista de site-uri pe care doriți să le utilizați Evaluare (Ridicat la Scăzut) @@ -467,10 +471,13 @@ URL invalid Toate extensiile au fost dezactivate din cauza unei defecțiuni pentru a vă ajuta să o găsiți pe cea care cauzează probleme. Se descarcă actualizarea aplicației… - CloudStream nu are niciun site instalat din start. Trebuie să instalați site-urile din depozite. \n \nAlăturați-vă Discord-ului nostru sau căutați online. + CloudStream nu are niciun site instalat din start. Trebuie să instalați site-urile din depozite. +\n +\nAlăturați-vă Discord-ului nostru sau căutați online. A început să descarce %1$d %2$s… Mod sigur pornit - Fișier Mod Sigur găsit! \nNu încarcă nicio extensie la pornire până când fișierul nu este eliminat. + Fișier Mod Sigur găsit! +\nNu încarcă nicio extensie la pornire până când fișierul nu este eliminat. Scoateți de la urmărit Actualizat (Vechi la Nou) Reporniți aplicația pentru a vedea schimbările. @@ -524,7 +531,13 @@ Actualizări al aplicației Subtitrări Dezactivați - Aici puteți schimba modul în care sunt ordonate sursele. Dacă un videoclip are o prioritate mai mare, acesta va apărea mai sus în selecția surselor. Suma dintre prioritatea sursei și prioritatea calității reprezintă prioritatea video. \n \nSursa A: 3 \nCalitate B: 7 \nVa avea o prioritate video combinată de 10. \n \nNOTĂ: Dacă suma este 10 sau mai mare, playerul va sări automat peste încărcare atunci când este încărcat link-ul respectiv! + Aici puteți schimba modul în care sunt ordonate sursele. Dacă un videoclip are o prioritate mai mare, acesta va apărea mai sus în selecția surselor. Suma dintre prioritatea sursei și prioritatea calității reprezintă prioritatea video. +\n +\nSursa A: 3 +\nCalitate B: 7 +\nVa avea o prioritate video combinată de 10. +\n +\nNOTĂ: Dacă suma este 10 sau mai mare, playerul va sări automat peste încărcare atunci când este încărcat link-ul respectiv! Nu s-a găsit plugin-uri în depozit Nu s-a găsit depozitul, verificați URL-ul și încercați cu un VPN Editați @@ -540,12 +553,18 @@ UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s Selectați modul de filtrare a descărcării plugin-urilor Ați votat deja - Elemente potențial duplicate au fost găsite în biblioteca ta: \n \n%s \n \nÎn ciuda acestui fapt, ai dori să adaugi acest alement, să le înlocuiești pe cele existente, sau să anulezi acțiunea? + Elemente potențial duplicate au fost găsite în biblioteca ta: +\n +\n%s +\n +\nÎn ciuda acestui fapt, ai dori să adaugi acest alement, să le înlocuiești pe cele existente, sau să anulezi acțiunea? %s a fost adăugat la favoriți/te %s a fost eliminat din favoriți/te Adaugă la favoriți/te Elimină din favoriți/te - Se pare că un element potențial duplicat deja există în biblioteca ta: \'%s.\' \n \nÎn ciuda aceasta, ai dori să adaugi acest element, să îl înlocuiești pe cel existent, sau să anulezi acțiunea? + Se pare că un element potențial duplicat deja există în biblioteca ta: \'%s.\' +\n +\nÎn ciuda aceasta, ai dori să adaugi acest element, să îl înlocuiești pe cel existent, sau să anulezi acțiunea? Introduce PIN-ul pentru %s Introduce PIN-ul actual Introduce PIN-ul @@ -600,7 +619,8 @@ Resetați Activați comutarea automată a orientării ecranului pe baza orientării video Blocare cu biometrie - %s \nrămase + %s +\nrămase Următorul în %s CloudStream Wiki Sezonul %1$d Episod %2$d va fi lansat în diff --git a/app/src/main/res/values-b+ru/strings.xml b/app/src/main/res/values-b+ru/strings.xml index ff15190e6..5c271f871 100644 --- a/app/src/main/res/values-b+ru/strings.xml +++ b/app/src/main/res/values-b+ru/strings.xml @@ -34,7 +34,8 @@ Предпросмотр фона Скорость (%.2fx) Оценили: %.1f - Новое обновление найдено! \n%1$s -> %2$s + Новое обновление найдено! +\n%1$s -> %2$s Заполнитель CloudStream Убрать @@ -171,8 +172,10 @@ Продолжить -30 +30 - Это будет удалено безвозвратно%s \nВы уверены? - %d мин. \nосталось + Это будет удалено безвозвратно%s +\nВы уверены? + %d мин. +\nосталось Завершено Год Рейтинг @@ -365,8 +368,8 @@ дополнение Пропустить настройку Ошибка - CamHD - CamRip + Cam + Cam HDR DVD WP @@ -408,7 +411,9 @@ Трейлер %s (отключено) Далее - В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев. \n \nПрисоединяйтесь к нашему Discord-серверу или найдите в интернете. + В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев. +\n +\nПрисоединяйтесь к нашему Discord-серверу или найдите в интернете. Недопустимые данные Разрешение и название Предыдущий @@ -481,7 +486,7 @@ Объем перемотка, используемый, когда плеер виден Плеер показан - Перемотки объем Плеер спрятан - Перемотки объем - Удалять рекламу из субтитров + Удалять лишнее из субтитров Местоположение ползунка, когда игрок скрыт Android TV Второго планa @@ -521,7 +526,13 @@ Вы уже проголосовали Никаких дополнений не обнаружено в источнике Поставить обычный - Здесь вы можете изменить порядок расположения источников. Если видео имеет более высокий приоритет, оно будет отображаться выше в списке источников. Сумма приоритета источника и приоритета качества составляет приоритет видео. \n \nИсточник А: 3 \nКачество Б: 7 \nБудет иметь общий приоритет видео 10. \n \nПРИМЕЧАНИЕ. Если сумма равна 10 или более, плеер автоматически пропустит загрузку при загрузке этой ссылки! + Здесь вы можете изменить порядок расположения источников. Если видео имеет более высокий приоритет, оно будет отображаться выше в списке источников. Сумма приоритета источника и приоритета качества составляет приоритет видео. +\n +\nИсточник А: 3 +\nКачество Б: 7 +\nБудет иметь общий приоритет видео 10. +\n +\nПРИМЕЧАНИЕ. Если сумма равна 10 или более, плеер автоматически пропустит загрузку при загрузке этой ссылки! Ссылки перезагружены Выберите учётную запись %s убрано из любимых @@ -575,7 +586,8 @@ Аудиокнига Медиа Разблокируйте приложение с помощью отпечатка пальца, Face ID, ПИН-кода, шаблона и пароля. - %s \nосталось + %s +\nосталось Отключить оптимизацию батареи Аутентификация по паролю/ПИН-коду Биометрическая аутентификация на этом устройстве не поддерживается @@ -605,7 +617,9 @@ Вы уверены, что хотите навсегда удалить все серии в данном сериале? \n \n%s Выберите элементы для удаления Удалить (%1$d | %2$s) - Вы уверены, что хотите навсегда удалить данный объект? \n \n%s + Вы уверены, что хотите навсегда удалить данный объект? +\n +\n%s Невозможно получить ПИН-код устройства, попробуйте локальную аутентификацию Откройте %s на вашем смартфоне или компьютере и введите данный код CloudStream Вики @@ -735,37 +749,4 @@ Отменить всё Вы хотите загрузить эпизод %s? Вы хотите отменить всё запланированные загрузки? - Показывать наложения метаданных проигрывателя - Видео - Предпросмотр - Прямой эфир - Отображать текущее время - Отображать в верхней части экрана часы с текущим временем. Применяется к главной странице и плееру - Настройки профиля - Скрывать источники с отрицательным приоритетом - Скрывать источники с ошибками - - %d скрытая ссылка - %d скрытые ссылки - %d скрытых ссылок - %d скрытых ссылок - - Edge - Logcat 🐈 - Запускать видео на паузе - Снижает нагрузку при выборе источников - Компрессор - Компрессор динамического диапазона - Показывать элементы управления компрессором в проигрывателе (помогает балансировать громкий/тихий звук) - Вкл. - Вкл. · %1$d дБ / +%2$d дБ - Выкл. - Лёгкий - Усиление диалогов - Сильный - Порог: %d дБ - Компенсирующее усиление: +%d дБ - Звуки громче %d дБ приглушаются - Повышает общую громкость на +%d дБ - Фон diff --git a/app/src/main/res/values-b+sk/strings.xml b/app/src/main/res/values-b+sk/strings.xml index f48b27143..8c1c4695f 100644 --- a/app/src/main/res/values-b+sk/strings.xml +++ b/app/src/main/res/values-b+sk/strings.xml @@ -1,6 +1,7 @@ - Našla sa nová aktualizácia! \n%1$s -> %2$s + Našla sa nová aktualizácia! +\n%1$s -> %2$s Výplň %1$dh %2$dm Epizóda %d bude vydaná za @@ -331,8 +332,10 @@ %1$s %2$s Vylúčenie zodpovednosti NSFW - Týmto sa natrvalo vymaže %s \nSte si istý? - %dm \nzostáva + Týmto sa natrvalo vymaže %s +\nSte si istý? + %dm +\nzostáva Prebieha Dokončené Rozloženie emulátora @@ -358,7 +361,9 @@ Zmazať repozitár URL adresa repozitára Verejný zoznam - CloudStream nemá nainštalované žiadne stránky v predvolenom nastavení. Musíte nainštalovať stránky z repozitára. \n \nPripojte sa k nášmu Discord alebo vyhľadajte online. + CloudStream nemá nainštalované žiadne stránky v predvolenom nastavení. Musíte nainštalovať stránky z repozitára. +\n +\nPripojte sa k nášmu Discord alebo vyhľadajte online. Nepodarilo sa nainštalovať novú verziu aplikácie Upozornenie: CloudStream 3 nenesie žiadnu zodpovednosť za používanie rozšírenia tretích strán a neposkytuje žiadnu podporu pre nich! Pridať repozitár diff --git a/app/src/main/res/values-b+so/strings.xml b/app/src/main/res/values-b+so/strings.xml index 8a8f29095..9e4e9f9f1 100644 --- a/app/src/main/res/values-b+so/strings.xml +++ b/app/src/main/res/values-b+so/strings.xml @@ -27,7 +27,8 @@ %d dqq Kadinka Qiimaysan: %.1f - App cusub baa soo baxay! \n%1$s -> %2$s + App cusub baa soo baxay! +\n%1$s -> %2$s Soo dejinta Raadi Dookhyo kale @@ -65,7 +66,7 @@ Kaydka gudaha Trj Way bilaabmatay soo dejintu - Khadka ka daaro + Daalaco Waa la joojiyey dejintan Way bilaabantay cuaboonaysiintu Soo raridda lifaaqyadu way fashilantay @@ -91,7 +92,7 @@ Tirtir Nooca cidhifyada Isticmaal xaddiga iftiinka ee moobilkaaga halkii aad adigu habayn lahayd ugu horreynba - Cusboonaysiinta iyo Kaydinta + Gurmadka iyo cusbooneysiinta Waa la kaydiyet xogta gurmadka Raadinta waxay kuugu kala qaybinaysaa mid kasta iyo qaybiyihiisa Way fashilantay in xogta laga soo celiyo faylka gurmadka%s @@ -100,7 +101,7 @@ Xogta waa la kaydiyey Heerka Ogolaanshaha lagu kaydinayo ma hayno. Fadlan isku day mar kale. - Akoonada iyo Amniga + Koontooyinka Xog Si iskii ah usoo deji sidkanaha Qari tayada muqaalka aad dooratay natiijada waxa aad raadiso @@ -153,7 +154,7 @@ Tusi Boodhka Kitsu Si iskii ah u cusbooneysii sidkanaha Si iskii ah usoo deji dhamaan sidkanayaasha aan weli lasoo dejin ee ku jira kaydka aad keentay. - Si toos ah u raadi update cusub markii aad furto appka. + Marka ugu horreysa ee aad appka furto ayaad arkaysaa haddii wax cusub yihiin Light novel app, isla dhiseyaasha appkan baa leh Tus wixii appka ku cusub Dib uso bilow fadhiisinta appka @@ -182,9 +183,11 @@ Tirtir faylka Sii wado -30 - Dhamaantii waa la saari doona %s \nSow ma hubtid? + Dhamaantii waa la saari doona %s +\nSow ma hubtid? Fashil ka yimi xigashada - %ddq \nAyaa hadhsan + %ddq +\nAyaa hadhsan Dhamaystirmay Sannadka Qiimaynta @@ -238,13 +241,13 @@ Hareeraha madow ka saar markaad daawanayso Qoraal-hoosaadka Habaynta Qrl-hoosaadka Koromakaastiga - Xawaaraha muuqaalka - Midig iyo bidix u jiid si aad u xakameyso halka uu muuqaalku marayo + Habka Xawaare-kordhinta + Midig iyo bidix u jiid si aad marba dhinac ugu dhaafiso muqaalka Laba jeer taabo midig ama bidix si aad u dhaafiso ama ku ceshato Laba jeer taabo si aad u dhaafiso - Inta aad is-dhaafinayso (Ilbiriqsiyo ahaan) + Inta aad is dhaafinayso Laba jeer taabo si aad u qallajiso - Labo jeer dhexda taabo si aad u joojiso + Dhexda taabo si aad u qallajisato Gurmad u samee xogta Iftiinka moobilka istcimaal Cusbooneysii marba halkaad marayso daawashada @@ -285,7 +288,7 @@ La degtay Soo degaya Ka Saar - Kor ama hoos u jiid geesaha muuqaalka si aad u maamusho codka ama iftiinka + Kor ama hoos u jiid labada dhinaca ee daaraha si aad u maamusho codka ama iftiinka Iskii-u-daarka xalqadda xigta Si iskii ah ayuu u daarayaa xalqadda xigta marka aad dhamaysato kaad daawanayso Si iskii ah ha ula socdo appku hadba xalqadda aad marayso @@ -310,7 +313,7 @@ Safe mode ayaa daaran Dhammaan Kordhiyeyaasha waa la wada demiyey cillad timi aawadeed si aad u ogaato ka cillada sababaya. %s (La xayiray) - Dib u fur app-ka si aad u aragto isbeddelada. + Marka dambe eed gasho isticmaal Eeg warbixinra Cilladda Qiimeynta: %s Lahaanshaha @@ -424,8 +427,12 @@ Xayiran: %d Aan dejinayn: %d Waxa la cusbooneysiiyey %d sidkane - Ugu horreyn cloudstream ma laha wax websaydyo uu filimaanta kasoo xigto, waxay noqonaysaa inaad adigu rakibato reboositarradooda. Sababtuna waa in mar dhexdaas ah na dacweeyeen shirkadda Sky UK Limited, markaa si aan mar dambe taasi u dhicin anagu kuma rakibi karno...\n\nDiscord naga soo qabo ama internetka ka baadh. - Digniin: CloudStream mas\'uuliyad kama qaadanayo isticmaalka kordhinnada (extensions) dhinac saddexaad mana bixiyo wax taageero ah oo iyaga ku saabsan! + Ugu horreyn cloudstream ma laha wax websaydyo uu filimaanta kasoo xigto, waxay noqonaysaa inaad adigu rakibato reboositarradooda... +\n +\nSababtuna waa in mar dhexdaas ah na dacweeyeen shirkadda Sky UK Limited🤮, markaa si aan mar dambe taasi u dhicin anagu kuma rakibi karno... +\n +\nDiscord naga soo qabo ama internetka ka baadh. + Soo deji dhamaan sidkanayaasha reboositarkan? Boodhka Boodhka xalqadda Boodhka weyn @@ -465,45 +472,4 @@ Bilowga Bilow isku qasan Qoraalka dhamaadka - Seasonka %1$d Xalqada %2$d waxaa la soo deyn doonaa - %1$d saac %2$d daq %3$d ilb - %1$ddaq %2$dilb - %1$dilb - Sawirka - Ka bilow bilowga - Waxa soo degaya - Aqoonsiga codka ma shaqeynayo - Bilow hadalka… - biraawsar - Daaro musalsalka oo dhan - Fiidiyowgaan waa Torrent, taasi waxay la macno tahay in la raadraaci karo dhaqdhaqaaqaaga daawashada.\nHubi inaad fahamsan tahay sida Torrent-ku u shaqeeyo ka hor intaadan sii socon. - Dooro waxyaabaha aad tirayso - Ma jiro muuqaal aad soo degsatay. - Hadda ma jiro wax safka ku jira oo soo degaya. - Waxaa laa daawan karaa khad la\'aan - Dooro dhammaan - Ha dooran dhammaan - Daaro muuqaallada taleefanka kuugu jira - Linkska dib ayaa loo cusboonaysiiyay - Magaca Repository-ka iyo URL-ka - Waa la koobiyeeyay! - Ogeysiiska xalqada cusub - Ka raadi extensions-ka kale - Muuji waxyaabaha laguu soo jeediyay - Daaro - Diiwaanka - Wuxuu ku darayaa inaad doorato xawaaraha muuqaalka - Iftiin dheeraad ah - Daar filter-ka iftiinka marka uu iftiinku 100% ka bato - Iftiinka dheeraadka ah waa shidan yahay - Inta jeer ee la kaydinayo - Kaydka - Talooyinka Raadinta - Soo bandhig talooyinka inta aad wax qorayso - Tirtir Talooyinka - Muuji xogta dulsaaran muuqaalka - Soo bandhig guddiga dhibidda - Dooro habka loo kala saarayo soo dejinta plugins-ka - Soo deji nooca hordhaca ah - Nooca hordhaca ah horey ayaa loo soo dejiyay. diff --git a/app/src/main/res/values-b+sv/strings.xml b/app/src/main/res/values-b+sv/strings.xml index d4ac26743..bf352e5e2 100644 --- a/app/src/main/res/values-b+sv/strings.xml +++ b/app/src/main/res/values-b+sv/strings.xml @@ -2,7 +2,8 @@ Betygsatt: %.1f Hastighet (%.2fx) - Ny uppdatering hittad! \n%1$s -> %2$s + Ny uppdatering hittad! +\n%1$s -> %2$s CloudStream Hem Sök @@ -116,7 +117,8 @@ Ta bort nerladdad fil Ta bort Avbryt - %s kommer att raderas permanent \nÄr du helt säker? + %s kommer att raderas permanent +\nÄr du helt säker? Pågående Färdig Status @@ -229,7 +231,8 @@ %1$d %2$s %1$s %2$d%3$s -30 - %dm \nåterstår + %dm +\nåterstår NSFW OVA Torrent @@ -422,7 +425,7 @@ Felsäkert läge på Starta om appen för att se ändringar. Intern spelare - CamHD + Kamera HD Tillägg nedladdad Lager URL eller Shortcode Börjat hämta %1$d %2$s… @@ -481,12 +484,16 @@ Visa community databaser Blandad inledning Skippa %s - CloudStream har inga webbplatser installerade som standard. Du måste installera webbplatser från arkiv. \n \nGå med i vår Discord eller sök online. + CloudStream har inga webbplatser installerade som standard. Du måste installera webbplatser från arkiv. +\n +\nGå med i vår Discord eller sök online. Välj bibliotek - Ditt bibliotek är tomt :( \nLogga in på ett bibliotekskonto eller lägg till program i ditt lokala bibliotek. + Ditt bibliotek är tomt :( +\nLogga in på ett bibliotekskonto eller lägg till program i ditt lokala bibliotek. Visa hoppa över popups för introduktion/eftertexter Ta bort från sett - Fil i säkertläge hittades! \nLaddar inte några tillägg vid start tills filen har tagits bort. + Fil i säkertläge hittades! +\nLaddar inte några tillägg vid start tills filen har tagits bort. Uppdaterar prenumererade program Prenumererad Prenumerera @@ -501,7 +508,7 @@ Ersätt alla Filler Förbikoppla ISP - CamRip + Kamera Kamera Alla tillägg stängdes av på grund av en krasch för att hjälpa dig hitta det tillägget som orsakar problem. Storlek @@ -551,9 +558,21 @@ Använd standard konto PIN-kod Sök mängden som används när spelaren är dold - Det verkar som om ett potentiellt duplicerat objekt redan finns i ditt bibliotek: \n \n\'%s.\' \n \nVill du lägga till det här objektet ändå, ersätta det befintliga eller avbryta åtgärden? - Det verkar som om ett potentiellt duplicerat objekt redan finns i ditt bibliotek: \'%s.\' \n \nVill du lägga till det här objektet ändå, ersätta det befintliga eller avbryta åtgärden? - Här kan du ändra hur källorna ska sorteras, om en video har högre prioritet visas den högre upp i källvalet. Summan av källprioriteten och kvalitetsprioriteten är videoprioriteten. \n \nKälla A: 3 \nKvalitet B: 7 \nKommer att ha en kombinerad videoprioritet på 10. \n \nOBS: Om summan är 10 eller mer kommer spelaren automatiskt att hoppa över laddningen när den länken laddas! + Det verkar som om ett potentiellt duplicerat objekt redan finns i ditt bibliotek: +\n +\n\'%s.\' +\n +\nVill du lägga till det här objektet ändå, ersätta det befintliga eller avbryta åtgärden? + Det verkar som om ett potentiellt duplicerat objekt redan finns i ditt bibliotek: \'%s.\' +\n +\nVill du lägga till det här objektet ändå, ersätta det befintliga eller avbryta åtgärden? + Här kan du ändra hur källorna ska sorteras, om en video har högre prioritet visas den högre upp i källvalet. Summan av källprioriteten och kvalitetsprioriteten är videoprioriteten. +\n +\nKälla A: 3 +\nKvalitet B: 7 +\nKommer att ha en kombinerad videoprioritet på 10. +\n +\nOBS: Om summan är 10 eller mer kommer spelaren automatiskt att hoppa över laddningen när den länken laddas! Avisering om nytt avsnitt Sök i andra tillägg Visa rekommendationer @@ -568,7 +587,8 @@ Efter några misslyckade försök stängs prompten. Starta bara om appen för att försöka igen. Favorit Ta bort från favoriter - %s \nkvarstår + %s +\nkvarstår kopierad! Lagringsnamn och URL För att säkerställa oavbrutna nedladdningar och aviseringar för prenumererade tv-program behöver CloudStream tillstånd att köras i bakgrunden. Genom att trycka på OK kommer du få en förfrågningsdialogruta. Tryck då på \'Tillåt\'.\n\nObservera att denna behörighet inte betyder att CS3 kommer att tömma ditt batteri. Den fungerar bara i bakgrunden när det behövs, till exempel när du tar emot aviseringar eller laddar ner videor från officiella tillägg. @@ -597,7 +617,9 @@ Autentisera lokalt QR-kodbild PIN-koden har upphört att gälla! - Är du säker på att du vill radera följande avsnitt permanent i %1$s? \n \n%2$s + Är du säker på att du vill radera följande avsnitt permanent i %1$s? +\n +\n%2$s Aktivera förhandsgranskningsminiatyr i sökfältet Förhandsgranskning av sökfältet Spela från början @@ -611,9 +633,15 @@ Avmarkera alla Radera Filer Radera (%1$d | %2$s) - Är du säker på att du vill ta bort följande objekt permanent? \n \n%s - Du kommer också permanent att radera alla avsnitt i följande serie: \n \n%s - Är du säker på att du permanent vill radera alla avsnitt i följande serie? \n \n%s + Är du säker på att du vill ta bort följande objekt permanent? +\n +\n%s + Du kommer också permanent att radera alla avsnitt i följande serie: +\n +\n%s + Är du säker på att du permanent vill radera alla avsnitt i följande serie? +\n +\n%s Ta bort insticksprogram Dölj namnen på spelarens kontroller Besök %s på din smartphone eller dator och ange koden ovan @@ -632,7 +660,7 @@ Aktivera torrent i Inställningar/Leverantörer/Föredragen media Avkodningsfel Ladda in första möjliga - Visa inte + Dölj Kantstorlek Egen Stöds ej @@ -722,7 +750,7 @@ Inget konto Dra uppåt igen för att gå över 100% Startar plugin uppdateringsprocessen! - %d plugin(s) har uppdaterats + %d plugin(s) har uppdaterats! Inga plugins blev uppdaterade. Spelarnotiser Spelarnotisen för att styra uppspelningen i bakgrunden @@ -732,17 +760,4 @@ Gör alla undertexter kursivstila Bakgrundsradie Visa spelarens metadata överlägg - Video - Förhandsvisning - Live - Visa realtidsklocka - Visa en realtidsklocka högst upp på skärmen. Gäller startsidan och spelaren - Profilinställningar - Dölj källor med negativ prioritet - Dölj källor med fel - - %d dold länk - %d dolda länkar - - Logcat 🐈 diff --git a/app/src/main/res/values-b+ta/strings.xml b/app/src/main/res/values-b+ta/strings.xml index 2537eba0c..fda0c36fd 100644 --- a/app/src/main/res/values-b+ta/strings.xml +++ b/app/src/main/res/values-b+ta/strings.xml @@ -358,7 +358,8 @@ முன்மாதிரி தளவமைப்பு பதிவிறக்கம் செய்யப்பட்ட கோப்பு பகுத்தல் - உங்கள் நூலகம் காலியாக உள்ளது :( \n நூலகக் கணக்கில் உள்நுழைக அல்லது உங்கள் உள்ளக நூலகத்தில் காட்சிகளைச் சேர்க்கவும். + உங்கள் நூலகம் காலியாக உள்ளது :( +\n நூலகக் கணக்கில் உள்நுழைக அல்லது உங்கள் உள்ளக நூலகத்தில் காட்சிகளைச் சேர்க்கவும். குழுவிலகவும் சுயவிவரங்கள் முள் 4 எழுத்துகளாக இருக்க வேண்டும் @@ -565,7 +566,8 @@ மதிப்பீடு (குறைந்த முதல் உயர் வரை) புதுப்பிக்கப்பட்டது (பழையது புதியது) இந்த பட்டியல் காலியாக உள்ளது. இன்னொரு இடத்திற்கு மாற முயற்சிக்கவும். - பாதுகாப்பான பயன்முறை கோப்பு கிடைத்தது! \n கோப்பு அகற்றப்படும் வரை தொடக்கத்தில் எந்த நீட்டிப்புகளையும் ஏற்றவில்லை. + பாதுகாப்பான பயன்முறை கோப்பு கிடைத்தது! +\n கோப்பு அகற்றப்படும் வரை தொடக்கத்தில் எந்த நீட்டிப்புகளையும் ஏற்றவில்லை. சந்தா காட்சிகளைப் புதுப்பித்தல் சந்தா எபிசோட் %d வெளியானது! @@ -609,7 +611,7 @@ பயன்பாட்டிலிருந்து வெளியேறுவதற்கு முன் உரையாடலைக் காட்டு உள்நாட்டில் அங்கீகரிக்கவும் தள்ளுபடி - உங்கள் ச்மார்ட்போன் அல்லது கணினியில் %s ஐப் பார்வையிட்டு மேலே உள்ள குறியீட்டை உள்ளிடவும் + உங்கள் ச்மார்ட்போன் அல்லது கணினியில் %s ஐப் பார்வையிட்டு மேலே உள்ள குறியீட்டை உள்ளிடவும் சாதன முள் குறியீட்டைப் பெற முடியாது, உள்ளக அங்கீகாரத்தை முயற்சிக்கவும் முள் குறியீடு இப்போது காலாவதியானது! விளிம்பு அளவு @@ -652,7 +654,7 @@ செருகுநிரல்களைப் புதுப்பிக்கவும் செருகுநிரல்களை கைமுறையாக புதுப்பிக்கவும் சொருகி புதுப்பிப்பு செயல்முறையைத் தொடங்குகிறது! - %d செருகுநிரல்(கள்) வெற்றிகரமாக புதுப்பிக்கப்பட்டது + வெற்றிகரமாக புதுப்பிக்கப்பட்டது %d சொருகி (கள்)! செருகுநிரல்கள் எதுவும் புதுப்பிக்கப்படவில்லை. பிளேயர் அறிவிப்புகள் பின்னணியில் இருந்து பின்னணியைக் கட்டுப்படுத்துவதற்கான பிளேயர் அறிவிப்பு @@ -731,17 +733,4 @@ %d பதிவிறக்கம் வரிசையில் உள்ளது %d பதிவிறக்கங்கள் வரிசையில் உள்ளன - பிளேயர் மேனிலை தரவு மேலடுக்கைக் காட்டு - ஒளிதோற்றம் - உண்மையான நேர கடிகாரத்தைக் காட்டு - திரையின் மேற்புறத்தில் நிகழ் நேர கடிகாரத்தைக் காட்டு. முகப்புப்பக்கம் மற்றும் பிளேயருக்குப் பொருந்தும் - முன்னோட்டம் - சுயவிவர அமைப்புகள் - எதிர்மறையான முன்னுரிமையுடன் ஆதாரங்களை மறை - பிழைகளுடன் ஆதாரங்களை மறைக்கவும் - - %d மறைக்கப்பட்ட இணைப்பு - %d மறைக்கப்பட்ட இணைப்புகள் - - வாழ்க diff --git a/app/src/main/res/values-b+tl/strings.xml b/app/src/main/res/values-b+tl/strings.xml index 4ed229ca7..4050ddbd7 100644 --- a/app/src/main/res/values-b+tl/strings.xml +++ b/app/src/main/res/values-b+tl/strings.xml @@ -15,7 +15,8 @@ Bilis (%.2fx) Rated: %.1f - Bagong update! \n%1$s -> %2$s + Bagong update! +\n%1$s -> %2$s CloudStream Home Maghanap @@ -137,7 +138,8 @@ Kanselahin I-pause I-resume - This will permanently delete %s \nAre you sure? + This will permanently delete %s +\nAre you sure? Patuloy Tapos na Katayuan diff --git a/app/src/main/res/values-b+tr/strings.xml b/app/src/main/res/values-b+tr/strings.xml index 17515a77b..fb0600c1a 100644 --- a/app/src/main/res/values-b+tr/strings.xml +++ b/app/src/main/res/values-b+tr/strings.xml @@ -19,7 +19,8 @@ Hız (%.2fx) Puan: %.1f - Yeni güncelleme bulundu! \n%1$s -> %2$s + Yeni güncelleme bulundu! +\n%1$s -> %2$s Dolgu %d dakika CloudStream @@ -186,7 +187,8 @@ Sürdür -30 +30 - %s tamamen silinecek \nEmin misiniz? + %s tamamen silinecek +\nEmin misiniz? %dd \nkaldı Devam ediyor Tamamlandı @@ -351,8 +353,8 @@ Rastgele Çok yakında… Cam - CamRip - CamHD + Cam + Cam HQ HD TS @@ -376,7 +378,7 @@ Geçersiz URL Hata Altyazılardan seçmeli altyazıyı (CC) kaldır - Altyazılardaki reklamları kaldır + Altyazılardaki şişkinliği kaldır Tercih edilen medya diline göre filtrele Ek içerikler Fragman @@ -458,10 +460,10 @@ Görünüm Özellikler Uygulama çıkışta güncellenecektir - Kapanışı - Açılışı + Bitiş + Açılış Hayır - Özeti + Özet Çıkmak istediğinize emin misin? Evet Uygulama güncellemesi indiriliyor… @@ -471,7 +473,7 @@ Eski Hareketler Tüm Diller - %s Atla + Geç %s İzlenenlerden kaldır Karışık son Karışık başlangıç @@ -483,8 +485,10 @@ Çok fazla metin. Panoya kaydedilemiyor. Kütüphane Tarayıcı - Kütüphaneniz boş :( \nBir kütüphane hesabında oturum açın veya yerel kütüphanenize programlar ekleyin. - Güvenli mod dosyası bulundu! \nDosya kaldırılana kadar başlangıçta herhangi bir uzantı yüklenmiyor. + Kütüphaneniz boş :( +\nBir kütüphane hesabında oturum açın veya yerel kütüphanenize programlar ekleyin. + Güvenli mod dosyası bulundu! +\nDosya kaldırılana kadar başlangıçta herhangi bir uzantı yüklenmiyor. Şuna Göre Sırala Sırala Güncellenme (Yeniden Eskiye) @@ -527,16 +531,26 @@ Mobil veri Varsayılanı ayarla Düzenle - Burada kaynakların nasıl sıralandığını değiştirebilirsiniz. Bir video daha yüksek bir önceliğe sahipse, kaynak seçiminde daha yüksek görünecektir. Kaynak önceliği ve kalite önceliğinin toplamı video önceliğidir. \n \nKaynak A: 3 \nKalite B: 7 \nBirleştirilmiş video önceliği 10 olacaktır. \n \nNOT: Toplam 10 veya daha fazlaysa, bu bağlantı yüklendiğinde oynatıcı otomatik olarak yüklemeyi atlayacaktır! + Burada kaynakların nasıl sıralandığını değiştirebilirsiniz. Bir video daha yüksek bir önceliğe sahipse, kaynak seçiminde daha yüksek görünecektir. Kaynak önceliği ve kalite önceliğinin toplamı video önceliğidir. +\n +\nKaynak A: 3 +\nKalite B: 7 +\nBirleştirilmiş video önceliği 10 olacaktır. +\n +\nNOT: Toplam 10 veya daha fazlaysa, bu bağlantı yüklendiğinde oynatıcı otomatik olarak yüklemeyi atlayacaktır! Kaliteler Profil arkaplanı - Kullanıcı arayüzü düzgün şekilde oluşturulamadı, bu BÜYÜK BİR HATADIR ve derhal bildirilmelidir %s + UI was unable to be created correctly, this is a MAJOR BUG and should be reported immediately %s Eklenti indirmeyi filtrelemek için mod seç %s favorilerden çıkarıldı Favoriler %s favorilere eklendi %s olarak giriş yapıldı - Kütüphanenizde potensiyel kopya ürünler bulundu: \n \n%s \n \nYine de ekleyerek var olanları değiştirmek mi istersiniz, yoksa iptal etmek mi? + Kütüphanenizde potensiyel kopya ürünler bulundu: +\n +\n%s +\n +\nYine de ekleyerek var olanları değiştirmek mi istersiniz, yoksa iptal etmek mi? %s için PIN girin Yedekleme sıklığı Potensiyel Kopya Bulundu @@ -559,7 +573,9 @@ Depo bulunamadı, bağlantı adresini kontrol edin veya VPN ile deneyin Zaten oyladınız Depoda eklenti bulunamadı - Görünüşe göre potansiyel bir kopya kütüphanenizde zaten bulunuyor: \'%s\' \n \nYine de ekleyerek var olanı değiştirmek mi istersiniz, yoksa iptal etmek mi? + Görünüşe göre potansiyel bir kopya kütüphanenizde zaten bulunuyor: \'%s\' +\n +\nYine de ekleyerek var olanı değiştirmek mi istersiniz, yoksa iptal etmek mi? PIN girin PIN Geçerli PIN\'i Giriniz @@ -621,12 +637,20 @@ Oynatıcı kontrolünün adlarını gizle Baştan Oynat Tümünü Seç - Aşağıdaki öğeleri kalıcı olarak silmek istediğinizden emin misiniz? \n \n%s - %1$s içindeki aşağıdaki bölümleri kalıcı olarak silmek istediğinizden emin misiniz? \n \n%2$s + Aşağıdaki öğeleri kalıcı olarak silmek istediğinizden emin misiniz? +\n +\n%s + %1$s içindeki aşağıdaki bölümleri kalıcı olarak silmek istediğinizden emin misiniz? +\n +\n%2$s Dosyaları Silin Sil (%1$d | %2$s) - Aşağıdaki dizideki tüm bölümleri kalıcı olarak silmek istediğinizden emin misiniz? \n \n%s - Ayrıca aşağıdaki dizideki tüm bölümleri kalıcı olarak sileceksiniz: \n \n%s + Aşağıdaki dizideki tüm bölümleri kalıcı olarak silmek istediğinizden emin misiniz? +\n +\n%s + Ayrıca aşağıdaki dizideki tüm bölümleri kalıcı olarak sileceksiniz: +\n +\n%s Silinecek Öğeleri Seçin Tüm Seçimi Kaldır Çevrimdışı izlemek için kullanılabilir @@ -661,7 +685,7 @@ Tarih %s Eklentileri Güncelle Eklenti güncellemesi başlıyor! - %d eklenti başarıyla güncellendi + %d eklenti başarıyla güncellendi! Hiçbir eklenti güncellenmedi. Eklentileri el ile güncelle Oynatıcı bildirimi arka planda oynatmasını kontrol etmek içindir @@ -746,21 +770,4 @@ Öncelikli kaynak Oynatıcıda video kaynaklarının nasıl sıralanacağını belirleyin Meta Verileri Katmanını Göster - Canlı - Ön Gösterim - Video - Gerçek zamanlı saati göster - Ekranın üst kısmında gerçek zamanlı bir saat göster. Ana sayfa ve oynatıcı için uygulanır - Profil ayarları - Eksi önceliğe sahip kaynakları gizle - Hatalı kaynakları gizle - - %d gizli bağlantı - %d gizli bağlantı - - Logcat 🐈 - Videoları duraklatılmış olarak başlat - Kaynak seçerken zorlanmayı azalt - Kenar - Arka plan diff --git a/app/src/main/res/values-b+uk/strings.xml b/app/src/main/res/values-b+uk/strings.xml index d8b27c2f1..2a9352183 100644 --- a/app/src/main/res/values-b+uk/strings.xml +++ b/app/src/main/res/values-b+uk/strings.xml @@ -1,21 +1,21 @@ Плакат - Постер епізоду - Завантаження скасовано - Змінити постачальника + Постер Епізоду + Завантаження Скасовано + Змінити Постачальника Назад Рейтинг: %.1f У ролях: %s - %d епізод вийде через + Епізод %d вийде через Плакат %1$s Еп %2$d - %1$d дн %2$d год %3$d хв - %1$d год %2$d хв - %d хв - Головний постер - Наступне випадкове - Попередній перегляд тла + %1$dд %2$dгод %3$dхв + %1$dгод %2$dхв + %dхв + Головний Постер + Наступний Випадковий + Попередній Перегляд Заднього Фону Швидкість (%.2fx) Знайдено нове оновлення!\n%1$s –> %2$s Пошук @@ -24,29 +24,29 @@ Налаштування Пошук… Пошук на %s… - Немає даних - Більше налаштувань + Немає Даних + Більше Опцій Наступний епізод Жанри - Відкрити в браузері - Пропустити завантаження + Відкрити в Браузері + Пропустити Завантаження Завантаження… Завершено - Заплановано - Покинуто - Відтворити фільм - Відтворити трейлер - Транслювати торрент + Планую Дивитися + Скинуто + Відтворити Фільм + Відтворити Трейлер + Транслювати Торрент Повторити з’єднання… Назад - Відтворити епізод + Відтворити Епізод Завантажено Завантаження - Завантаження завершено + Завантаження Завершено Дуб. Суб. - Видалити файл - Відновити завантаження + Видалити Файл + Відновити Завантаження Приховати Переглянути Подробиці @@ -57,26 +57,26 @@ Скопіювати Закрити Зберегти - Швидкість програвача - Колір вікна - Тип межі + Швидкість Плеєра + Колір Вікна + Тип Межі Шрифт - Розмір шрифту + Розмір Шрифту Пошук за постачальниками Пошук за типами - Жодного банана не надано - Автовибір мови - Завантажити мови - Мова субтитрів - Утримуйте, щоби скинути до типових + Жодного Benenes не надано + Авто-Вибір Мови + Завантажити Мови + Мова Субтитрів + Утримуйте, щоби скинути до типових налаштувань Імпортуйте шрифти, помістивши їх до %s - Продовжити перегляд + Продовжити Перегляд Вилучити Докладніше Цей постачальник є торентом, рекомендується використовувати VPN Опис - Сюжет не знайдено - Опис не знайдено + Сюжет Не Знайдено + Опис Не Знайдено Показати Logcat 🐈 Продовжувати відтворення в мініатюрному програвачі поверх інших застосунків Прибрати чорні смуги @@ -92,33 +92,33 @@ Філлер Відтворити в CloudStream Мережева трансляція - Переглядаю + Переглядання Поділитися Відкладено - Передивляюся + Переглядаю Повторно Завантажити - Відтворити трансляцію + Відтворити Трансляцію Джерела Субтитри - Внутрішнє сховище - Завантаження призупинено - Завантаження розпочато - Не вдалося завантажити - Оновлення розпочато - Помилка завантаження посилань - Призупинити завантаження - Переглянути файл + Внутрішнє Сховище + Завантаження Призупинено + Завантаження Розпочато + Завантаження не Вдалося + Оновлення Розпочато + Помилка Завантаження Посилань + Призупинити Завантаження + Переглянути Файл Докладніше - Фільтрувати закладки + Фільтрувати Закладки Очистити - Налаштування субтитрів - Колір тла - Висота субтитрів - Колір тексту - Колір обведення + Налаштування Субтитрів + Колір Тла + Висота Субтитрів + Колір Тексту + Колір Обведення Автовідтворення наступного епізоду Проведіть збоку в бік, щоби керувати часом відтворення у відео - Дано %d бананів розробникам + %d Benenes надано розробникам Кнопка зміни розміру програвача @string/home_play Для коректної роботи цього постачальника може знадобитися VPN @@ -138,12 +138,12 @@ Дані збережено Помилка резервного копіювання %s Пошук - Облікові записи та безпека - Оновлення та резервне копіювання + Облікові Записи та Безпека + Оновлення та Резервне Копіювання Подробиці - Розширений пошук + Розширений Пошук Показувати результати пошуку, розділені за постачальниками - Показувати філлери для аніме + Показувати наповнювачі для аніме Показувати трейлери Приховати вибрану якість відео у результатах пошуку Автозавантаження розширень @@ -153,25 +153,25 @@ Github Застосунок для ранобе від тих самих розробників Застосунок для аніме від тих самих розробників - Дати бананів розробникам - Мова застосунку + Дати benene розробникам + Мова Застосунку Цей постачальник не має підтримування Chromecast - Посилань не знайдено - Переглянути епізод + Посилань Не Знайдено + Переглянути Епізод Скинути до типових значень - Немає сезону + Немає Сезону Епізоди %1$d %2$s С Е - Видалити файл + Видалити Файл Видалити Скасувати Відновити -30 Це назавжди видалить %s\nВи впевнені? - %d хв\nзалишилося - Виходить + %dхв\nзалишилося + Триває Завершено Рейтинг Тривалість @@ -184,16 +184,16 @@ Телесеріали Мультфільми Аніме - ОВА - Азіатські драми - Прямі трансляції + OVA + Азіатські Драми + Прямі Трансляції Інші Серіал Мультфільм Аніме - Документалка - Азіатська драма - Пряма трансляція + Документальний Фільм + Азіатська Драма + Пряма Трансляція Відео Помилка джерела Віддалена помилка @@ -203,15 +203,15 @@ Переглянути в %s Автозавантаження Завантажити дзеркало - Перевірити наявність оновлень + Перевірити Наявність Оновлень Забл./Розбл. Пропустити ОП Не показувати знову Оновити - Бажана якість перегляду (Wi-Fi) + Бажана якість перегляду (WiFi) Заголовок Перемкнути елементи інтерфейсу на плакаті - Оновлення не знайдено + Оновлення Не Знайдено Натисніть двічі праворуч або ліворуч, щоби перемотати вперед або назад Використовувати системну яскравість у програвачі замість темного накладання Завантажено файл резервної копії @@ -222,10 +222,10 @@ Автооновлення розширень Автоматично встановлювати всі розширення, які ще не встановлено, з доданих сховищ. Автоматично перевіряти нові оновлення після запуску застосунку. - Посилання скопійовано до буфера обміну - Деякі пристрої не підтримують новий встановлювач пакетів. Спробуйте старий варіант, якщо оновлення не встановлюються. + Покликання скопійовано до буфера обміну + Деякі пристрої не підтримують новий інсталятор пакетів. Спробуйте старий варіант, якщо оновлення не встановлюються. Приєднуйтеся до Discord - Дано банани + Дано benene Рік +30 %1$s %2$d%3$s @@ -240,21 +240,21 @@ Стислий зміст Фільми Перезавантажити посилання - Документалки + Документальні Фільми NSFW Фільм - ОВА + OVA Торент Мітка якості NSFW Несподівана помилка програвача Помилка завантаження, перевірте дозвіл на зберігання - Епізод через Chromecast + Chromecast епізод Мітка субтитрів Джерело Завантажити субтитри Мітка дубляжу - Пропустити це оновлення + Пропустити це Оновлення Усе На весь екран Розтягнути @@ -275,9 +275,9 @@ Макет застосунку Бажані медіа Автоматично - Макет телевізора - Макет телефона - Макет емулятора + Телевізійна Обгортка + Телефона обгортка + Емуляторна обгортка Основний колір Тема застосунку Розташування назви плаката @@ -296,7 +296,7 @@ %d / 10 /%d %s автентифіковано - Не вдалося ввійти в %s + Не вдалося увійти в %s Нічого Звичайний Мін. @@ -319,9 +319,9 @@ HDR SDR Web - Зображення плаката + Зображення Плаката Програвач - Роздільність та заголовок + Роздільна здатність та заголовок Недійсний ID Недійсна URL-адреса Резервне копіювання @@ -341,15 +341,15 @@ DNS через HTTPS Шлях завантаження Додайте двійника наявного сайту, з іншою URL-адресою - Показувати аніме з дубляжем / із субтитрами + Показувати Дубльоване/З Субтитрами Аніме Застереження Розширення Дії 127.0.0.1 Макет Кодування субтитрів - Увімкнути NSFW у підтримуваних розширеннях - Макет + Увімкнути NSFW вміст на підтримуваних Розширеннях + Обгортка Постачальники https://example.com %1$s %2$s @@ -375,20 +375,20 @@ Фільтрувати за бажаною мовою медіа 4K Заголовок - Роздільність + Роздільна здатність Помилка Трейлер Додатково https://example.com/example.mp4 Cam - CamRip - CamHD + Cam + Cam HQ HD TS TC - Вилучати рекламу з субтитрів - Джерело переходу (необов’язково) + Вилучати роздуття субтитрів + Referer (необов’язково) Далі Переглядайте відео на цих мовах Пропустити налаштування @@ -396,7 +396,7 @@ Готово Розширення Додати репозиторій - Назва репозиторію (необов’язково) + Назва репозиторію (Опціонально) URL-адреса репозиторію або короткий код Розширення завантажено Розширення завантажено @@ -432,7 +432,7 @@ Список відтворення HLS Вбудований програвач Ендінґ - Підсумок + Коротке повторення Пропустити %s Змішаний ендінґ Подяки @@ -449,10 +449,10 @@ Установлення оновлення застосунку… Не вдалося встановити нову версію застосунку Застарілий - Встановлювач пакунків + Встановлювач Пакунків Застосунок буде оновлено після виходу Це також призведе до видалення всіх розширень сховища - Усі мови + Усі Мови Назад Змініть вигляд застосунку відповідно до вашого пристрою Розширення видалено @@ -467,26 +467,26 @@ Застосунок не знайдено Змішаний опенінґ Вилучити з переглянутого - Оновленням (від старого до нового) - Оновленням (від нового до старого) + Оновленням (від Старого до Нового) + Оновленням (від Нового до Старого) Бібліотека Сортувати - Рейтингом (від високого до низького) + Рейтингом (від Високого до Низького) Сортувати за Алфавітом (від А до Я) - Рейтингом (від низького до високого) + Рейтингом (від Низького до Високого) Ваша бібліотека порожня :(\nУвійдіть в обліковий запис бібліотеки або додайте щось до вашої локальної бібліотеки. Алфавітом (від Я до А) - Оберіть бібліотеку + Оберіть Бібліотеку Відкрити з Браузер Цей список порожній. Спробуйте перейти до іншого. Файл безпечного режиму знайдено!\nРозширення не завантажуватимуться під час запуску, доки файл не буде видалено. Android TV - Прогрвач приховано: крок перемотування - Програвач показано: крок перемотування + Прогрвач Приховано – Крок Перемотування + Програвач Показано – Крок Перемотування Крок перемотування, який використовується, коли програвач видимий - Крок перемотування, який використовується, коли програвач прихований + Крок перемотування, який використовується, коли плеєр прихований Провалено Пройдено Перезапустити @@ -500,10 +500,10 @@ Ви відписалися від %s Епізод %d випущено! Повернути - Проксі GitHub + GitHub Проксі Не вдалось отримати доступ до GitHub. Увімкнення проксі-сервера jsDelivr… Обходи ISP - Обхід блокування прямих посилань GitHub за допомогою jsDelivr. Може спричинити затримку оновлень на кілька днів. + Обхід блокування чистих gitHub URLs за допомогою jsDelivr. Можлива затримка оновлень на кілька днів. Бажана якість перегляду (мобільні дані) Встановити типові Профілі @@ -527,7 +527,7 @@ Вподобані %s додано до вподобаних У вашій бібліотеці виявлено можливі дублікати:\n\n%s\n\nУсе одно хочете додати цей елемент, замінити наявні чи скасувати дію? - Знайдено можливий дублікат + Знайдено Можливий Дублікат Заблокувати профіль Додати до обраного Замінити все @@ -538,32 +538,32 @@ Додати Підписатися Вилучити з обраного - Оберіть обліковий запис + Оберіть Обліковий Запис Схоже, що у вашій бібліотеці вже є можливий дублікат: \'%s.\'\n\nУсе одно хочете додати цей елемент, замінити наявний чи скасувати дію? Уведіть PIN-код PIN-код Уведіть поточний PIN-код Увійшли як %s Уведіть PIN-код для %s - Використовувати типовий обліковий запис + Використовувати Типовий Обліковий Запис Пропускати вибір облікового запису під час запуску - Керувати обліковими записами + Керувати Обліковими Записами Редагувати обліковий запис Показувати кнопку перемикання орієнтації екрана Обернути - Посилання перезавантажені + Посилання Перезавантажені Автообертання Увімкнути автоматичну зміну орієнтації екрана відповідно до відео Додати налаштування швидкості до програвача - Перевірити всі розширення + Перевірити всі Розширення Пошук в інших розширеннях Показати рекомендації - Ця перевірка лише для розробників і не підтверджує або заперечує роботу жодного розширення. + Ця Перевірка лише для розробників і не підтверджує або заперечує роботу жодного розширення. Сповіщення про новий епізод - Автентифікація за паролем / PIN-кодом + Автентифікація за Паролем/PIN-кодом Розблокуйте CloudStream - Біометричне блокування - Розблоковуйте застосунок за допомогою відбитка пальця, Face ID, PIN-коду, графічного ключа або пароля. + Біометричне Блокування + Розблоковуйте застосунок за допомогою відбитка пальця, Face ID, PIN-коду, Графічного Ключа або Пароля. Щойно було виконано резервне копіювання даних CloudStream. Хоча ймовірність цього вкрай мала, усі пристрої можуть поводитися по-різному. У рідкісних випадках, якщо ви втратите доступ до застосунку, повністю очистіть дані застосунку та відновіть їх із резервної копії. Просимо вибачення за будь-які незручності, що можуть виникнути. Біометрична автентифікація не підтримується на цьому пристрої Після кількох невдалих спроб вікно запиту зникне. Перезапустіть застосунок, щоби спробувати ще раз. @@ -572,11 +572,11 @@ Додати до вподобаного скопійовано! Назва репозиторію та URL - Помилка копіювання, скопіюйте logcat та зверніться до служби підтримки застосунку. - Помилка доступу до буфера обміну, спробуйте ще раз. - Гаразд + Помилка копіювання, Будь-ласка скопіюйте logcat та зверніться до служби підтримки застосунку. + Помилка доступу до буфера обміну, Будь-ласка спробуйте ще раз. + OK Вимкнути оптимізацію батареї - Щоби забезпечити безперервне завантаження та сповіщення про підписані телепередачі, CloudStream потребує дозволу на роботу у фоновому режимі. Натиснувши «Гаразд», ви побачите діалогове вікно запиту. Натисніть «Дозволити».\n\nЗверніть увагу, що цей дозвіл не означає, що CS3 розряджатиме ваш акумулятор. Він працюватиме у фоновому режимі лише за необхідності, наприклад, під час отримання сповіщень або завантаження відео з офіційних розширень. + Щоби забезпечити безперервне завантаження та сповіщення про підписані телепередачі, CloudStream потребує дозволу на роботу у фоновому режимі. Натиснувши OK, ви побачите діалогове вікно запиту. Натисніть \'Дозволити\'.\n\nЗверніть увагу, що цей дозвіл не означає, що CS3 розряджатиме ваш акумулятор. Він працюватиме у фоновому режимі лише за необхідності, наприклад, під час отримання сповіщень або завантаження відео з офіційних розширень. Споживання батареї застосунком уже змінено на необмежене Не вдається відкрити подробиці про застосунок CloudStream. Аудіокнига @@ -584,7 +584,7 @@ Медіа Скинути Наступний через %s - %1$d сезон %2$d епізод вийде через + Сезон %1$d Епізод %2$d вийде через Оберіть пристрій для трансляції Трансляція через дзеркало CloudStream Wiki @@ -592,71 +592,71 @@ Облікові записи Зображення QR-коду Відкрити сховище - Відвідайте %s на своєму смартфоні або комп’ютері та введіть вищевказаний код - Не вдається отримати PIN-код пристрою, спробуйте локальну автентифікацію - Термін дії PIN-коду закінчився! - Термін дії коду закінчується через %1$d хв %2$d с - Локальна автентифікація + Відвідайте %s на своєму смартфоні або комп\'ютері та введіть вищевказаний код + Не вдається отримати PIN-код пристрою, спробуйте локальну аутентифікацію + PIN-код зараз закінчився ! + Термін дії коду закінчується через %1$dхв %2$dс + Локальна Аутентифікація Відхилити - Відтворити з початку + Відтворити з Початку Попередження Видалити розширення Наразі завантажень немає. Приховати назви елементів керування в програвачі Відкрити локальне відео - Датою випуску (від нових до старих) - Датою випуску (від старих до нових) - Оберіть елементи для видалення - Обрати все - Зняти виділення + Датою випуску (від Нових до Старих) + Датою випуску (від Старих до Нових) + Оберіть Елементи для Видалення + Обрати Все + Зняти Вибір Всіх Видалити (%1$d | %2$s) Ви впевнені, що хочете назавжди видалити такі епізоди в %1$s?\n\n%2$s Ви також назавжди видалите всі епізоди в такому серіалі:\n\n%s - Доступно для перегляду поза мережею - Видалити файли + Доступно для перегляду в оффлайн режимі + Видалити Файли Ви впевнені, що хочете назавжди видалити такі елементи?\n\n%s Ви впевнені, що хочете назавжди видалити всі епізоди в такому серіалі?\n\n%s Попередній перегляд на шкалі перегляду Увімкнути мініатюру попереднього перегляду на шкалі перегляду Субтитри ще не завантажено Підтвердіть перед виходом - Показати + Відобразити Показувати діалог перед виходом із застосунку - Не показувати + Не відображати Розташування теки для резервних копій Власний - Це відео – торрент, це означає, що ваша відеоактивність може відстежуватися.\nПереконайтеся, що розумієте, що таке торрент, перед тим як продовжити. + Це відео – Торрент, це означає, що ваша відео діяльність може відстежуватися.\nПереконайтеся, що розумієте, що таке Торрент, перед тим як продовжити. Розмір обведення Аудіо Подкаст Непідтримувана помилка Помилка кодування Завантажити перший доступний - Увімкніть торент в Налаштування - > Постачальники - > Бажані медіа - Перезапустіть застосунок та прийміть спливне вікно «Транслювати торрент», щоби продовжити. + Увімкніть торент в Налаштування/Постачальники/Бажані медіа + Перезапустіть застосунок та прийміть спливне вікно Stream Torrent, щоби продовжити. Програмне декодування Програмне декодування дозволяє плеєру відтворювати відеофайли, які не підтримуються вашим пристроєм, але може спричинити затримки або нестабільне відтворення у високій роздільній здатності. - Датою виходу (найновіша) - Епізодом (за зростанням) - Рейтингом (найнижчий) + Датою виходу (Найновіша) + Епізодом (За Зростанням) + Рейтингом (Найнижчий) Рейтинг %s - Епізодом (за спаданням) - Рейтингом (найвищий) - Датою виходу (найстаріша) + Епізодом (за Спаданням) + Рейтингом (Найвищий) + Датою виходу (Найстаріша) Еп. %s Дата %s - Оновити розширення - Успішно оновлено %d розширення(-ь) + Оновити Розширення + Успішно оновлено %d розширення(-ь)! Оновити розширення вручну Починається оновлення розширень! Не оновлено жодного розширення. Сповіщення програвача для керування відтворенням у фоновому режимі Сповіщення програвача Розпізнавання мовлення недоступне - Почніть говорити… + Почніть Говорити… Вбудовані Мережеві - Радіус тла + Радіус Тла Зробити всі субтитри потовщеним Зробити всі субтитри курсивом Гучність перевищила 100% @@ -664,8 +664,8 @@ Одночасних з’єднань Змінює межі екрана Обрізання зображення - Перейти до завантажень - Немає з’єднання з мережею. \n\nПерепід’єднайтеся до мережі та спробуйте ще раз або перегляньте завантажені відео локально. + Перейти до Завантажень + Немає підключення до Інтернету.\n\nБудь ласка, підключіться до Інтернету та спробуйте ще раз або перегляньте завантажені відео офлайн. Скільки різних елементів можна завантажити паралельно Паралельних завантажень Скільки одночасних з’єднань може використовувати кожне завантаження @@ -674,46 +674,46 @@ Завжди запитувати Змінювати швидкість при утриманні Утримуйте, щоб отримати 2-кратну швидкість - %1$d год %2$d хв %3$d с - %1$d хв %2$d с - %1$d с + %1$dгод %2$dхв %3$dс + %1$dхв %2$dс + %1$dс Мітка рейтингу Немає облікового запису - Редагувати зображення профілю - Введіть URL-адресу зображення профілю - URL-адресу не знайдено - Недійсна URL-адреса або зображення - Зображення успішно оновлено + Редагувати Зображення Профілю + Введіть URL-адресу Зображення Профілю + URL-адресу Не Знайдено + Недійсна URL-адреса або Зображення + Зображення Успішно Оновлено Позначити як переглянуте до цього епізоду Вилучити переглянуті до цього епізоду Перезавантажено - Перезавантажити постачальника + Перезавантажити Постачальника Послуг Переглянути в дзеркалі" Назва - Роздільність та назва - Вирівнювання субтитрів + Роздільна здатність та назва + Вирівнювання Субтитрів Внизу ліворуч Внизу по центру Внизу праворуч - Посередині зліва - Посередині в центрі - Посередині справа + Середній лівий + Середній центр + Середній правий Угорі ліворуч - Угорі в центрі + Верхній центр Угорі праворуч - Відтворити весь серіал - Встановити передрелізну версію - Передрелізна версія вже встановлена. - Не вдалося встановити передрелізну версію. - Текст епізоду - Пропозиції пошуку + Відтворити Повні Серії + Встановити перед-релізну версію + Перед-релізна версія вже встановлена. + Не вдалося встановити перед-релізну версію. + Текст Епізоду + Пропозиції Пошуку Показувати підказки пошуку під час введення тексту - Очистити пропозиції + Очистити Пропозиції Додаткова яскравість Увімкнути фільтр яскравості при перевищенні 100% яскравості дисплея extra_brightness_enabled Показати панель трансляції - Інформація про медіа + Інформація Про Медіа Назва джерела Черга завантаження Наразі немає завантажень у черзі. @@ -735,40 +735,5 @@ Пріоритетне джерело Виберіть спосіб сортування джерел відео у програвачі - Показувати накладання метаданих програвача - Відео - Передперегляд - Наживо - Показувати поточний час - Показувати поточний час у верхній частині екрана (на головній та у програвачі) - Налаштування профілю - Приховати джерела з від’ємним пріоритетом - Приховати джерела з помилками - - %d приховане посилання - %d приховані посилання - %d прихованих посилань - %d прихованих посилань - - Logcat 🐈 - Запускати відео на паузі - Зменште стрес під час вибору джерел - Край - Тло - Стискач - Стискач динамічного діапазону - Показувати керування стискача у програвачі (допомагає збалансувати гучні й тихі звуки) - Увімк - Увімк · %1$d дБ / +%2$d дБ - Вимк - Легкий - Підсилення діалогів - Важкий - Поріг: %d дБ - Компенсація підсилення: +%d дБ - Звуки гучністю понад %d дБ приглушуються - Збільшує загальну гучність на +%d дБ - URL - Вибір зображення - Типові зображення + Показувати Накладання Метаданих Програвача diff --git a/app/src/main/res/values-b+ur/strings.xml b/app/src/main/res/values-b+ur/strings.xml index 94728e2f8..9a72e586f 100644 --- a/app/src/main/res/values-b+ur/strings.xml +++ b/app/src/main/res/values-b+ur/strings.xml @@ -10,7 +10,8 @@ ذریعہ تبدیل کریں پس منظر کا دیکھنا درجہ بندی: %.1f - نیا update آگیا ہے! \n%1$s -> %2$s + نیا update آگیا ہے! +\n%1$s -> %2$s بھرنے والا %d منٹ %1$d دن %2$d گھنٹے %3$d منٹ @@ -188,8 +189,10 @@ از سر نو شروع کریں -30 +30 - یہ مستقل طور پر حذف ہوجائے گا %s \nتمھيں يقين ہے? - %dm \nباقی + یہ مستقل طور پر حذف ہوجائے گا %s +\nتمھيں يقين ہے? + %dm +\nباقی احوال مکمل حالت @@ -438,7 +441,8 @@ پلیئردکھایا گیا - Seek Amount پلیئر کے نظر آنے پر استعمال کی جانے والی Seek Amount پلیئر کے چھپنے پر استعمال ہونے والی seek amount - سیف موڈ فائل مل گئی! \nفائل کو ہٹانے تک اسٹارٹ اپ پر کوئی ایکسٹینشن لوڈ نہیں کرنا۔ + سیف موڈ فائل مل گئی! +\nفائل کو ہٹانے تک اسٹارٹ اپ پر کوئی ایکسٹینشن لوڈ نہیں کرنا۔ شروع کریں ناکام کامیاب ہو گیا @@ -450,7 +454,8 @@ آئی ایس پی بائی پاسز %s کو سبسکرائب کیا Bypass blocking of raw github URLs using jsDelivr. اپ ڈیٹس میں کچھ دنوں کی تاخیر ہو سکتی ہے - آپ کی لائبریری خالی ہے:( \nلائبریری اکاؤنٹ میں لاگ ان کریں یا اپنی مقامی لائبریری میں شوز شامل کریں۔ + آپ کی لائبریری خالی ہے:( +\nلائبریری اکاؤنٹ میں لاگ ان کریں یا اپنی مقامی لائبریری میں شوز شامل کریں۔ غلط URL براؤزر ویب @@ -499,7 +504,9 @@ سب ٹائٹلز سے بند کیپشنز کو ہٹا دیں اپنے آلے کے مطابق ایپ کی شکل تبدیل کریں اگلے - CloudStream میں بذریعہ ڈیفالٹ کوئی سائٹ انسٹال نہیں ہے۔ آپ کو ریپوزٹری سے سائٹس انسٹال کرنے کی ضرورت ہے۔ \n \nہمارے Discord میں شامل ہوں یا آن لائن تلاش کریں۔ + CloudStream میں بذریعہ ڈیفالٹ کوئی سائٹ انسٹال نہیں ہے۔ آپ کو ریپوزٹری سے سائٹس انسٹال کرنے کی ضرورت ہے۔ +\n +\nہمارے Discord میں شامل ہوں یا آن لائن تلاش کریں۔ تمام ایکسٹینشنز کریش کی وجہ سے آف کر دی گئیں تاکہ آپ کو پریشانی کا باعث تلاش کرنے میں مدد مل سکے۔ پہلے ایکسٹینشن انسٹال کریں بہت زیادہ متن۔ کلپ بورڈ میں محفوظ کرنے سے قاصر۔ @@ -521,7 +528,13 @@ آپ نے پہلے ہی ووٹ دیا ہے مخزن میں کوئی پلگ انز نہیں ملا ترجیحی تعین کریں - یہاں آپ تبدیلی کرسکتے ہیں کہ سورسز کو کس طرح کی ترتیب دی جائے۔ اگر ایک ویڈیو کی زیادہ پرائیورٹی ہوتی ہے تو یہ سورس کی انتخاب میں زیادہ اوپر آئے گی۔ سورس کی پرائیورٹی اور کوالٹی کی پرائیورٹی کا مجموعہ ویڈیو کی پرائیورٹی ہوتی ہے۔ \n \nسورس A: 3 \nکوالٹی B: 7 \nاس کا مجموعی ویڈیو پرائیورٹی 10 ہوتی ہے۔ \n \nنوٹ: اگر مجموعہ 10 یا اس سے زیادہ ہو تو پلیر وہ لنک لوڈ کرنے کو خود بخود چھوڑ دے گا! + یہاں آپ تبدیلی کرسکتے ہیں کہ سورسز کو کس طرح کی ترتیب دی جائے۔ اگر ایک ویڈیو کی زیادہ پرائیورٹی ہوتی ہے تو یہ سورس کی انتخاب میں زیادہ اوپر آئے گی۔ سورس کی پرائیورٹی اور کوالٹی کی پرائیورٹی کا مجموعہ ویڈیو کی پرائیورٹی ہوتی ہے۔ +\n +\nسورس A: 3 +\nکوالٹی B: 7 +\nاس کا مجموعی ویڈیو پرائیورٹی 10 ہوتی ہے۔ +\n +\nنوٹ: اگر مجموعہ 10 یا اس سے زیادہ ہو تو پلیر وہ لنک لوڈ کرنے کو خود بخود چھوڑ دے گا! پسندیدہ %s کو پسندیدہ میں شامل کیا گیا %s کو پسندیدہ سے ختم کیا گیا @@ -548,8 +561,14 @@ جمع کریں سب کو بدل دیں بدل دیں - آپ کی لائبریری میں ممکنہ ڈپلیکیٹ آئٹمز مل گئے ہیں: \n \n%s \n \nکیا آپ بہرحال اس آئٹم کو شامل کرنا چاہیں گے، موجودہ کو تبدیل کرنا چاہیں گے، یا کارروائی کو منسوخ کریں گے؟ - ایسا معلوم ہوتا ہے کہ آپ کی لائبریری میں ممکنہ طور پر ڈپلیکیٹ آئٹم پہلے سے موجود ہے: \'%s۔\' \n \nکیا آپ بہرحال اس آئٹم کو شامل کرنا چاہیں گے، موجودہ کو تبدیل کرنا چاہیں گے، یا کارروائی کو منسوخ کریں گے؟ + آپ کی لائبریری میں ممکنہ ڈپلیکیٹ آئٹمز مل گئے ہیں: +\n +\n%s +\n +\nکیا آپ بہرحال اس آئٹم کو شامل کرنا چاہیں گے، موجودہ کو تبدیل کرنا چاہیں گے، یا کارروائی کو منسوخ کریں گے؟ + ایسا معلوم ہوتا ہے کہ آپ کی لائبریری میں ممکنہ طور پر ڈپلیکیٹ آئٹم پہلے سے موجود ہے: \'%s۔\' +\n +\nکیا آپ بہرحال اس آئٹم کو شامل کرنا چاہیں گے، موجودہ کو تبدیل کرنا چاہیں گے، یا کارروائی کو منسوخ کریں گے؟ پروفائل لاک کریں آغاز پر اکاؤنٹ کا انتخاب چھوڑ دیں ویڈیو واقفیت کی بنیاد پر اسکرین کی سمت بندی کی خودکار سوئچنگ کو فعال کریں @@ -557,7 +576,8 @@ کاپی کر لیا! ذخیرہ کا نام اور URL %s میں آنے والا ہے - %s \nباقی + %s +\nباقی کلپ بورڈ تک رسائی میں خرابی، براہ کرم دوبارہ کوشش کریں۔ کاپی کرنے میں خرابی، براہ کرم logcat کاپی کریں اور ایپ سپورٹ سے رابطہ کریں۔ سبسکرائب شدہ ٹی وی شوز کے لیے بلاتعطل ڈاؤن لوڈز اور اطلاعات کو یقینی بنانے کے لیے، CloudStream کو پس منظر میں چلنے کی اجازت درکار ہے۔ OK دبانے سے، آپ کو App info پر بھیج دیا جائے گا۔ وہاں اسکرول کریں 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 اور battery usage کو 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙 کردیں، اس اجازت کا مطلب یہ نہیں ہے کہ CS3 آپ کی بیٹری ختم کردے گا۔ یہ صرف ضرورت پڑنے پر پس منظر میں کام کرے گا، جیسے کہ جب اطلاعات موصول ہوں یا آفیشل ایکسٹینشنز سے ویڈیوز ڈاؤن لوڈ کریں۔ اگر آپ منسوخ کرنے کا انتخاب کرتے ہیں، تو آپ اس ترتیب کو بعد میں 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨 میں ایڈجسٹ کر سکتے ہیں۔ diff --git a/app/src/main/res/values-b+vi/strings.xml b/app/src/main/res/values-b+vi/strings.xml index 4ff76b1d2..c0f4744fd 100644 --- a/app/src/main/res/values-b+vi/strings.xml +++ b/app/src/main/res/values-b+vi/strings.xml @@ -14,21 +14,22 @@ Poster tập phim Poster chính Tập tiếp theo ngẫu nhiên - Thoát + Quay lại Thay đổi Nguồn phim Xem trước hình nền Tốc độ (%.2fx) Đánh giá: %.1f - Đã tìm thấy bản cập nhật mới!\n%1$s -> %2$s + Đã tìm thấy bản cập nhật mới! +\n%1$s -> %2$s Bộ lọc %d phút CloudStream Phát bằng CloudStream - Trang chủ - Tìm kiếm + Trang Chủ + Tìm Kiếm Tải xuống - Cài đặt + Cài Đặt Tìm kiếm… Tìm kiếm %s… Không có dữ liệu @@ -36,7 +37,7 @@ Tập tiếp theo Thể loại Chia sẻ - Phát bằng Trình duyệt + Mở bằng trình duyệt Bỏ tải Đang tải… Đang xem @@ -46,9 +47,9 @@ Xem sau Xem lại Phát - Phát Trực tiếp + Phát Livestream Phát Torrent - Nguồn + Nguồn phim Phụ đề Thử kết nối lại… Quay lại @@ -57,7 +58,7 @@ Tải xuống Đã tải xuống Đang tải xuống - Tải xuống đã tạm dừng + Đã tạm dừng tải xuống Tải xuống đã bắt đầu Tải xuống thất bại Tải xuống đã hủy @@ -87,7 +88,7 @@ Tốc độ phát Cài đặt phụ đề Màu chữ - Màu viền + Màu viền chữ Màu nền Màu cửa sổ Kiểu viền @@ -101,7 +102,7 @@ Tự động chọn ngôn ngữ Ngôn ngữ tải xuống Ngôn ngữ phụ đề - Nhấn giữ để đặt lại mặc định + Nhấn giữ để đặt lại về mặc định Thêm phông chữ tại %s Tiếp tục xem Xóa @@ -128,10 +129,10 @@ Vuốt lên hoặc xuống cạnh trái hoặc phải để điều chỉnh độ sáng hoặc âm lượng Tự động phát tập tiếp theo Phát tập tiếp theo sau khi hết tập hiện tại - Nhấn hai lần để tua - Nhấn hai lần để tạm dừng + Nhấn 2 lần để tua + Nhấn 2 lần để tạm dừng Thời lượng tua (Giây) - Nhấn hai lần vào cạnh trái hoặc phải để tua về trước hoặc sau + Nhấn 2 lần vào cạnh trái hoặc phải để tua về trước hoặc sau Nhấn vào giữa hai lần để tạm dừng Sử dụng độ sáng hệ thống Dùng độ sáng hệ thống thay cho lớp phủ tối trong trình phát ứng dụng @@ -166,7 +167,7 @@ Ngôn ngữ ứng dụng Nguồn phim này chưa hỗ trợ Chromecast Không tìm thấy liên kết - Đã sao chép liên kết vào bảng nhớ tạm + Đã sao chép liên kết vào bộ nhớ tạm Phát Tập phim Đặt lại giá trị mặc định Mùa @@ -185,7 +186,8 @@ Tiếp tục -30 +30 - %s sẽ bị xoá vĩnh viễn \nBạn có chắc chắn không? + %s sẽ bị xoá vĩnh viễn +\nBạn có chắc chắn muốn xóa? %d phút\ncòn lại Đang chiếu Hoàn thành @@ -228,14 +230,14 @@ Lỗi nguồn phim Lỗi nguồn từ xa Lỗi kết xuất - Lỗi trình phát bất ngờ + Đã có lỗi xảy ra. Vui lòng thử lại sau Lỗi tải xuống. Hãy kiểm tra quyền truy cập bộ nhớ - Truyền tập phim - Truyền nguồn thay thế + Phát tập phim bằng Chromecast + Phản chiếu màn hình bằng Chromecast Phát bằng ứng dụng Phát bằng %s Tự động tải xuống - Tải xuống nguồn thay thế + Nguồn tải dự phòng Tải lại các liên kết Tải xuống phụ đề Nhãn chất lượng phim @@ -248,20 +250,20 @@ Khóa Thu phóng Nguồn - Bỏ qua Giới thiệu + Bỏ qua giới thiệu Không hiện lại Bỏ qua bản cập nhật này Cập nhật Chất lượng xem ưu tiên (WiFi) Số ký tự tối đa tiêu đề trình phát - Hiển thị thông tin trình phát - Kích thước bộ đệm video - Thời lượng bộ đệm video + Hiện thông tin trình phát + Kích thước bộ nhớ đệm video + Thời lượng bộ nhớ đệm Bộ nhớ đệm video trên thiết bị Xoá bộ nhớ đệm hình ảnh và video Sẽ gây lỗi nếu đặt quá cao trên thiết bị có bộ nhớ thấp như Android TV. - Sẽ gây lỗi nếu đặt quá cao trên thiết bị có dung lượng lưu trữ thấp như Android TV. - DNS qua HTTPS + Sẽ gây lỗi nếu đặt quá cao trên máy có dung lượng lưu trữ thấp như Android TV. + DNS over HTTPS Rất hữu ích để bỏ chặn ISP Bản sao trang web Xoá trang web @@ -293,7 +295,7 @@ Đặt tên phim dưới poster Mật khẩu - Tên người dùng + Tài khoản Email 127.0.0.1 Tên trang mới @@ -313,10 +315,10 @@ %d / 10 /?? /%d - %s đã xác thực - Không thể đăng nhập tại %s + Đã xác thực %s + Không thể xác thực %s - Không có + Mặc định Bình thường Tất cả Tối đa @@ -337,7 +339,7 @@ https://en.wikipedia.org/w/index.php?title=Pangram&oldid=225849300 https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog --> - Phụ đề của bạn sẽ trông tương tự như thế này + Bạch kim rất quý nên sẽ dùng để lắp vô xương Được đề xuất Đã tải %s Chọn từ tệp @@ -345,13 +347,13 @@ Tệp đã tải xuống Vai chính Vai phụ - Diễn viên quần chúng + Lý lịch Nguồn Ngẫu nhiên Sắp có… Cam - CamRip - Bản HD(Đẹp) + Cam + Cam HQ HD TS @@ -375,7 +377,7 @@ Lỗi đường dẫn Lỗi Xóa bỏ chú thích chi tiết trong phụ đề - Xóa quảng cáo trong phụ đề + Xóa bỏ nội dung thừa trong phụ đề Lọc theo ngôn ngữ ưu tiên Thêm Trailer @@ -389,9 +391,9 @@ Bạn muốn xem gì Hoàn tất Tiện ích mở rộng - Thêm kho nguồn phim - Tên kho nguồn phim (Tùy chọn) - URL kho nguồn phim hoặc Mã ngắn + Thêm kho tiện ích + Tên kho tiện ích (Tùy chọn) + URL kho tiện ích hoặc Mã ngắn Tiện ích mở rộng đã tải Tiện ích mở rộng đã xoá Không tải được %s @@ -402,22 +404,24 @@ Tải xuống hàng loạt tiện ích mở rộng tiện ích mở rộng - Việc này sẽ xóa tất cả tiện ích mở rộng trong kho nguồn phim - Xoá kho nguồn phim + Việc này sẽ xóa tất cả tiện ích mở rộng trong kho tiện ích + Xoá kho tiện ích Tải xuống danh sách các trang web bạn muốn sử dụng Đã tải xuống: %d Đã vô hiệu: %d Chưa tải xuống: %d - CloudStream không có sẵn trang web nào. Bạn cần cài đặt các trang web từ kho lưu trữ. \n \nHãy tham gia Discord của chúng tôi hoặc tìm kiếm trực tuyến. - Xem các kho nguồn phim của cộng đồng + CloudStream không có sẵn trang web nào. Bạn cần cài đặt các trang web từ kho lưu trữ. +\n +\nHãy tham gia Discord của chúng tôi hoặc tìm kiếm trực tuyến. + Xem kho lưu trữ của cộng đồng Danh sách công khai In hoa toàn bộ phụ đề Cảnh báo: CloudStream không chịu trách nhiệm về các tiện ích mở rộng bên thứ ba và không cung cấp bất kỳ sự hỗ trợ nào! - %s (Đã vô hiệu) - Âm thanh & video + %s (Đã vô hiệu hoá) + Âm thanh & Độ phân giải Âm thanh - Video - Khởi động lại ứng dụng để thấy các thay đổi. + Độ phân giải video + Khởi động lại ứng dụng để thấy câc thay đổi. Chế độ an toàn được bật Tất cả tiện ích mở rộng đã được tắt do ứng dụng bị ngừng bất thường để giúp bạn tìm ra vấn đề gây lỗi. Xem thông tin sự cố @@ -426,11 +430,11 @@ Tự động tải xuống tiện ích mở rộng Làm lại tiến trình thiết lập Trình cài đặt APK - Một số thiết bị không hỗ trợ trình cài đặt gói mới. Hãy thử chọn chế độ tương thích cũ nếu các bản cập nhật không thể cài đặt. + Một số thiết bị không hỗ trợ trình cài đặt gói mới. Hãy thử chọn chế độ tương thích cũ nếu các bản cập nhật không cài đặt. %1$s %2$d%3$s Phát Trailer - Tự động cài đặt tất cả tiện ích mở rộng chưa được cài đặt từ những kho nguồn phim đã thêm. - Cập nhật đã bắt đầu + Tự động cài đặt tất cả tiện ích mở rộng chưa được cài đặt từ những kho tiện ích đã thêm. + Bắt đầu cập nhật Liên kết Danh sách HLS Trình phát ưu tiên @@ -469,8 +473,8 @@ Danh đề Giới thiệu Xoá lịch sử - Hiển thị cửa sổ bật lên của bỏ qua giới thiệu cho mở đầu/kết thúc - Văn bản quá dài. Không thể lưu vào bảng nhớ tạm. + Hiện các popup bỏ qua cho mở đầu/kết thúc + Văn bản quá dài. Không thể lưu vào bộ nhớ tạm. Xoá khỏi đã xem Bạn có chắc muốn thoát? Có @@ -484,7 +488,8 @@ Chế độ tương thích cũ Đã cập nhật (Mới đến Cũ) Đã cập nhật (Cũ đến Mới) - Thư viện của bạn đang trống :( \nĐăng nhập vào tài khoản thư viện hoặc thêm phim vào thư viện cục bộ. + Thư viện của bạn đang trống :( +\nĐăng nhập vào tài khoản thư viện hoặc thêm phim vào thư viện cục bộ. Mở bằng Siêu dữ liệu không được cung cấp bởi trang web, video sẽ không tải được nếu nó không tồn tại trên trang web. Trình cài đặt gói @@ -512,7 +517,8 @@ Dừng Bỏ chặn nhà mạng Đã bỏ đăng ký %s - Tìm thấy tệp Safe mode! \nKhông tải bất cứ tiện ích mở rộng nào khi khởi động cho đến khi loại bỏ tệp. + Tìm thấy tệp Safe mode! +\nKhông tải bất cứ tiện ích mở rộng nào khi khởi động cho đến khi loại bỏ tệp. Hoàn tác Đang cập nhật các phim đã đăng kí Bỏ chặn các URL gốc của GitHub bằng jsDelivr. Có thể làm cập nhật bị trễ vài ngày. @@ -529,18 +535,28 @@ Hồ sơ Trợ giúp Nền hồ sơ - Tại đây bạn có thể thay đổi cách sắp xếp các nguồn. Nếu video có mức độ ưu tiên cao hơn thì video đó sẽ xuất hiện cao hơn trong lựa chọn nguồn. Tổng ưu tiên nguồn và ưu tiên chất lượng là ưu tiên video. \n \nNguồn A: 3 \nChất lượng B: 7 \nSẽ có mức độ ưu tiên video kết hợp là 10. \n \nLƯU Ý: Nếu tổng là 10 hoặc nhiều hơn, trình phát sẽ tự động bỏ tải khi liên kết đó được tải! + Tại đây bạn có thể thay đổi cách sắp xếp các nguồn. Nếu video có mức độ ưu tiên cao hơn thì video đó sẽ xuất hiện cao hơn trong lựa chọn nguồn. Tổng ưu tiên nguồn và ưu tiên chất lượng là ưu tiên video. +\n +\nNguồn A: 3 +\nChất lượng B: 7 +\nSẽ có mức độ ưu tiên video kết hợp là 10. +\n +\nLƯU Ý: Nếu tổng là 10 hoặc nhiều hơn, trình phát sẽ tự động bỏ tải khi liên kết đó được tải! Chất lượng Bạn đã bình chọn - Tắt - Không tìm thấy kho nguồn phim, hãy kiểm tra URL và thử lại với VPN + Vô hiệu hoá + Không tìm thấy kho tiện ích, hãy kiểm tra URL và thử lại với VPN Không tìm thấy tiện ích mở rộng Không thể khởi tạo UI, đây là một LỖI LỚN và cần được báo cáo ngay lập tức tới %s Chọn chế độ lọc tiện ích mở rộng tải xuống %s đã xóa khỏi mục yêu thích Yêu thích %s đã thêm vào mục yêu thích - Các mục có thể trùng lặp đã được tìm thấy trong thư viện của bạn: \n \n%s \n \nBạn vẫn muốn thêm mục này, thay thế những mục hiện có hay hủy hành động? + Các mục có thể trùng lặp đã được tìm thấy trong thư viện của bạn: +\n +\n%s +\n +\nBạn vẫn muốn thêm mục này, thay thế những mục hiện có hay hủy hành động? Tần suất sao lưu Đã tìm thấy bản sao tiềm năng Khóa hồ sơ @@ -553,8 +569,10 @@ Thêm vào Đăng ký Xóa khỏi mục yêu thích - Chọn hồ sơ của bạn - Có vẻ như một mục có khả năng trùng lặp đã tồn tại trong thư viện của bạn: \'%s.\' \n \nBạn vẫn muốn thêm mục này, thay thế mục hiện có hay hủy hành động? + Ai đang xem + Có vẻ như một mục có khả năng trùng lặp đã tồn tại trong thư viện của bạn: \'%s.\' +\n +\nBạn vẫn muốn thêm mục này, thay thế mục hiện có hay hủy hành động? Nhập mã PIN PIN Nhập mã PIN hiện tại @@ -565,7 +583,7 @@ Quản lý hồ sơ Chỉnh sửa hồ sơ Tải lại liên kết - Tìm kiếm trong tiện ích mở rộng khác + Tìm kiếm tiện ích mở rộng khác Hiển thị đề xuất Kiểm tra tất cả Tiện ích mở rộng Xoay @@ -575,7 +593,7 @@ Bật tự động xoay màn hình theo hướng của video Tự động xoay đã sao chép! - Lỗi truy cập Bảng nhớ tạm, Vui lòng thử lại. + Lỗi truy cập Bộ nhớ tạm, Vui lòng thử lại. Lỗi sao chép, Vui lòng sao chép logcat và liên hệ hỗ trợ ứng dụng. Yêu thích OK @@ -595,17 +613,17 @@ Bài kiểm tra này chỉ dành cho các nhà phát triển và không xác nhận hay phủ nhận việc hoạt động của bất kỳ tiện ích mở rộng nào. Chế độ tiêu thụ pin của ứng dụng đã được đặt ở mức không giới hạn Phương tiện - Tên và URL kho nguồn phim + Tên và URL kho tiện ích Đặt lại Để đảm bảo quá trình tải xuống và thông báo cho các chương trình truyền hình đã đăng ký không bị gián đoạn, CloudStream cần có quyền chạy ở chế độ nền. Bằng cách nhấn OK, một hộp thoại yêu cầu sẽ hiển thị. Vui lòng nhấn \"Cho phép\".\n\nXin lưu ý, quyền này không có nghĩa là CS3 sẽ làm hao pin của bạn. Nó sẽ chỉ hoạt động ở chế độ nền khi cần thiết, chẳng hạn như khi nhận được thông báo hoặc tải xuống video từ các tiện ích mở rộng chính thức. Mùa %1$d Tập %2$d sẽ được phát hành vào - Sắp ra mắt sau %s + Sắp tới sau %s Chọn thiết bị truyền Bảo mật Tài khoản Mã QR Bỏ qua - Mở kho nguồn phim + Mở kho tiện ích CloudStream Wiki Truy cập %s trên điện thoại hoặc máy tính và nhập mã bên trên Mã PIN đã hết hạn! @@ -613,7 +631,7 @@ Không lấy được mã PIN, vui lòng thử xác thực cục bộ Không có tải xuống nào. Xác thực cục bộ - Truyền nguồn thay thế + Phản chiếu màn hình Phát từ đầu Mở video có sẵn Cảnh báo @@ -624,10 +642,16 @@ Bỏ chọn tất cả Xoá các tệp Xoá (%1$d | %2$s) - Bạn có chắc chắn muốn xóa vĩnh viễn các mục sau không? \n \n%s - Bạn có chắc chắn muốn xóa vĩnh viễn các tập trong %1$s? \n \n%2$s + Bạn có chắc chắn muốn xóa vĩnh viễn các mục sau không? +\n +\n%s + Bạn có chắc chắn muốn xóa vĩnh viễn các tập trong %1$s? +\n +\n%2$s Bạn cũng sẽ xóa vĩnh viễn tất cả các tập trong loạt phim: \n \n%s - Bạn có chắc chắn muốn xóa vĩnh viễn tất cả các tập trong loạt phim này không? \n \n%s + Bạn có chắc chắn muốn xóa vĩnh viễn tất cả các tập trong loạt phim này không? +\n +\n%s Xóa tiện ích mở rộng Ngày phát hành (Cũ đến mới) Ẩn tên các nút điều khiển trình phát @@ -635,7 +659,7 @@ Xem trước trên thanh tua Chưa tải phụ đề nào Xác nhận trước khi thoát - Hiển thị hộp thoại xác nhận trước khi thoát ứng dụng + Hiện hộp thoại xác nhận trước khi thoát ứng dụng Không hiển thị Hiển thị Vị trí thư mục sao lưu @@ -644,8 +668,8 @@ Video này là Torrent, điều này có nghĩa là hoạt động video của bạn có thể được theo dõi.\nHãy đảm bảo rằng bạn hiểu về Torrent trước khi tiếp tục. Lỗi mã hóa Giải mã phần mềm cho phép phát các tệp video không được thiết bị của bạn hỗ trợ, nhưng có thể gây ra phản hồi chậm hoặc phát lại không ổn định ở độ phân giải cao. - Giải mã phần mềm - Khởi động lại ứng dụng và chấp nhận cửa sổ bật lên của Stream Torrent để tiếp tục. + Bộ giải mã ứng dụng + Khởi động lại ứng dụng và chấp nhận cửa sổ Stream Torrent để tiếp tục. Kích hoạt torrent trong Cài đặt/Nguồn phim/Thể loại ưu tiên Tải phụ đề đầu tiên có sẵn Âm thanh @@ -660,7 +684,7 @@ Cập nhật tiện ích mở rộng Không có tiện ích mở rộng nào được cập nhật. Ngày phát sóng (Cũ nhất) - Đã cập nhật thành công %d tiện ích mở rộng + Đã cập nhật thành công %d tiện ích mở rộng! Xếp hạng (Cao nhất) Cập nhật tiện ích mở rộng thủ công Tập (Giảm dần) @@ -685,7 +709,7 @@ Đến mục Tải xuống Không có kết nối Internet. \n\nVui lòng kết nối Internet rồi thử lại, hoặc xem các nội dung đã tải xuống khi đang ngoại tuyến. Thay đổi khung hiển thị màn hình - Quét chồng lấn + Vượt khung Thay đổi kích thước poster Kích thước poster Tăng tốc độ phát khi nhấn giữ @@ -700,12 +724,12 @@ URL hoặc ảnh không hợp lệ Đã cập nhật ảnh thành công Đánh dấu là đã xem đến tập này - Xóa đã xem đến tập này + Xóa những tập đã xem đến tập này Đã tải lại Tải lại nguồn phim Tên Độ phân giải và tên - Phát nguồn thay thế" + Phản chiếu màn hình" Căn chỉnh phụ đề Dưới trái Dưới giữa @@ -725,13 +749,13 @@ Cài đặt bản phát hành trước thất bại. Tập Bật bộ lọc độ sáng khi độ sáng màn hình vượt quá 100% - Hiển thị bảng dàn diễn viên + Hiển thị bảng diễn viên Thông tin video Độ sáng bổ sung Tên nguồn Hàng đợi tải xuống Không có tải xuống đang chờ nào. - Quyết định cách sắp xếp các nguồn video trong trình phát + Quyết định cách sắp xếp các nguồn video trong trình phát. Ưu tiên nguồn Tải xuống tất cả Hủy tất cả @@ -745,20 +769,4 @@ Đã bật độ sáng bổ sung Hiển thị lớp phủ siêu dữ liệu trình phát - Trực tiếp - Video - Xem trước - Hiển thị đồng hồ thời gian thực - Hiển thị đồng hồ thời gian thực ở phía trên màn hình. Áp dụng cho trang chủ và trình phát - Cài đặt hồ sơ - Ẩn nguồn có độ ưu tiên âm - Ẩn nguồn có lỗi - - %d liên kết bị ẩn - - Nhật ký gỡ lỗi - Phát video đang dừng - Nền - Góc - Giảm bớt căng thẳng khi chọn nguồn phát diff --git a/app/src/main/res/values-b+zh+TW/strings.xml b/app/src/main/res/values-b+zh+TW/strings.xml index cfd8adf05..e5879ae37 100644 --- a/app/src/main/res/values-b+zh+TW/strings.xml +++ b/app/src/main/res/values-b+zh+TW/strings.xml @@ -19,7 +19,8 @@ 速度(%.2fx) 評分:%.1f - 發現新版本! \n%1$s -> %2$s + 發現新版本! +\n%1$s -> %2$s 填充 %d 分鐘 CloudStream @@ -186,8 +187,10 @@ 繼續 -30 +30 - 這將永遠刪除 %s \n你確定嗎? - 剩下 \n%d 分鐘 + 這將永遠刪除 %s +\n你確定嗎? + 剩下 +\n%d 分鐘 連載中 已完結 狀態 @@ -410,7 +413,9 @@ 已停用:%d 未下載:%d 已更新 %d 外掛程式 - CloudStream 預設沒有安裝網站。你需要從儲存庫安裝網站。 \n \n加入我們的 Discord 或在網路上搜尋。 + CloudStream 預設沒有安裝網站。你需要從儲存庫安裝網站。 +\n +\n加入我們的 Discord 或在網路上搜尋。 查看 公開清單 字幕全大寫 @@ -498,7 +503,8 @@ 按字母順序(A 到 Z) 按字母順序(Z 到 A) 選擇媒體庫 - 找到安全模式檔案! \n在刪除此檔案之前,將不會在啟動時載入任何擴充功能。 + 找到安全模式檔案! +\n在刪除此檔案之前,將不會在啟動時載入任何擴充功能。 日誌 失敗 通過 @@ -516,7 +522,8 @@ 還原 無法存取 GitHub。 正在開啟 jsDelivr proxy… 使用 jsDelivr 繞過直接使用 GitHub 網址時的存取封鎖。 可能導致更新延遲數天。 - 您的媒體庫是空的 :( \n登入媒體庫帳號或將節目新增到您本機的媒體庫。 + 您的媒體庫是空的 :( +\n登入媒體庫帳號或將節目新增到您本機的媒體庫。 您的媒體庫是空的。可嘗試以不同的帳號登入。 正在更新訂閱節目 備份頻率 @@ -524,7 +531,11 @@ 我的最愛 %s 已加入我的最愛 以 %s 的身分登入 - 您的媒體庫中似乎有多個重覆的項目: \n \n%s \n \n您要強制加入、取代已有項目、還是取消操作? + 您的媒體庫中似乎有多個重覆的項目: +\n +\n%s +\n +\n您要強制加入、取代已有項目、還是取消操作? 輸入 %s 的 PIN 碼 行動數據 找到可能重覆的項目 @@ -556,11 +567,19 @@ 找不到資源庫,請檢查網址與 VPN 設定 您已完成投票 在資源庫中找不到外掛程式 - 您的媒體庫中似乎有重覆的項目:「%s」。 \n \n您要強制加入、取代已有項目、還是取消操作? + 您的媒體庫中似乎有重覆的項目:「%s」。 +\n +\n您要強制加入、取代已有項目、還是取消操作? 設回預設 輸入 PIN 碼 PIN 碼 - 您可在此調整來源的排序方式。具有愈小的優先值的影片,在來源選擇中顯示得愈前面。來源優先值與品質優先值的加總就是影片優先值。 \n例如: \n來源 A:3 \n品質 B: 7 \n則該來源的影片優先值為 10。 \n \n注意:如果加總達到 10 或更高,則載入該連結時播放器將自動跳過載入! + 您可在此調整來源的排序方式。具有愈小的優先值的影片,在來源選擇中顯示得愈前面。來源優先值與品質優先值的加總就是影片優先值。 +\n例如: +\n來源 A:3 +\n品質 B: 7 +\n則該來源的影片優先值為 10。 +\n +\n注意:如果加總達到 10 或更高,則載入該連結時播放器將自動跳過載入! 輸入目前的 PIN 碼 顯示切換畫面方向的按鈕 選擇篩選外掛程式下載的模式 @@ -580,7 +599,8 @@ 顯示推薦 在播放器中新增速度選項 即將在 %s 推出 - %s \n剩餘 + %s +\n剩餘 測試所有擴充功能 停用電池優化 有聲書 @@ -744,7 +764,4 @@ 軟體解碼使程式可以播放裝置不支援的影片,但可能導致播放高解析的影片時的延遲或不穩定。 音量已超過 100% 顯示播放器元資料遮罩層 - 影片 - 預覽 - 播放中 diff --git a/app/src/main/res/values-b+zh/strings.xml b/app/src/main/res/values-b+zh/strings.xml index d68150677..de0cb834a 100644 --- a/app/src/main/res/values-b+zh/strings.xml +++ b/app/src/main/res/values-b+zh/strings.xml @@ -19,7 +19,8 @@ 速度(%.2fx) 评分:%.1f - 发现新版本! \n%1$s -> %2$s + 发现新版本! +\n%1$s -> %2$s 填充 %d 分钟 CloudStream @@ -187,8 +188,10 @@ 继续 -30 +30 - 这将永久删除 %s \n您确定吗? - %d 分钟 \n剩余 + 这将永久删除 %s +\n您确定吗? + %d 分钟 +\n剩余 连载中 已完结 状态 @@ -352,8 +355,8 @@ 随机 即将到来… Cam - CamRip - CamHD + Cam + Cam HQ HD TS @@ -411,7 +414,9 @@ 已禁用:%d 未下载:%d 已更新 %d 插件 - CloudStream 默认不安装片源。您需要从仓库中安装片源。 \n \n加入我们的 Discord 或在网上搜索。 + CloudStream 默认不安装片源。您需要从仓库中安装片源。 +\n +\n加入我们的 Discord 或在网上搜索。 查看社区仓库 公开列表 字幕全大写 @@ -481,7 +486,8 @@ 应用退出后将会更新 插件已下载 从已观看中移除 - 发现安全模式文件! \n启动时不加载任何扩展,直到文件被删除。 + 发现安全模式文件! +\n启动时不加载任何扩展,直到文件被删除。 浏览器 库 排序方式 @@ -494,7 +500,8 @@ 字母排序(从 Z 到 A) 选择库 打开方式 - 您的库是空的 :( \n登录库账户或添加节目到您的本地库。 + 您的库是空的 :( +\n登录库账户或添加节目到您的本地库。 此列表是空的,请尝试切换到另一个。 播放器显示 - 快进快退秒数 播放器可见时使用的快进快退秒数 @@ -527,7 +534,13 @@ 配置文件 帮助 移动流量 - 在这里,您可以更改源的排序方式。如果视频具有更高的优先级,它将在源选择中显示得更高。源优先级和质量优先级的总和就是视频优先级。 \n \n来源 A:3 \n质量 B: 7 \n组合视频优先级为 10。 \n \n注意:如果总和为 10 或更多,则加载该链接时播放器将自动跳过加载! + 在这里,您可以更改源的排序方式。如果视频具有更高的优先级,它将在源选择中显示得更高。源优先级和质量优先级的总和就是视频优先级。 +\n +\n来源 A:3 +\n质量 B: 7 +\n组合视频优先级为 10。 +\n +\n注意:如果总和为 10 或更多,则加载该链接时播放器将自动跳过加载! 质量 个人资料背景 PIN @@ -557,8 +570,14 @@ 您已投票 %s已添加到收藏夹 %s已从收藏夹中删除 - 您的资料库中似乎已经存在一个可能相同的项目:\'%s.\' \n \n您想添加该项目、替换现有项目还是取消操作? - 在您的资料库中发现了潜在的重复项目: \n \n%s \n \n您想添加此项目、替换现有项目还是取消操作? + 您的资料库中似乎已经存在一个可能相同的项目:\'%s.\' +\n +\n您想添加该项目、替换现有项目还是取消操作? + 在您的资料库中发现了潜在的重复项目: +\n +\n%s +\n +\n您想添加此项目、替换现有项目还是取消操作? 确认PIN 输入来自 %s 的 PIN 码 锁定个人资料 @@ -578,7 +597,8 @@ 解锁 CloudStream 使用生物识别技术锁定 密码或 PIN 验证 - %s \n剩余 + %s +\n剩余 测试所有扩展 已复制! 访问剪贴板出错,请重试。 @@ -624,10 +644,18 @@ 全不选 删除文件 删除 (%1$d | %2$s) - 您确定要永久删除以下项目吗? \n \n%s - 您确定要永久删除 %1$s中的下述剧集吗? \n \n%2$s - 您还将永久删除下述系列中的所有剧集: \n \n%s - 您确定要永久删除下述系列的所有剧集吗? \n \n%s + 您确定要永久删除以下项目吗? +\n +\n%s + 您确定要永久删除 %1$s中的下述剧集吗? +\n +\n%2$s + 您还将永久删除下述系列中的所有剧集: +\n +\n%s + 您确定要永久删除下述系列的所有剧集吗? +\n +\n%s 发布日期(从新至旧) 无法获取设备 PIN 码,尝试本地身份验证 PIN 码现已过期! @@ -655,7 +683,7 @@ 得分 %s 日期 %s 更新插件 - 成功更新了 %d 个插件 + 成功更新了 %d 个插件! 没有插件被更新。 手动更新插件 集数(升序) @@ -744,36 +772,4 @@ 确定在播放器中如何排列视频源的顺序 已启用额外亮度 显示播放器元数据遮罩层 - 视频 - 预览 - 播放中 - 显示实时时钟 - 在屏幕顶部显示实时时钟。应用于主页和播放器 - 配置文件设置 - 隐藏负优先级的来源 - 隐藏有错误的来源 - - %d 条隐藏链接 - - Logcat 🐈 - 开始暂停的视频 - 降低选择源时的压力 - 边缘 - 后台 - 压缩器 - 动态范围压缩器 - 在播放器中显示压缩器控件(帮助平衡响亮/安静声音) - 开 - 开 · %1$d dB / +%2$d dB - 关 - 轻度 - 对话增强 - 重度 - 阈值:%d dB - 增益补偿:+%d dB - 会降低比 %d dB 响的声音的音量 - 将总体音量调高 %d dB - Url - 图片选择器 - 默认图片 diff --git a/app/src/main/res/values-b+be/strings.xml b/app/src/main/res/values-be/strings.xml similarity index 97% rename from app/src/main/res/values-b+be/strings.xml rename to app/src/main/res/values-be/strings.xml index 9fbf8a8d5..1a47a6a63 100644 --- a/app/src/main/res/values-b+be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -34,7 +34,7 @@ Шукаць па %s… Распазнаванне маўлення недаступна Пачніце размаўляць… - Няма дадзеных + Няма даных Больш параметраў Наступная серыя Жанры @@ -125,10 +125,10 @@ Працягнуць прагляд Выдаліць Больш інфармацыі - @string/home_play + \@string/home_play Для карэктнай працы гэтага пастаўшчыка можа спатрэбіцца VPN Гэты пастаўшчык — Torrent, рэкамендуецца VPN - Вэб-сайт не пастаўляе метададзеных, загрузіць відэа не ўдасца, калі на сайце яго няма. + Вэб-сайт не пастаўляе метаданых, загрузіць відэа не ўдасца, калі на сайце яго няма. Апісанне Сюжэту не знойдзена Апісання не знойдзена @@ -159,12 +159,12 @@ Выкарыстоўваць сістэмную яркасць у прайгравальніку замест цёмнага накладання Абнаўляць працэс прагляду Аўтаматычна сінхранізаваць прагрэс бягучай серыі - Аднавіць дадзеныя з рэзервовай копіі - Рэзервовае капіраванне дадзеных + Аднавіць даныя з рэзервовай копіі + Рэзервовае капіраванне даных Частата рэзервовага капіравання Загружаны файл рэзервовай копіі - Не ўдалося аднавіць дадзеныя з файла %s - Дадзеныя захаваны + Не ўдалося аднавіць даныя з файла %s + Даныя захаваны Няма дазволу да сховішча. Паспрабуйце ізноў. Памылка пры рэзервовым капіраванні %s Пошук @@ -247,7 +247,7 @@ Сціслы агляд у чарзе Субцітраў няма - Прадвызначаны + Прадвызначанае Свабодна Ужыта Праграма @@ -315,7 +315,7 @@ Прапусціць гэта абнаўленне Абнавіць Прыярытэтная якасць прагляду (WiFi) - Прыярытэтная якасць прагляду (Мабільная перадача дадзеных) + Прыярытэтная якасць прагляду (Мабільная перадача даных) Максімальная колькасць сімвалаў у загалоўку праглядальніка Паказваць інфармацыю ў прайгравальніку Памер буфера відэа @@ -458,7 +458,7 @@ Раздзяляльнасць Звесткі пра медыя Памылковы ID - Памылковыя дадзеныя + Памылковыя даныя Памылковы URL-адрэс Памылка Прыбраць схаваныя цітры з субцітраў @@ -480,7 +480,7 @@ Назва рэпазіторыя (неабавязкова) URL-адрас рэпазіторыя або кароткі код Убудова загружана - Убудова спампавана + Убудава спампавана Убудова выдалена Не ўдалося загрузіць %s 18+ @@ -499,7 +499,7 @@ Спампавана: %d Выключана: %d Не спампавана: %d - Абноўлена %d убудоў + Абноўлена %d плагіна(ў) Прадвызначана на CloudStream няма ўсталяваных вэб-сайтаў. Вам трэба ўсталяваць вэб-сайты з рэпазіторыяў. \n \nДалучыцеся да нашага сервера Discord або пашукайце ў сетцы. Праглядзець рэпазіторыі ад супольнасці Публічны спіс @@ -595,7 +595,7 @@ Адпісацца Профіль %d Wi-Fi - Мабільная перадача дадзеных + Мабільная перадача даных Выбраць як прадвызначаны Выкарыстоўваць Рэдагаваць @@ -643,7 +643,7 @@ Праверка сапраўднасці біяметрыяй не падтрымліваецца на гэтай прыладзе Разблакіруйце праграму адбіткам пальца, Face ID, PIN-кодам, узорам разблакіроўкі або паролем. Праз некалькі няўдалых спроб акно з запытам закрыецца. Проста перазапусціце праграму, каб паўтарыць спробу. - Вашы дадзеныя CloudStream былібылі толькі што зарэзерваваныя. Нягледзячы на тое, што магчымасць вельмі маленькая, усе прылады могуць паводзіць сябе па-рознаму. У рэдкасным выпадку, калі вы страціце доступ да праграмы, поўнасцю ачысціце даныя і аднавіце іх праз рэзервовую копію. Выбачайце за любую нязручнасць, якая можа з гэтага атрымацца. + Вашы даныя CloudStream былі зарэзерваваныя. Нягледзячы на тое, што магчымасць вельмі маленькая, усе прылады могуць паводзіць сябе па-рознаму. У рэдкасным выпадку, калі вы страціце доступ да праграмы, поўнасцю ачысціце даныя і аднавіце іх праз рэзервовую копію. Выбачайце за любую нязручнасць, якая можа з гэтага атрымацца. Скінуць CloudStream-Вікі Наведайце %s на вашым смартфоне або камп\'ютары і ўвядзіце код вышэй @@ -735,8 +735,5 @@ %d спампоўванняў у чарзе %d спампоўванняў у чарзе - Паказваць накладанне з метададзенымі ў прайгравальніку - Відэа - Перадпрагляд - Ужывую + Паказваць накладанне з метаданымі ў прайгравальніку diff --git a/app/src/main/res/values-b+ca/strings.xml b/app/src/main/res/values-ca/strings.xml similarity index 99% rename from app/src/main/res/values-b+ca/strings.xml rename to app/src/main/res/values-ca/strings.xml index 621243c78..495eb5e3c 100644 --- a/app/src/main/res/values-b+ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -123,7 +123,7 @@ Continua mirant Eliminar Més info - @string/home_play + \@string/home_play Potser necessiteu una VPN per que aquest proveïdor funcioni correctament Aquest proveïdor és un Torrent, es recomana fer servir una VPN El lloc no proveeix metadades, la càrrega del video fallarà si no existeix al lloc. diff --git a/app/src/main/res/values-lo/strings.xml b/app/src/main/res/values-lo/strings.xml deleted file mode 100644 index c1a3daa02..000000000 --- a/app/src/main/res/values-lo/strings.xml +++ /dev/null @@ -1,744 +0,0 @@ - - - %1$s ຕອນ %2$d - ນັກສະແດງ: %s - ຕອນທີ %d ຈະອອກໃນອີກ - ຊີຊັນ %1$d ຕອນ %2$d ຈະອອກໃນອີກ - %1$dd %2$dh %3$dm - %1$dh %2$dm - %dນ - %1$dh %2$dm %3$ds - %1$dm %2$ds - %1$ds - ໂປສເຕີ - ໂປສເຕີ - ໂປສເຕີຕອນ - ໂປສເຕີຫຼັກ - ສຸ່ມຖັດໄປ - ກັບຄືນ - ຫຼິ້ນແຕ່ເລີ່ມຕົ້ນ - ປ່ຽນຜູ້ໃຫ້ບໍລິການ - ຕົວຢ່າງພື້ນຫຼັງ - ຄວາມໄວ (%.2fx) - ຄະແນນ: %.1f - ພົບການອັບເດດໃໝ່!\n%1$s -> %2$s - ເນື້ອຫາເສີມ (Filler) - %d ນາທີ - CloudStream - ຫຼິ້ນດ້ວຍ CloudStream - ໜ້າຫຼັກ - ຄົ້ນຫາ - ດາວໂຫຼດ - ຄິວດາວໂຫຼດ - ການຕັ້ງຄ່າ - ຄົ້ນຫາ… - ຄົ້ນຫາ %s… - ບໍ່ສາມາດໃຊ້ການຮັບຮູ້ສຽງໄດ້ - ເລີ່ມເວົ້າ… - ບໍ່ມີຂໍ້ມູນ - ຕົວເລືອກເພີ່ມເຕີມ - ຕອນຕໍ່ໄປ - ປະເພດ - ແບ່ງປັນ - ເປີດໃນບຣາວເຊີ - ບຣາວເຊີ - ຂ້າມການໂຫຼດ - ກຳລັງໂຫຼດ… - ກຳລັງເບິ່ງ - ພັກໄວ້ກ່ອນ - ເບິ່ງຈົບແລ້ວ - ເລີກເບິ່ງ - ແຜນທີ່ຈະເບິ່ງ - ກຳລັງເບິ່ງຄືນ - ຫຼິ້ນຮູບເງົາ - ຫຼິ້ນຕົວຢ່າງ - ຫຼິ້ນໄລຟ໌ສົດ - ສະຕຣີມ Torrent - ຫຼິ້ນຊີຣີທັງໝົດ - ວິດີໂອນີ້ແມ່ນ Torrent, ຊຶ່ງໝາຍຄວາມວ່າການເຄື່ອນໄຫວຂອງທ່ານສາມາດຖືກຕິດຕາມໄດ້.\nກະລຸນາໝັ້ນໃຈວ່າທ່ານເຂົ້າໃຈກ່ຽວກັບ Torrent ກ່ອນທີ່ຈະດຳເນີນການຕໍ່. - ແຫຼ່ງວິດີໂອ - ຄຳບັນຍາຍ (Subtitles) - ລອງເຊື່ອມຕໍ່ໃໝ່… - ກັບຄືນ - ຫຼິ້ນຕອນນີ້ - ດາວໂຫຼດ - ດາວໂຫຼດແລ້ວ - ກຳລັງດາວໂຫຼດ - ຢຸດການດາວໂຫຼດຊົ່ວຄາວ - ເລີ່ມການດາວໂຫຼດ - ດາວໂຫຼດບໍ່ສຳເລັດ - ຍົກເລີກການດາວໂຫຼດ - ດາວໂຫຼດສຳເລັດ - ເລືອກລາຍການທີ່ຈະລຶບ - ຍັງບໍ່ມີລາຍການດາວໂຫຼດໃນຂະນະນີ້. - ຍັງບໍ່ມີຄິວດາວໂຫຼດໃນຂະນະນີ້. - ສາມາດເບິ່ງແບບອອຟໄລໄດ້ - ເລືອກທັງໝົດ - ຍົກເລີກການເລືອກທັງໝົດ - ເລີ່ມການອັບເດດ - ສະຕຣີມຜ່ານເຄືອຂ່າຍ - ເປີດວິດີໂອໃນເຄື່ອງ - ເກີດຂໍ້ຜິດພາດໃນການໂຫຼດລິ້ງ - ໂຫຼດລິ້ງໃໝ່ແລ້ວ - ໜ່ວຍຄວາມຈຳພາຍໃນ - ພາກ - ຊັບ - ລຶບໄຟລ໌ - ຫຼິ້ນໄຟລ໌ - ດາວໂຫຼດຕໍ່ - ຢຸດການດາວໂຫຼດ - ຂໍ້ມູນເພີ່ມເຕີມ - ເຊື່ອງ - ຫຼິ້ນ - ຂໍ້ມູນ - ກັ່ນຕອງບຸກມາກ - ບຸກມາກ - ລຶບອອກ - ຕັ້ງສະຖານະການເບິ່ງ - ນຳໃຊ້ - ຄັດລອກ - ປິດ - ລ້າງຂໍ້ມູນ - ບັນທຶກ - ຊື່ ແລະ URL ຂອງ Repository - ຄັດລອກແລ້ວ! - ແຈ້ງເຕືອນຕອນໃໝ່ - ຄົ້ນຫາໃນສ່ວນເສີມອື່ນໆ - ສະແດງລາຍການແນະນຳ - ຄວາມໄວເຄື່ອງຫຼິ້ນ - ຕັ້ງຄ່າຄຳບັນຍາຍ - ສີຂໍ້ຄວາມ - ສີຂອບຂໍ້ຄວາມ - ສີພື້ນຫຼັງ - ສີໜ້າຕ່າງ - ປະເພດຂອບ - ລະດັບຄວາມສູງຂອງຄຳບັນຍາຍ - ຟອນທ໌ - ຂະໜາດຟອນທ໌ - ຄົ້ນຫາໂດຍໃຊ້ຜູ້ໃຫ້ບໍລິການ - ຄົ້ນຫາໂດຍໃຊ້ປະເພດ - ມອບ Benene ໃຫ້ຜູ້ພັດທະນາແລ້ວ %d ອັນ - ຍັງບໍ່ໄດ້ມອບ Benene - ເລືອກພາສາອັດຕະໂນມັດ - ພາສາທີ່ຈະດາວໂຫຼດ - ພາສາຂອງຄຳບັນຍາຍ - ກົດຄ້າງເພື່ອຄືນຄ່າເລີ່ມຕົ້ນ - ນຳເຂົ້າຟອນທ໌ໂດຍການວາງໄວ້ໃນ %s - ເບິ່ງຕໍ່ - ລຶບອອກ - ຂໍ້ມູນເພີ່ມເຕີມ - \@string/home_play - ອາດຈຳເປັນຕ້ອງໃຊ້ VPN ເພື່ອໃຫ້ຜູ້ໃຫ້ບໍລິການນີ້ເຮັດວຽກໄດ້ຢ່າງຖືກຕ້ອງ - ຜູ້ໃຫ້ບໍລິການນີ້ແມ່ນ Torrent, ແນະນຳໃຫ້ໃຊ້ VPN - ເວັບໄຊທ໌ບໍ່ໄດ້ໃຫ້ຂໍ້ມູນ Metadata, ວິດີໂອອາດຈະໂຫຼດບໍ່ໄດ້ຖ້າມັນບໍ່ມີຢູ່ໃນເວັບໄຊທ໌. - ຄຳອະທິບາຍ - ບໍ່ພົບເນື້ອເລື່ອງ - ບໍ່ພົບຄຳອະທິບາຍ - ສະແດງ Logcat 🐈 - ບັນທຶກ (Log) - ຮູບຊ້ອນຮູບ (PiP) - ຫຼິ້ນຕໍ່ເນື່ອງໃນເຄື່ອງຫຼິ້ນຂະໜາດນ້ອຍເທິງແອັບຯອື່ນໆ - ປຸ່ມປັບຂະໜາດເຄື່ອງຫຼິ້ນ - ລຶບຂອບສີດຳອອກ - ຄຳບັນຍາຍ - ຕັ້ງຄ່າຄຳບັນຍາຍໃນເຄື່ອງຫຼິ້ນ - ຄຳບັນຍາຍ Chromecast - ຕັ້ງຄ່າຄຳບັນຍາຍ Chromecast - ຄວາມໄວໃນການຫຼິ້ນ - ເພີ່ມຕົວເລືອກຄວາມໄວໃນເຄື່ອງຫຼິ້ນ - ປັດເພື່ອເລື່ອນເບິ່ງ - ປັດໄປທາງຊ້າຍ ຫຼື ຂວາ ເພື່ອຄວບຄຸມຕຳແໜ່ງໃນວິດີໂອ - ປັດເພື່ອປ່ຽນການຕັ້ງຄ່າ - ປັດຂຶ້ນ ຫຼື ລົງ ທາງດ້ານຊ້າຍ ຫຼື ຂວາ ເພື່ອປ່ຽນຄວາມສະຫວ່າງ ຫຼື ສຽງ - ຫຼິ້ນຕອນຕໍ່ໄປອັດຕະໂນມັດ - ເລີ່ມຕອນຕໍ່ໄປເມື່ອຕອນປັດຈຸບັນຈົບລົງ - ແຕະສອງຄັ້ງເພື່ອເລື່ອນ - ແຕະສອງຄັ້ງເພື່ອຢຸດ - ໄລຍະເວລາການເລື່ອນ (ວິນາທີ) - ແຕະສອງຄັ້ງທາງດ້ານຂວາ ຫຼື ຊ້າຍ ເພື່ອເລື່ອນໄປໜ້າ ຫຼື ຖອຍຫຼັງ - ແຕະສອງຄັ້ງຢູ່ເຄິ່ງກາງເພື່ອຢຸດ - ໃຊ້ຄວາມສະຫວ່າງຂອງລະບົບ - ໃຊ້ຄວາມສະຫວ່າງຂອງລະບົບໃນເຄື່ອງຫຼິ້ນແທນການໃຊ້ແຜ່ນມົວສີດຳ - ຄວາມສະຫວ່າງພິເສດ - ເປີດໃຊ້ຟິວເຕີຄວາມສະຫວ່າງເມື່ອຄວາມສະຫວ່າງໜ້າຈໍເກີນ 100% - extra_brightness_enabled - ອັບເດດຄວາມຄືບໜ້າການເບິ່ງ - ຊິງຄ໌ຄວາມຄືບໜ້າຂອງຕອນປັດຈຸບັນໂດຍອັດຕະໂນມັດ - ກູ້ຄືນຂໍ້ມູນຈາກຂໍ້ມູນສຳຮອງ - ສຳຮອງຂໍ້ມູນ - ຄວາມຖີ່ໃນການສຳຮອງຂໍ້ມູນ - ໂຫຼດໄຟລ໌ສຳຮອງແລ້ວ - ບໍ່ສາມາດກູ້ຄືນຂໍ້ມູນຈາກໄຟລ໌ %s - ບັນທຶກຂໍ້ມູນແລ້ວ - ບໍ່ມີສິດໃນການເຂົ້າເຖິງບ່ອນເກັບຂໍ້ມູນ. ກະລຸນາລອງໃໝ່. - ເກີດຂໍ້ຜິດພາດໃນການສຳຮອງຂໍ້ມູນ %s - ຄົ້ນຫາ - ຄັງເກັບ - ບັນຊີ ແລະ ຄວາມປອດໄພ - ອັບເດດ ແລະ ສຳຮອງຂໍ້ມູນ - ຂໍ້ມູນ - ການຄົ້ນຫາຂັ້ນສູງ - ສະແດງຜົນການຄົ້ນຫາແຍກຕາມຜູ້ໃຫ້ບໍລິການ - ຄຳແນະນຳການຄົ້ນຫາ - ສະແດງຄຳແນະນຳໃນຂະນະທີ່ພິມຄົ້ນຫາ - ລ້າງຄຳແນະນຳ - ສະແດງຕອນເສີມ (filler) ສຳລັບອານິເມະ - ສະແດງຂໍ້ມູນ Metadata ເທິງເຄື່ອງຫຼິ້ນ - ສະແດງຕົວຢ່າງ (Trailer) - ສະແດງໂປສເຕີຈາກ Kitsu - ສະແດງລາຍຊື່ນັກສະແດງ - ເຊື່ອງຄຸນນະພາບວິດີໂອທີ່ເລືອກໃນຜົນການຄົ້ນຫາ - ອັບເດດປລັກອິນອັດຕະໂນມັດ - ດາວໂຫຼດປລັກອິນອັດຕະໂນມັດ - ເລືອກໂໝດການກັ່ນຕອງການດາວໂຫຼດປລັກອິນ - ຕິດຕັ້ງປລັກອິນທັງໝົດທີ່ຍັງບໍ່ໄດ້ຕິດຕັ້ງຈາກ repository ທີ່ເພີ່ມໄວ້ໂດຍອັດຕະໂນມັດ. - ສະແດງການອັບເດດແອັບຯ - ຄົ້ນຫາການອັບເດດໃໝ່ໂດຍອັດຕະໂນມັດຫຼັງຈາກເປີດແອັບຯ. - ຕັ້ງຄ່າໃໝ່ທັງໝົດ - ຕິດຕັ້ງເວີຊັນທົດລອງ (Pre-release) - ຕິດຕັ້ງເວີຊັນທົດລອງແລ້ວ. - ບໍ່ສາມາດຕິດຕັ້ງເວີຊັນທົດລອງໄດ້. - ຕົວຕິດຕັ້ງ APK - ບາງອຸປະກອນບໍ່ຮອງຮັບຕົວຕິດຕັ້ງແພັກເກດແບບໃໝ່. ລອງໃຊ້ຕົວເລືອກແບບເກົ່າ (Legacy) ຖ້າບໍ່ສາມາດອັບເດດໄດ້. - Github - ແອັບຯ Light novel ໂດຍຜູ້ພັດທະນາດຽວກັນ - ແອັບຯ Anime ໂດຍຜູ້ພັດທະນາດຽວກັນ - ເຂົ້າຮ່ວມ Discord - ມອບ Benene ໃຫ້ຜູ້ພັດທະນາ - ມອບ Benene ແລ້ວ - ພາສາຂອງແອັບຯ - ຜູ້ໃຫ້ບໍລິການນີ້ບໍ່ຮອງຮັບ Chromecast - ບໍ່ພົບລິ້ງ - ຄັດລອກລິ້ງລົງໃນຄລິບບອດແລ້ວ - ຫຼິ້ນຕອນນີ້ - ຄືນຄ່າເປັນຄ່າເລີ່ມຕົ້ນ - ຊີຊັນ - %1$s %2$d%3$s - ບໍ່ມີຊີຊັນ - ຕອນ - ຕອນ - %1$d-%2$d - %1$d %2$s - ຈະມາໃນອີກ %s - S - E - ບໍ່ພົບຕອນ - ລຶບ - ລຶບໄຟລ໌ - ລຶບໄຟລ໌ທັງໝົດ - ລຶບ (%1$d | %2$s) - ຍົກເລີກ - ຢຸດ - ເລີ່ມ - ບໍ່ສຳເລັດ - ສຳເລັດ - ຄຳເຕືອນ - ຫຼິ້ນຕໍ່ - -30 - +30 - ນີ້ຈະລຶບ %s ຢ່າງຖາວອນ\nທ່ານແນ່ໃຈບໍ່? - ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບລາຍການຕໍ່ໄປນີ້ຢ່າງຖາວອນ?\n\n%s - ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບຕອນຕໍ່ໄປນີ້ໃນ %1$s ຢ່າງຖາວອນ?\n\n%2$s - ທ່ານຈະລຶບທຸກຕອນໃນຊີຣີຕໍ່ໄປນີ້ຢ່າງຖາວອນ:\n\n%s - ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບທຸກຕອນໃນຊີຣີຕໍ່ໄປນີ້ຢ່າງຖາວອນ?\n\n%s - ເຫຼືອອີກ\n%d ນາທີ - ເຫຼືອອີກ\n%s - ກຳລັງສາຍ - ຈົບແລ້ວ - ສະຖານະ - ປີ - ຄະແນນ - ຄວາມຍາວ - ເວັບໄຊທ໌ - ເລື່ອງຫຍໍ້ - ຢູ່ໃນຄິວ - ບໍ່ມີຄຳບັນຍາຍ - ຄ່າເລີ່ມຕົ້ນ - ວ່າງ - ໃຊ້ໄປແລ້ວ - ແອັບຯ - ຮູບເງົາ - ຊີຣີ - ກາຕູນ - ອານິເມະ - Torrents - ສາລະຄະດີ - OVA - ຊີຣີເອເຊຍ - ໄລຟ໌ສົດ - 18+ (NSFW) - ອື່ນໆ - ຮູບເງົາ - ຊີຣີ - ກາຕູນ - ອານິເມະ - OVA - Torrent - ສາລະຄະດີ - ຊີຣີເອເຊຍ - ໄລຟ໌ສົດ - 18+ (NSFW) - ວິດີໂອ - ເພງ - ໜັງສືສຽງ - ສື່ - ສຽງ - ພອດແຄສຕ໌ - ວິດີໂອ - ຂໍ້ຜິດພາດຈາກແຫຼ່ງວິດີໂອ - ຂໍ້ຜິດພາດທາງໄກ - ຂໍ້ຜິດພາດໃນການປະມວນຜົນສັນຍານ - ຂໍ້ຜິດພາດໃນການເຂົ້າລະຫັດ - ຂໍ້ຜິດພາດ: ບໍ່ຮອງຮັບ - ເກີດຂໍ້ຜິດພາດທີ່ບໍ່ຄາດຄິດໃນເຄື່ອງຫຼິ້ນ - ການດາວໂຫຼດຜິດພາດ, ກະລຸນາກວດສອບສິດການເຂົ້າເຖິງບ່ອນເກັບຂໍ້ມູນ - Chromecast ຕອນນີ້ - Chromecast mirror - Cast mirror - ຫຼິ້ນໃນແອັບຯ - ຫຼິ້ນ mirror" - ຫຼິ້ນໃນ %s - ດາວໂຫຼດອັດຕະໂນມັດ - ດາວໂຫຼດ mirror - ໂຫຼດລິ້ງໃໝ່ - ດາວໂຫຼດຄຳບັນຍາຍ - ປ້າຍກຳກັບຄຸນນະພາບ - ປ້າຍກຳກັບພາກສຽງ - ປ້າຍກຳກັບຄຳບັນຍາຍ - ປ້າຍກຳກັບຄະແນນ - ຊື່ເລື່ອງ - ຂໍ້ຄວາມບອກຕອນ - ເປີດ/ປິດ ອົງປະກອບ UI ເທິງໂປສເຕີ - ບໍ່ພົບການອັບເດດ - ກວດສອບການອັບເດດ - ລັອກ - ປັບຂະໜາດ - ແຫຼ່ງວິດີໂອ - ຂ້າມ OP - ບໍ່ຕ້ອງສະແດງອີກ - ຂ້າມການອັບເດດນີ້ - ອັບເດດ - ຄຸນນະພາບການເບິ່ງທີ່ຕ້ອງການ (WiFi) - ຄຸນນະພາບການເບິ່ງທີ່ຕ້ອງການ (ເນັດມືຖື) - ຈຳນວນຕົວອັກສອນສູງສຸດຂອງຊື່ໃນເຄື່ອງຫຼິ້ນ - ສະແດງຂໍ້ມູນເຄື່ອງຫຼິ້ນ - ຂະໜາດບັບເຟີວິດີໂອ - ຄວາມຍາວບັບເຟີວິດີໂອ - ແຄດວິດີໂອໃນພື້ນທີ່ເກັບຂໍ້ມູນ - ລ້າງແຄດວິດີໂອ ແລະ ຮູບພາບ - ເມື່ອສະແດງເຄື່ອງຫຼິ້ນ - ໄລຍະການເລື່ອນ - ໄລຍະການເລື່ອນທີ່ໃຊ້ເມື່ອເຫັນເຄື່ອງຫຼິ້ນ - ເມື່ອເຊື່ອງເຄື່ອງຫຼິ້ນ - ໄລຍະການເລື່ອນ - ໄລຍະການເລື່ອນທີ່ໃຊ້ເມື່ອເຄື່ອງຫຼິ້ນຖືກເຊື່ອງໄວ້ - ອາດເຮັດໃຫ້ແອັບຯຄ້າງໄດ້ ຖ້າຕັ້ງຄ່າສູງເກີນໄປໃນອຸປະກອນທີ່ມີແຮມໜ້ອຍ ເຊັ່ນ Android TV. - ອາດເຮັດໃຫ້ເກີດບັນຫາ ຖ້າຕັ້ງຄ່າສູງເກີນໄປໃນອຸປະກອນທີ່ມີພື້ນທີ່ໜ້ອຍ ເຊັ່ນ Android TV. - DNS over HTTPS - ມີປະໂຫຍດໃນການຂ້າມການບລັອກຈາກ ISP - GitHub Proxy - ບໍ່ສາມາດເຊື່ອມຕໍ່ GitHub ໄດ້. ກຳລັງເປີດໃຊ້ jsDelivr proxy… - ຂ້າມການບລັອກ URL ຂອງ GitHub ໂດຍໃຊ້ jsDelivr. ອາດຈະເຮັດໃຫ້ການອັບເດດຊ້າລົງ 2-3 ມື້. - ຄັດລອກເວັບໄຊທ໌ - ລຶບເວັບໄຊທ໌ - ເພີ່ມເວັບໄຊທ໌ທີ່ຄືກັບເວັບໄຊທ໌ທີ່ມີຢູ່ແລ້ວ ແຕ່ໃຊ້ URL ອື່ນ - ບ່ອນເກັບໄຟລ໌ດາວໂຫຼດ - NGINX server URL - ສະແດງອານິເມະ ພາກສຽງ/ຄຳບັນຍາຍ - ພໍດີກັບໜ້າຈໍ - ຍືດໃຫ້ເຕັມ - ຊູມ - ຂໍ້ຄວາມປະຕິເສດຄວາມຮັບຜິດຊອບ - ການຂ້າມ ISP - ລິ້ງ - ອັບເດດແອັບຯ - ສຳຮອງຂໍ້ມູນ - ສ່ວນເສີມ - ການກະທຳ - ແຄດ - Android TV - ການຄວບຄຸມດ້ວຍທ່າທາງ - ຄວາມປອດໄພ - ບັນຊີ - ຄຸນສົມບັດເຄື່ອງຫຼິ້ນ - ຄຳບັນຍາຍ - ຮູບແບບການສະແດງຜົນ - ຄ່າເລີ່ມຕົ້ນ - ຮູບລັກສະນະ - ຄຸນສົມບັດ - ທົ່ວໄປ - ສະແດງໂມງເວລາຈິງ - ສະແດງໂມງເວລາຈິງຢູ່ເທິງສຸດຂອງໜ້າຈໍ. ນຳໃຊ້ກັບໜ້າຫຼັກ ແລະ ເຄື່ອງຫຼິ້ນ - ປຸ່ມສຸ່ມ - ສະແດງປຸ່ມສຸ່ມໃນໜ້າທຳອິດ ແລະ ຫ້ອງສະໝຸດ - ພາສາຂອງສ່ວນເສີມ - ຮູບແບບແອັບ - ສື່ທີ່ມັກ - ເປີດເນື້ອຫາ 18+ (NSFW) ໃນສ່ວນເສີມທີ່ຮອງຮັບ - ການເຂົ້າລະຫັດຄຳບັນຍາຍ - ຜູ້ໃຫ້ບໍລິການ - ທົດສອບຜູ້ໃຫ້ບໍລິການ - ທົດສອບສ່ວນເສີມທັງໝົດ - ການທົດສອບນີ້ແມ່ນສຳລັບຜູ້ພັດທະນາເທົ່ານັ້ນ ແລະ ບໍ່ໄດ້ຢືນຢັນ ຫຼື ປະຕິເສດການເຮັດວຽກຂອງສ່ວນເສີມໃດໆ. - ຮູບແບບ - ອັດຕະໂນມັດ - ຮູບແບບ TV - ຮູບແບບໂທລະສັບ - ຮູບແບບຕົວຈຳລອງ - ສີຫຼັກ - ທີມຂອງແອັບ - ຕຳແໜ່ງຊື່ຂອງໂປສເຕີ - ວາງຊື່ໄວ້ກ້ອງໂປສເຕີ - ລະຫັດຜ່ານ123 - ຊື່ຜູ້ໃຊ້ - hello@world.com - 127.0.0.1 - ຊື່ເວັບໄຊໃໝ່ - https://example.com - ລະຫັດພາສາ (lo) - %1$s %2$s - ບັນຊີ - ອອກຈາກລະບົບ - ເຂົ້າສູ່ລະບົບ - ຢືນຢັນຕົວຕົນພາຍໃນເຄື່ອງ - ສະຫຼັບບັນຊີ - ເພີ່ມບັນຊີ - ສ້າງບັນຊີ - ເພີ່ມການຕິດຕາມ - ເພີ່ມ %s ແລ້ວ - ຊິງຄ໌ຂໍ້ມູນ - ໃຫ້ຄະແນນແລ້ວ - %d / 10 - /?? - /%d - %s ຢືນຢັນຕົວຕົນແລ້ວ - ບໍ່ສາມາດເຂົ້າສູ່ລະບົບໄດ້ທີ່ %s - ປິດໃຊ້ງານ - ບໍ່ມີ - ປົກກະຕິ - ທັງໝົດ - ສູງສຸດ - ຕ່ຳສຸດ - ເສັ້ນຂອບ - ຍຸບລົງ - ເງົາ - ນູນຂຶ້ນ - ປັບເວລາຄຳບັນຍາຍ - 1000 ms - ຄວາມຊັກຊ້າຂອງຄຳບັນຍາຍ - ໃຊ້ສິ່ງນີ້ຖ້າຄຳບັນຍາຍສະແດງໄວເກີນໄປ %d ms - ໃຊ້ສິ່ງນີ້ຖ້າຄຳບັນຍາຍສະແດງຊ້າເກີນໄປ %d ms - ບໍ່ມີການຊັກຊ້າຂອງຄຳບັນຍາຍ - ເຝີດີມີຊີ້ນໝູອົບອັດແໜ້ນ - ແນະນຳ - ໂຫຼດ %s ແລ້ວ - ໂຫຼດຈາກໄຟລ໌ - ໂຫຼດຈາກອິນເຕີເນັດ - ໂຫຼດອັນທຳອິດທີ່ມີ - ໄຟລ໌ທີ່ດາວໂຫຼດແລ້ວ - ນັກສະແດງຫຼັກ - ນັກສະແດງສົມທົບ - ນັກສະແດງພື້ນຫຼັງ - ແຫຼ່ງຂໍ້ມູນ - ສຸ່ມ - ໄວໆນີ້… - ກ້ອງ - ກ້ອງ - ກ້ອງ - HQ - HD - TS - TC - Blu-ray - WP - DVD - 4K - SD - UHD - HDR - SDR - ເວັບ - ຮູບໂປສເຕີ - ຮູບ QR Code - ເຄື່ອງຫຼິ້ນ - ຄວາມລະອຽດ ແລະ ຊື່ - ຊື່ - ຄວາມລະອຽດ - ຂໍ້ມູນສື່ - ID ບໍ່ຖືກຕ້ອງ - ຂໍ້ມູນບໍ່ຖືກຕ້ອງ - URL ບໍ່ຖືກຕ້ອງ - ຂໍ້ຜິດພາດ - ເອົາຄຳອະທິບາຍສຽງອອກຈາກຄຳບັນຍາຍ - ເອົາສ່ວນເກີນອອກຈາກຄຳບັນຍາຍ - ກັ່ນຕອງຕາມພາສາສື່ທີ່ມັກ - ເພີ່ມເຕີມ - ຕົວຢ່າງໜັງ - https://example.com/example.mp4 - Referer (ທາງເລືອກ) - ຖັດໄປ - ເບິ່ງວິດີໂອໃນພາສາເຫຼົ່ານີ້ - ກ່ອນໜ້າ - ຂ້າມການຕັ້ງຄ່າ - ປ່ຽນຮູບລັກສະນະຂອງແອັບໃຫ້ເໝາະສົມກັບອຸປະກອນຂອງທ່ານ - ພາກສ່ວນໃດທີ່ເຈົ້າຢາກເບິ່ງ - ສຳເລັດ - ສ່ວນເສີມ - ເພີ່ມບ່ອນເກັບຂໍ້ມູນ (Repository) - ຊື່ບ່ອນເກັບຂໍ້ມູນ (ທາງເລືອກ) - URL ບ່ອນເກັບຂໍ້ມູນ ຫຼື ລະຫັດສັ້ນ - ໂຫຼດປລັກອິນແລ້ວ - ດາວໂຫຼດປລັກອິນແລ້ວ - ລົບປລັກອິນແລ້ວ - ບໍ່ສາມາດໂຫຼດ %s ໄດ້ - 18+ - ເລີ່ມດາວໂຫຼດ %1$d %2$s… - ດາວໂຫຼດ %1$d %2$s ສຳເລັດ - %s ທັງໝົດຖືກດາວໂຫຼດແລ້ວ - ບໍ່ພົບປລັກອິນໃນບ່ອນເກັບຂໍ້ມູນ - ບໍ່ພົບບ່ອນເກັບຂໍ້ມູນ, ກວດເບິ່ງ URL ຫຼື ລອງໃຊ້ VPN - ດາວໂຫຼດເປັນກຸ່ມ - ປລັກອິນ - ປລັກອິນ - ນີ້ຈະລົບປລັກອິນທັງໝົດໃນບ່ອນເກັບຂໍ້ມູນນີ້ຄືກັນ - ລົບບ່ອນເກັບຂໍ້ມູນ - ລົບປລັກອິນ - ດາວໂຫຼດລາຍຊື່ເວັບໄຊທີ່ເຈົ້າຕ້ອງການໃຊ້ - ດາວໂຫຼດແລ້ວ: %d - ປິດການໃຊ້ງານ: %d - ຍັງບໍ່ໄດ້ດາວໂຫຼດ: %d - ອັບເດດແລ້ວ %d ປລັກອິນ - CloudStream ບໍ່ມີເວັບໄຊຕິດຕັ້ງມາໃຫ້ແຕ່ທຳອິດ. ເຈົ້າຕ້ອງຕິດຕັ້ງເວັບໄຊຈາກບ່ອນເກັບຂໍ້ມູນ (Repositories). \n \nເຂົ້າຮ່ວມ Discord ຂອງພວກເຮົາ ຫຼື ຄົ້ນຫາທາງອອນລາຍ. - ເບິ່ງບ່ອນເກັບຂໍ້ມູນຂອງຊຸມຊົນ - ລາຍການສາທາລະນະ - ເຮັດໃຫ້ຄຳບັນຍາຍທັງໝົດເປັນຕົວພິມໃຫຍ່ - ຄຳເຕືອນ: CloudStream ບໍ່ມີສ່ວນຮັບຜິດຊອບໃດໆໃນການໃຊ້ສ່ວນເສີມຈາກບຸກຄົນທີສາມ ແລະ ບໍ່ມີການຊ່ວຍເຫຼືອສຳລັບພວກມັນ! - %s (ປິດໃຊ້ງານ) - ແທຣັກ - ແທຣັກສຽງ - ແທຣັກວິດີໂອ - ເລີ່ມແອັບໃໝ່ເພື່ອເບິ່ງການປ່ຽນແປງ. - ເລີ່ມໃໝ່ - ຢຸດ - ເປີດໂໝດປອດໄພ - ສ່ວນເສີມທັງໝົດຖືກປິດເນື່ອງຈາກແອັບຂັດຂ້ອງ ເພື່ອຊ່ວຍໃຫ້ເຈົ້າຫາຕົວທີ່ເຮັດໃຫ້ເກີດບັນຫາ. - ເບິ່ງຂໍ້ມູນການຂັດຂ້ອງ - ຄະແນນ: %s - ລາຍລະອຽດ - ເວີຊັນ - ສະຖານະ - ຂະໜາດ - ຜູ້ຂຽນ - ທີ່ຮອງຮັບ - ພາສາ - ກະລຸນາຕິດຕັ້ງສ່ວນເສີມກ່ອນ - HLS Playlist - ເຄື່ອງຫຼິ້ນວິດີໂອທີ່ມັກ - ເຄື່ອງຫຼິ້ນພາຍໃນແອັບ - ຖາມທຸກຄັ້ງ - ເລືອກອຸປະກອນ Cast - ບໍ່ພົບແອັບ - ທຸກພາສາ - ຂ້າມ %s - ເພງເປີດ - ເພງປິດ - ສະຫຼຸບຄວາມເດີມ - ເພງປິດແບບປະສົມ - ເພງເປີດແບບປະສົມ - ລາຍຊື່ຜູ້ສ້າງ - ຕົວຢ່າງຕອນຕໍ່ໄປ - ບົດນຳ - ລ້າງປະຫວັດ - ປະຫວັດ - ສະແດງປຸ່ມຂ້າມສຳລັບເພງເປີດ/ເພງປິດ - ຂໍ້ຄວາມຫຼາຍເກີນໄປ. ບໍ່ສາມາດບັນທຶກໃສ່ຄລິບບອດໄດ້. - ເກີດຂໍ້ຜິດພາດໃນການເຂົ້າເຖິງຄລິບບອດ, ກະລຸນາລອງໃໝ່. - ເກີດຂໍ້ຜິດພາດໃນການຄັດລອກ, ກະລຸນາຄັດລອກ logcat ແລະ ຕິດຕໍ່ທີມງານ. - ໝາຍວ່າເບິ່ງແລ້ວ - ເອົາອອກຈາກປະຫວັດການເບິ່ງ - ເຈົ້າແນ່ໃຈບໍ່ວ່າຕ້ອງການອອກ? - ແມ່ນ - ບໍ່ - ຕົກລົງ - ຍົກເລີກ - ເປີດບ່ອນເກັບຂໍ້ມູນ - ປິດການປະຢັດແບັດເຕີຣີ - ເພື່ອໃຫ້ການດາວໂຫຼດ ແລະ ການແຈ້ງເຕືອນລາຍການທີ່ຕິດຕາມບໍ່ຖືກຂັດຈັງຫວະ, CloudStream ຕ້ອງການການອະນຸຍາດໃຫ້ເຮັດວຽກຢູ່ພື້ນຫຼັງ. ເມື່ອກົດ ຕົກລົງ, ເຈົ້າຈະເຫັນໜ້າຕ່າງຂໍອະນຸຍາດ, ກະລຸນາກົດ \'ອະນຸຍາດ\'.\n\nໝາຍເຫດ: ການອະນຸຍາດນີ້ບໍ່ໄດ້ໝາຍຄວາມວ່າແອັບຈະກິນແບັດເຕີຣີ. ມັນຈະເຮັດວຽກຢູ່ພື້ນຫຼັງສະເພາະເວລາທີ່ຈຳເປັນ ເຊັ່ນ: ເວລາຮັບການແຈ້ງເຕືອນ ຫຼື ດາວໂຫຼດວິດີໂອ. - ການໃຊ້ແບັດເຕີຣີຂອງແອັບຖືກຕັ້ງເປັນແບບບໍ່ຈຳກັດແລ້ວ - ບໍ່ສາມາດເປີດຂໍ້ມູນແອັບ CloudStream ໄດ້. - ກຳລັງດາວໂຫຼດອັບເດດແອັບ… - ກຳລັງຕິດຕັ້ງອັບເດດແອັບ… - ບໍ່ສາມາດຕິດຕັ້ງເວີຊັນໃໝ່ຂອງແອັບໄດ້ - ແບບເກົ່າ - PackageInstaller - ແອັບຈະຖືກອັບເດດເມື່ອທ່ານອອກ - ຈັດຮຽງຕາມ - ຈັດຮຽງ - ຄະແນນ (ສູງ ຫາ ຕ່ຳ) - ຄະແນນ (ຕ່ຳ ຫາ ສູງ) - ອັບເດດ (ໃໝ່ ຫາ ເກົ່າ) - ອັບເດດ (ເກົ່າ ຫາ ໃໝ່) - ຕາມຕົວອັກສອນ (ກ ຫາ ຮ) - ຕາມຕົວອັກສອນ (ຮ ຫາ ກ) - ຕອນ (ນ້ອຍ ຫາ ຫຼາຍ) - ຕອນ (ຫຼາຍ ຫາ ນ້ອຍ) - ຄະແນນ (ສູງສຸດ) - ຄະແນນ (ຕ່ຳສຸດ) - ວັນທີສາຍ (ໃໝ່ສຸດ) - ວັນທີສາຍ (ເກົ່າສຸດ) - ຕອນ %s - ຄະແນນ %s - ວັນທີ %s - ເລືອກຫ້ອງສະໝຸດ - ເປີດດ້ວຍ - ຫ້ອງສະໝຸດຂອງເຈົ້າຍັງວ່າງ :( \nເຂົ້າສູ່ລະບົບບັນຊີຫ້ອງສະໝຸດ ຫຼື ເພີ່ມລາຍການໃສ່ຫ້ອງສະໝຸດໃນເຄື່ອງ. - ລາຍການນີ້ຍັງວ່າງ. ລອງສະຫຼັບໄປອັນອື່ນເບິ່ງ. - ພົບໄຟລ໌ໂໝດປອດໄພ!\nຈະບໍ່ໂຫຼດສ່ວນເສີມໃດໆຕອນເລີ່ມຕົ້ນ ຈົນກວ່າໄຟລ໌ຈະຖືກລົບອອກ. - ຄືນຄ່າ - ກຳລັງອັບເດດລາຍການທີ່ຕິດຕາມ - ຕິດຕາມແລ້ວ - ຕິດຕາມ %s ແລ້ວ - ຍົກເລີກການຕິດຕາມ %s ແລ້ວ - ຕອນທີ %d ອອກແລ້ວ! - ຕິດຕາມ - ຍົກເລີກການຕິດຕາມ - ໂປຣໄຟລ໌ %d - ຕັ້ງຄ່າໂປຣໄຟລ໌ - ເຊື່ອງແຫຼ່ງຂໍ້ມູນທີ່ມີລຳດັບຄວາມສຳຄັນຕິດລົບ - ເຊື່ອງແຫຼ່ງຂໍ້ມູນທີ່ມີຂໍ້ຜິດພາດ - - ເຊື່ອງ %d ລິ້ງ - - Wi-Fi - ຂໍ້ມູນມືຖື - ຕັ້ງເປັນຄ່າເລີ່ມຕົ້ນ - ໃຊ້ງານ - ແກ້ໄຂ - ລຳດັບຄວາມສຳຄັນຂອງແຫຼ່ງຂໍ້ມູນ - ຕັດສິນໃຈວ່າຈະໃຫ້ຈັດຮຽງແຫຼ່ງວິດີໂອແນວໃດໃນເຄື່ອງຫຼິ້ນ - ໂປຣໄຟລ໌ - ຊ່ວຍເຫຼືອ - ຢູ່ບ່ອນນີ້ ເຈົ້າສາມາດປ່ຽນວິທີການຈັດຮຽງແຫຼ່ງຂໍ້ມູນ. ຖ້າວິດີໂອມີລຳດັບຄວາມສຳຄັນສູງກວ່າ ມັນຈະປາກົດຢູ່ທາງເທິງໃນການເລືອກແຫຼ່ງຂໍ້ມູນ. ຜົນລວມຂອງລຳດັບຄວາມສຳຄັນຂອງແຫຼ່ງຂໍ້ມູນ ແລະ ຄຸນນະພາບ ແມ່ນລຳດັບຄວາມສຳຄັນຂອງວິດີໂອ. \n\nແຫຼ່ງຂໍ້ມູນ A: 3 \nຄຸນນະພາບ B: 7 \nຈະມີລຳດັບຄວາມສຳຄັນລວມແມ່ນ 10. \n\nໝາຍເຫດ: ຖ້າຜົນລວມແມ່ນ 10 ຫຼື ຫຼາຍກວ່າ, ເຄື່ອງຫຼິ້ນຈະຂ້າມການໂຫຼດໂດຍອັດຕະໂນມັດເມື່ອລິ້ງນັ້ນຖືກໂຫຼດ! - ຄຸນນະພາບ - ພື້ນຫຼັງໂປຣໄຟລ໌ - UI ບໍ່ສາມາດສ້າງໄດ້ຢ່າງຖືກຕ້ອງ, ນີ້ແມ່ນບັກໃຫຍ່ ແລະ ຄວນລາຍງານທັນທີ %s - ເຈົ້າໄດ້ລົງຄະແນນແລ້ວ - ລາຍການທີ່ມັກ - ເພີ່ມ %s ເຂົ້າໃນລາຍການທີ່ມັກແລ້ວ - ເອົາ %s ອອກຈາກລາຍການທີ່ມັກແລ້ວ - ເພີ່ມເຂົ້າໃນລາຍການທີ່ມັກ - ເອົາອອກຈາກລາຍການທີ່ມັກ - ພົບລາຍການທີ່ອາດຈະຊ້ຳກັນ - ເພີ່ມ - ແທນທີ່ - ແທນທີ່ທັງໝົດ - ເບິ່ງຄືວ່າມີລາຍການທີ່ອາດຈະຊ້ຳກັນຢູ່ໃນຫ້ອງສະໝຸດຂອງເຈົ້າ: \'%s.\' \n\nເຈົ້າຕ້ອງການເພີ່ມລາຍການນີ້ຕໍ່ໄປ, ແທນທີ່ອັນເກົ່າ ຫຼື ຍົກເລີກການກະທຳ? - ພົບລາຍການທີ່ອາດຈະຊ້ຳກັນໃນຫ້ອງສະໝຸດຂອງເຈົ້າ: \n\n%s \n\nເຈົ້າຕ້ອງການເພີ່ມລາຍການນີ້ຕໍ່ໄປ, ແທນທີ່ອັນເກົ່າ ຫຼື ຍົກເລີກການກະທຳ? - ໃສ່ PIN - ໃສ່ PIN ສຳລັບ %s - ໃສ່ PIN ປັດຈຸບັນ - ລັອກໂປຣໄຟລ໌ - PIN - PIN ບໍ່ຖືກຕ້ອງ. ກະລຸນາລອງໃໝ່. - PIN ຕ້ອງມີ 4 ຕົວ - ເລືອກບັນຊີ - ບໍ່ມີບັນຊີ - ຈັດການບັນຊີ - ແກ້ໄຂບັນຊີ - ເຂົ້າສູ່ລະບົບໂດຍ %s - ຂ້າມການເລືອກບັນຊີຕອນເລີ່ມຕົ້ນ - ໃຊ້ບັນຊີເລີ່ມຕົ້ນ - ໝຸນວິດີໂອ - ສະແດງປຸ່ມສະຫຼັບການວາງແນວໜ້າຈໍ - ເປີດການສະຫຼັບແນວໜ້າຈໍອັດຕະໂນມັດຕາມແນວວິດີໂອ - ໝຸນອັດຕະໂນມັດ - ມັກ - ເລີກມັກ - ປົດລັອກ CloudStream - ລັອກດ້ວຍໄບໂອເມທຣິກ (ລາຍນິ້ວມື/ໃບໜ້າ) - ການຢືນຢັນດ້ວຍລະຫັດຜ່ານ/PIN - ອຸປະກອນນີ້ບໍ່ຮອງຮັບການຢືນຢັນດ້ວຍໄບໂອເມທຣິກ - ປົດລັອກແອັບດ້ວຍລາຍນິ້ວມື, ໃບໜ້າ, PIN, ຮູບແບບ ຫຼື ລະຫັດຜ່ານ. - ຫຼັງຈາກພະຍາຍາມລົ້ມເຫຼວຫຼາຍຄັ້ງ, ໜ້າຕ່າງຈະປິດລົງ. ໃຫ້ເລີ່ມແອັບໃໝ່ເພື່ອລອງອີກຄັ້ງ. - ຂໍ້ມູນ CloudStream ຂອງທ່ານຖືກສຳຮອງໄວ້ແລ້ວ. ເຖິງແມ່ນວ່າມັນຈະເກີດຂຶ້ນໄດ້ຍາກ, ແຕ່ອຸປະກອນແຕ່ລະເຄື່ອງອາດເຮັດວຽກຕ່າງກັນ. ໃນກໍລະນີທີ່ທ່ານຖືກລັອກບໍ່ໃຫ້ເຂົ້າເຖິງແອັບ, ໃຫ້ລ້າງຂໍ້ມູນແອັບທັງໝົດ ແລະ ກູ້ຄືນຈາກໄຟລ໌ສຳຮອງ. ພວກເຮົາຂໍອະໄພໃນຄວາມບໍ່ສະດວກ. - ຕັ້ງຄ່າໃໝ່ - CloudStream Wiki - ເຂົ້າໄປທີ່ %s ໃນມືຖື ຫຼື ຄອມພິວເຕີຂອງທ່ານ ແລະ ໃສ່ລະຫັດຂ້າງເທິງ - ບໍ່ສາມາດຮັບລະຫັດ PIN ຂອງອຸປະກອນໄດ້, ລອງຢືນຢັນຕົວຕົນພາຍໃນເຄື່ອງ - ລະຫັດ PIN ໝົດອາຍຸແລ້ວ! - ລະຫັດຈະໝົດອາຍຸໃນ %1$dm %2$ds - ວັນທີວາງຈຳໜ່າຍ (ໃໝ່ ຫາ ເກົ່າ) - ວັນທີວາງຈຳໜ່າຍ (ເກົ່າ ຫາ ໃໝ່) - ເຊື່ອງຊື່ປຸ່ມຄວບຄຸມໃນເຄື່ອງຫຼິ້ນ - ຕົວຢ່າງຮູບໃນແຖບເລື່ອນເວລາ - ເປີດສະແດງຮູບຕົວຢ່າງເທິງແຖບເລື່ອນເວລາ - ຍັງບໍ່ໄດ້ໂຫຼດຄຳບັນຍາຍ - ຕຳແໜ່ງໂຟນເດີສຳຮອງຂໍ້ມູນ - ກຳນົດເອງ - ຢືນຢັນກ່ອນອອກ - ສະແດງໜ້າຕ່າງຢືນຢັນກ່ອນອອກຈາກແອັບ - ສະແດງ - ບໍ່ສະແດງ - ຂະໜາດເສັ້ນຂອບ - ເປີດໃຊ້ torrent ໃນ ຕັ້ງຄ່າ/ຜູ້ໃຫ້ບໍລິການ/ສື່ທີ່ມັກ - ເລີ່ມແອັບໃໝ່ ແລະ ກົດຍອມຮັບ Stream Torrent ເພື່ອດຳເນີນການຕໍ່. - ການຖອດລະຫັດດ້ວຍຊອບແວ - ການຖອດລະຫັດດ້ວຍຊອບແວຊ່ວຍໃຫ້ຫຼິ້ນໄຟລ໌ທີ່ອຸປະກອນບໍ່ຮອງຮັບໄດ້, ແຕ່ອາດເຮັດໃຫ້ກະຕຸກ ຫຼື ບໍ່ສະຖຽນໃນຄວາມລະອຽດສູງ. - ລະດັບສຽງເກີນ 100% ແລ້ວ - ເລື່ອນຂຶ້ນອີກຄັ້ງເພື່ອໃຫ້ສຽງດັງເກີນ 100% - ອັບເດດປລັກອິນ - ອັບເດດປລັກອິນດ້ວຍຕົນເອງ - ເລີ່ມຂະບວນການອັບເດດປລັກອິນ! - ອັບເດດ %d ປລັກອິນສຳເລັດແລ້ວ - ບໍ່ມີປລັກອິນໃດຖືກອັບເດດ. - ການແຈ້ງເຕືອນເຄື່ອງຫຼິ້ນ - ການແຈ້ງເຕືອນເຄື່ອງຫຼິ້ນສຳລັບຄວບຄຸມການຫຼິ້ນຈາກພື້ນຫຼັງ - ຝັງໃນຕົວ - ອອນລາຍ - ເຮັດໃຫ້ຄຳບັນຍາຍທັງໝົດເປັນຕົວເຂັ້ມ - ເຮັດໃຫ້ຄຳບັນຍາຍທັງໝົດເປັນຕົວອຽງ - ຄວາມມົນຂອງພື້ນຫຼັງ - ຈຳນວນລາຍການທີ່ສາມາດດາວໂຫຼດພ້ອມກັນໄດ້ - ດາວໂຫຼດຂະໜານ - ການເຊື່ອມຕໍ່ພ້ອມກັນ - ຈຳນວນການເຊື່ອມຕໍ່ພ້ອມກັນທີ່ແຕ່ລະການດາວໂຫຼດສາມາດໃຊ້ໄດ້ - ໄປທີ່ລາຍການດາວໂຫຼດ - ບໍ່ມີການເຊື່ອມຕໍ່ອິນເຕີເນັດ. \n\nກະລຸນາເຊື່ອມຕໍ່ອິນເຕີເນັດແລ້ວລອງໃໝ່, ຫຼື ເບິ່ງໄຟລ໌ທີ່ດາວໂຫຼດໄວ້ໃນຂະນະທີ່ອອບລາຍ. - ປ່ຽນຂອບເຂດຂອງໜ້າຈໍ - Overscan - ປ່ຽນຂະໜາດຂອງໂປສເຕີ - ຂະໜາດໂປສເຕີ - ກົດຄ້າງເພື່ອສະຫຼັບຄວາມໄວ - ກົດຄ້າງໄວ້ເພື່ອຫຼິ້ນດ້ວຍຄວາມໄວ 2 ເທົ່າ - ແກ້ໄຂຮູບໂປຣໄຟລ໌ - ໃສ່ URL ຮູບໂປຣໄຟລ໌ - ບໍ່ພົບ URL - URL ຫຼື ຮູບພາບບໍ່ຖືກຕ້ອງ - ອັບເດດຮູບພາບສຳເລັດແລ້ວ - ໝາຍວ່າເບິ່ງແລ້ວຈົນຮອດຕອນນີ້ - ເອົາເຄື່ອງໝາຍເບິ່ງແລ້ວອອກຈົນຮອດຕອນນີ້ - ໂຫຼດໃໝ່ແລ້ວ - ໂຫຼດຜູ້ໃຫ້ບໍລິການໃໝ່ - ຊື່ - ຊື່ແຫຼ່ງຂໍ້ມູນ - ຄວາມລະອຽດ ແລະ ຊື່ - ດາວໂຫຼດທັງໝົດ - ຍົກເລີກທັງໝົດ - ເຈົ້າຕ້ອງການດາວໂຫຼດຕອນ %s ບໍ່? - ເຈົ້າຕ້ອງການຍົກເລີກການດາວໂຫຼດທີ່ຄ້າງຢູ່ໃນຄິວທັງໝົດບໍ່? - ການຈັດວາງຄຳບັນຍາຍ - ລຸ່ມຊ້າຍ - ລຸ່ມກາງ - ລຸ່ມຂວາ - ກາງຊ້າຍ - ກາງ - ກາງຂວາ - ເທິງຊ້າຍ - ເທິງກາງ - ເທິງຂວາ - - ກຳລັງດາວໂຫຼດ %d ລາຍການ - - - %d ລາຍການຢູ່ໃນຄິວ - - ຖ່າຍທອດສົດ - diff --git a/app/src/main/res/values-b+sq/strings.xml b/app/src/main/res/values-sq/strings.xml similarity index 62% rename from app/src/main/res/values-b+sq/strings.xml rename to app/src/main/res/values-sq/strings.xml index d083809bb..51d2379de 100644 --- a/app/src/main/res/values-b+sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -75,7 +75,7 @@ E disponueshme për shikim offline Zgjidh të gjitha Çzgjidh të gjitha - Përditësimi filloi + Update-i filloi Transmetim në rrjet Hap video lokale Gabim gjatë ngarkimit të lidhjeve @@ -83,7 +83,7 @@ Memorie e brendshme Dublim Titra - Fshi skedarin + Fshi Skedarin Luaj skedarin Rifillo shkarkimin Ndalo shkarkimin @@ -117,7 +117,7 @@ Madhësia e shkrimit Kërko duke përdorur ofruesit Kërko duke përdorur tipet - %d Banane të dhëna për developer-at + %d Banane të dhëna per developer-at Asnjë Banane e dhënë Zgjedhje automatike e gjuhës Shkarko gjuhët @@ -127,7 +127,7 @@ Vazhdo shikimin Hiq Më shumë informacion - @string/home_play + \@string/home_play Një VPN mund të nevojitet që ky ofrues të funksionojë në rregull Ky ofrues është Torrent, rekomandohet një VPN Metadata nuk ofrohet nga kjo faqe, ngarkimi i videos do të dështojë nëse nuk ekziston në këtë faqe. @@ -216,7 +216,7 @@ %1$s %2$d%3$s Asnje sezon Episodi - Episode + Episodet %1$d-%2$d %1$d %2$s Vazhdon në %s @@ -224,8 +224,8 @@ E Nuk u gjet asnjë episod Fshij - Fshi skedarin - Fshi skedarët + Fshij skedarin + Fshij skedarët Fshij (%1$d | %2$s) Anulo Ndalo @@ -269,7 +269,7 @@ NSFW Tjera Film - Serial + Seri Vizatimorë Anime OVA @@ -309,7 +309,7 @@ Teksti i episodit Zgjidh elementët e ndërfaqes mbi poster Nuk u gjet asnjë përditësim - Kontrollo për përditësime + Kontrollo për përditësim Çelës Ndrysho madhësinë Burimi @@ -494,254 +494,4 @@ Të gjitha %s janë shkarkuar tashmë Asnjë shtesë nuk u gjet në repository Repository nuk u gjet, verifiko URL-në ose provoje me VPN - Të gjitha shtesat u ndaluan për shkak të një gabimi për të ndihmuar në gjetjen e shkaktarit. - Lista publike - CloudStream nuk vjen me faqe të instaluara. Duhet ti shtosh faqet nga repository-t.\n\nNa u bashko në Discord ose kërko në internet. - Autorët - shtesë - Titrat vetëm me shkronja të mëdha - Luajtësi i preferuar i videove - Përshkrimi - Shiko repository-t e komunitetit - Modaliteti i sigurt është aktive - Të pa shkarkuara: %d - Instalo shtesën para - Rifillo aplikacionin për të parë ndryshimet. - Çaktivizuar: %d - Pauzë - Gjurmë - Shkarkimi në grup përfundoi - Kjo do të heqë të gjitha shtesat - Gjuha - Rifillo - Fshi shtesën - Madhësia - Shiko të dhënat e gabimit - Të suportuara - Shkarkuar: %d - Gjurmë video-je - U përditësuan %d shtesa - Lista e HLS - shtesa - Vlerësimi: %s - Paralajmërim: CloudStream nuk mban asnjë përgjegjësi për përdorimin e shtesave të palëve të treta dhe nuk ofron asnjë mbështetje për to! - Statusi - Fshi repository-n - Versioni - %s (Çaktivizuar) - Shkarko listën e faqeve që dëshiron të përdorësh - Gjurmë audio-je - Video - Luajtësi i integruar - Pyet gjithmonë - Zgjidh pajisjen për transmetim - Aplikacioni nuk u gjet - Të gjitha gjuhët - Kalo %s - Hapjen - Mbylljen - Përmbledhjen - Mbylljen e përzier - Hapjen e përzier - Kreditet - Parashikimin - Intron - Pastro historikun - Historia - Shfaq butonat për kapërcimin e hyrjes/mbylljes - Teksti është shumë i gjatë. Nuk mund të ruhet në Clipboard. - Gabim gjatë aksesimit të Clipboard-it. Ju lutem provoni përsëri. - Kopjimi dështoi. Kopjo logcat-in dhe kontakto support-in e aplikacionit. - Shënoje si e përfunduar - Hiqe nga të shikuarat - Jeni i sigurt që dëshironi të dilni? - Po - Jo - Në rregull - Largo - Hap repository-n - Çaktivizo kursimin e baterisë - Për të siguruar shkarkime të pandërprera dhe njoftime për serialet e abonuar, CloudStream ka nevojë për leje për të funksionuar në sfond. Duke shtypur OK, do t’ju shfaqet një dialog kërkese. Ju lutem shtypni “Lejo”.\n\nJu lutemi vini re: kjo leje nuk do të thotë që CS3 do të harxhojë baterinë tuaj. Do të funksionojë në sfond vetëm kur është e nevojshme, si p.sh. kur merr njoftime ose shkarkon video nga shtesat zyrtare. - Përdorimi i baterisë është i vendosur si i pakufizuar - Informacioni i aplikacionit CloudStream nuk mund të hapet. - Po shkarkohet përditësimi i aplikacionit… - Po instalohet përditësimi i aplikacionit… - Instalimi i versionit të ri dështoi - Version i vjetër - Instaluesi i paketës - Aplikacioni do të përditësohet sapo të mbyllet - Rendit nga - Rendit - Vlerësimi (nga më i larti te më i ulëti) - Vlerësimi (nga më i ulëti te më i larti) - Përditësuar (nga më i riu te më i vjetri) - Përditësuar (nga më i vjetri te më i riu) - Alfabetike (A–Z) - Alfabetike (Z–A) - Episodet (rritëse) - Episodet (zbritëse) - Vlerësimi (më i larti) - Vlerësimi (më i ulëti) - Data e publikimit (më e fundit) - Data e publikimit (më e hershmja) - Ep %s - Vlerësimi %s - Data %s - Hape me - Kjo listë është bosh. Provo një tjetër. - U gjet skedari i modalitetit të sigurt!\nNuk do të ngarkohen asnjë shtesë gjatë nisjes derisa skedari të hiqet. - Riktheje në gjendjen e mëparshme - Duke përditësuar serialet që ndiqni - I abonuar - I abonuar në %s - U çabonove nga %s - Episodi %d u publikua! - Abonohu - Çabonohu - Profili %d - Wi-Fi - Mobile data - Vendose si parazgjedhje - Përdor - Ndrysho - Prioriteti i burimit - Vendos si duhet të renditen burimet e videos në luajtës - Profilet - Ndihmë - Këtu mund të ndryshoni si renditen burimet. Nëse një video ka prioritet më të lartë, do të shfaqet më lart në përzgjedhjen e burimeve. Shuma e prioritetit të burimit dhe prioritetit të cilësisë është prioriteti i videos.\n\nBurimi A: 3\nCilësia B: 7\nDo të ketë një prioritet të kombinuar video-je prej 10.\n\nSHËNIM: Nëse shuma është 10 ose më shumë, luajtësi do të kapërcejë automatikisht ngarkimin kur të hapet ai link! - Cilësitë - Sfondi i profilit - UI nuk mundi të krijohej siç duhet, ky është një PROBLEM SERIOZ dhe duhet raportuar menjëherë %s - Ju keni votuar - Të preferuarat - %s u shtua te të preferuarat - %s u hoq nga të preferuarat - Shto te të preferuarat - Hiqe nga të preferuarat - U gjet dublikatë e mundshme - Shto - Zëvendëso - Zëvendëso të gjitha - Duket se një artikull i mundshëm i dyfishuar tashmë ekziston në bibliotekën tuaj: ‘%s’.\n\nDëshironi ta shtoni gjithsesi, ta zëvendësoni atë ekzistuesin, apo ta anuloni veprimin? - Janë gjetur artikuj të mundshëm të dyfishtë në bibliotekën tuaj:\n\n%s\n\nDëshironi ta shtoni gjithsesi, të zëvendësoni ato ekzistuesit, apo të anuloni veprimin? - Fut PIN-in - Fut PIN-in për %s - Fut PIN-in aktual - Kyç profilin - PIN - PIN-i i gabuar. Ju lutem provoni përsëri. - PIN-i duhet të ketë 4 karaktere - Zgjidh një Llogari - Asnjë llogari - Menaxho Llogaritë - Ndrysho llogarinë - I identifikuar si %s - Kapërce përzgjedhjen e llogarisë gjatë nisjes - Përdor llogarinë e paracaktuar - Rrotullo - Shfaq një buton për ndryshimin e orientimit të ekranit - Aktivizo rrotullimin automatik të ekranit sipas orientimit të videos - Rrotullim automatik - I preferuar - Hiq nga të preferuarat - Hap CloudStream-in - Kyç me biometrikë - Verifikim me fjalëkalim/PIN - Biometrikat nuk janë të disponueshme në këtë pajisje - Hap aplikacionin me gjurmë gishti, Face ID, PIN, Pattern ose fjalëkalim. - Pas disa përpjekjesh të dështuara, rifillo aplikacionin për ta provuar përsëri. - Të dhënat tuaja të CloudStream janë kopjuar tani. Edhe pse mundësia është shumë e ulët, disa pajisje mund të sillen ndryshe. Në raste të rralla, nëse bllokoheni nga hyrja në aplikacion, fshini plotësisht të dhënat e aplikacionit dhe rikthejini ato nga kopja rezervë. Na vjen shumë keq për çdo shqetësim që mund të shkaktohet nga kjo. - Rivendos - CloudStream Wiki - Vizitoni %s në telefonin ose kompjuterin tuaj dhe futni kodin e mësipërm - Nuk u mor kodi PIN i pajisjes, provo verifikim lokal - PIN-i ka skaduar! - Kodi skadon pas %1$dm %2$ds - Data e publikimit (nga më e reja te më e vjetra) - Data e publikimit (nga më e vjetra te më e reja) - Fshih emrat e kontrolleve të luajtësit - Shiriti i kërkimit - Aktivizo miniaturën e pamjes paraprake në shiritin kërkues - Nuk ka titra të ngarkuara ende - Vendndodhja e dosjes së kopjes rezervë - I personalizuar - Konfirmo para daljes - Shfaq dialogun para daljes nga aplikacioni - Shfaqe - Mos e shfaq - Madhësia e konturit - Aktivizo torrent-in në Cilësimet/Ofruesit/Media e preferuar - Rifillo aplikacionin dhe prano dritaren “Stream Torrent” për të vazhduar. - Dekodim me softuer - Dekodimi me softuer i lejon player-it të luajë video që nuk mbështeten nga pajisja juaj, por mund të shkaktojë vonesa ose paqëndrueshmëri në rezolucione të larta. - Volumi është mbi 100% - Rrëshqit lart përsëri për të kaluar mbi 100% - Përditëso shtesat - Përditëso shtesat manualisht - Filloi procesi i përditësimit të shtesave! - %d Shtes(a) u përditësuan me sukses - Nuk u përditësua asnjë shtesë. - Njoftimet e luajtësit - Njoftimi për kontrollin e luajtjes nga sfondi - I integruar - Online - Bëji të gjitha titrat me shkronja të trasha - Bëji të gjitha titrat me shkronja të pjerrëta - Rrezja e sfondit - Sa elementë të ndryshëm mund të shkarkohen njëkohësisht - Shkarkimet paralele - Lidhje paralele - Numri i lidhjeve paralele për çdo shkarkim - Shko tek Shkarkimet - Nuk ka lidhje interneti.\n\nJu lutemi lidhuni me internetin dhe provojeni përsëri, ose shikoni shkarkimet tuaja offline. - Ndryshon kufijtë e ekranit - Tejskenim - Ndryshon madhësinë e posterave - Madhësia e posterit - Ndërrimi i shpejtësisë me shtypje të gjatë - Mbaj shtypur për shpejtësi 2x - Ndrysho foton e profilit - Fut URL-n e fotos së profilit - Nuk u gjet asnjë URL - URL ose imazh i pavlefshëm - Imazhi u përditësua me sukses - Shëno si të parë deri në këtë episod - Hiq të parët deri në këtë episod - U ringarkuan - Ringarko ofruesin - Emri - Emri i burimit - Rezolucioni dhe emri - Shkarko të gjitha - Anulo të gjitha - Dëshiron të shkarkosh episodin %s? - Dëshiron të anulosh të gjitha shkarkimet në radhë? - Pozicionimi i titrave - Poshtë majtas - Poshtë në mes - Poshtë djathtas - Në mes majtas - Në mes në qendër - Në mes djathtas - Lart majtas - Lart në mes - Lart djathtas - - %d shkarkim aktiv - %d shkarkime aktive - - - %d shkarkim në radhë - %d shkarkime në radhë - - Live - Cilësimet e profil-it - Fshih burimet me prioritet negativ - Fshih burimet me error-e - - %d link i fshehur - %d linqe të fshehura - - Shfaq orën në kohë reale - Shfaq një orë në krye të ekranit. Aplikohet në faqen kryesore dhe në luajtës diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 53b29475b..7c71b5571 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -70,7 +70,6 @@ - diff --git a/app/src/main/res/values/donottranslate-strings.xml b/app/src/main/res/values/donottranslate-strings.xml index bf1566cdb..6a4c82713 100644 --- a/app/src/main/res/values/donottranslate-strings.xml +++ b/app/src/main/res/values/donottranslate-strings.xml @@ -28,7 +28,6 @@ use_system_brightness_key swipe_enabled_key playback_speed_enabled_key - compressor_enabled_key player_resize_enabled_key player_source_priority_key pip_enabled_key @@ -102,7 +101,6 @@ opensubtitles_key subdl_key animeskip_key - tv_layout_clock_key pref_category_security_key pref_category_gestures_key @@ -143,16 +141,16 @@ Any legal issues regarding the content on this application should be taken up with the actual file hosts and providers themselves as we are not affiliated with them. - \n\nIn case of copyright infringement, please directly contact the responsible parties or the streaming websites. + In case of copyright infringement, please directly contact the responsible parties or the streaming websites. - \n\nThe app is purely for educational and personal use. + The app is purely for educational and personal use. - \n\nCloudStream does not host any content on the app, and has no control over what media is put up or taken down. + CloudStream does not host any content on the app, and has no control over what media is put up or taken down. CloudStream functions like any other search engine, such as Google. CloudStream does not host, upload or - manage any videos, films or content. It simply crawls, aggregates and displays links in a convenient, + manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface. - \n\nIt merely scrapes 3rd-party websites that are publicly accessible via any regular web browser. It is the + It merely scrapes 3rd-party websites that are publicly accessible via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream at your own risk. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6c4295199..31cf951cf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -115,7 +115,6 @@ Outline Color Background Color Window Color - Edge Edge Type Subtitle Elevation Font @@ -140,7 +139,6 @@ No Plot Found No Description Found Show Logcat 🐈 - Logcat 🐈 Log Picture-in-picture Continues playback in a miniature player on top of other apps @@ -158,8 +156,6 @@ Slide up or down on the left or right side to change brightness or volume Autoplay next episode Start the next episode when the current one ends - Start videos paused - Reduce stress when selecting sources Double tap to seek Double tap to pause Player seek amount (Seconds) @@ -378,8 +374,6 @@ Looks Features General - Show real time clock - Show a real time clock at the top of the screen. Applies to the homepage and player Random Button Show random button on Homepage and Library Extension languages @@ -462,8 +456,8 @@ Random Coming soon… Cam - CamRip - CamHD + Cam + Cam HQ HD TS @@ -489,7 +483,7 @@ Invalid URL Error Remove closed captions from subtitles - Remove ads from subtitles + Remove bloat from subtitles Filter by preferred media language Extras Trailer @@ -536,19 +530,6 @@ Warning: CloudStream does not take any responsibility for using third-party extensions and does not provide any support for them! %s (Disabled) Tracks - Compressor - Dynamic Range Compressor - Show compressor controls in the player (helps balance loud/quiet audio) - On - On · %1$d dB / +%2$d dB - Off - Light - Dialogue Boost - Heavy - Threshold: %d dB - Makeup Gain: +%d dB - Sounds louder than %d dB get turned down - Turns up the overall volume by +%d dB Audio tracks Video tracks Restart the app to see changes. @@ -641,13 +622,6 @@ Subscribe Unsubscribe Profile %d - Profile settings - Hide sources with a negative priority - Hide sources with errors - - %d hidden link - %d hidden links - Wi-Fi Mobile data Set default @@ -750,7 +724,7 @@ Update plugins manually Starting plugin update process! - Successfully updated %d plugin(s) + Successfully updated %d plugin(s)! No plugins were updated. Player notifications The player notification for controlling the playback from the background @@ -759,7 +733,6 @@ Make all subtitles bold Make all subtitles italic Background Radius - Background How many different items that can be downloaded in parallel Parallel downloads Concurrent connections @@ -776,9 +749,6 @@ LongPress Speed Toggle Hold to get 2x speed Edit Profile Image - Url - Image picker - Default images Enter Profile Image URL No URL Found Invalid URL or Image @@ -813,5 +783,5 @@ %d downloads queued Live - Installed plugins + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a5f8492dc..f386bb62e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -63,7 +63,6 @@ @color/iconColor @color/white @color/black - @color/grayTextColor ?android:attr/selectableItemBackground @style/CustomPreferenceThemeOverlay @@ -179,7 +178,6 @@ @color/lightGrayTextColor @color/black @color/white - @color/transparent @color/blackText true true @@ -474,9 +472,6 @@