diff --git a/.github/workflows/issue_action.yml b/.github/workflows/issue_action.yml
index 79e7766c..28b737b3 100644
--- a/.github/workflows/issue_action.yml
+++ b/.github/workflows/issue_action.yml
@@ -18,7 +18,7 @@ jobs:
uses: actions-cool/issues-similarity-analysis@v1
with:
token: ${{ steps.generate_token.outputs.token }}
- filter-threshold: 0.5
+ filter-threshold: 0.60
title-excludes: ''
comment-title: |
### Your issue looks similar to these issues:
@@ -41,7 +41,7 @@ jobs:
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 "::set-output name=name::${RES}"
+ 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
diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml
index d58caf1e..edfb24be 100644
--- a/.github/workflows/prerelease.yml
+++ b/.github/workflows/prerelease.yml
@@ -33,7 +33,7 @@ jobs:
curl -H "Authorization: token ${{ secrets.PAT }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/KillerDogeEmpire/secrets/master/keystore_password.txt"
KEY_PWD="$(cat keystore_password.txt)"
echo "::add-mask::${KEY_PWD}"
- echo "::set-output name=key_pwd::$KEY_PWD"
+ echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
- name: Run Gradle
run: |
./gradlew assemblePrerelease makeJar androidSourcesJar
@@ -49,6 +49,6 @@ jobs:
prerelease: true
title: "Pre-release Build"
files: |
- app/build/outputs/apk/prerelease/*.apk
+ app/build/outputs/apk/prerelease/release/*.apk
app/build/libs/app-sources.jar
app/build/classes.jar
\ No newline at end of file
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 1a4db134..36199cd6 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -15,9 +15,9 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run Gradle
- run: ./gradlew assembleDebug
+ run: ./gradlew assemblePrereleaseDebug
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: pull-request-build
- path: "app/build/outputs/apk/debug/*.apk"
+ path: "app/build/outputs/apk/prerelease/debug/*.apk"
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index 652d9f3f..333d4937 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -31,5 +31,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index f81aa03c..79ce6a91 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@
***The list of supported languages:***
* 🇱🇧 Arabic
+* 🇧🇬 Bulgarian
* 🇭🇷 Croatian
* 🇨🇿 Czech
* 🇳🇱 Dutch
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 7cb6d2d7..00000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,233 +0,0 @@
-plugins {
- id 'com.android.application'
- id 'kotlin-android'
- id 'kotlin-kapt'
- id 'kotlin-android-extensions'
- id 'org.jetbrains.dokka'
-}
-
-def tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
-def allFilesFromDir = new File(tmpFilePath).listFiles()
-def prereleaseStoreFile = null
-if (allFilesFromDir != null) {
- prereleaseStoreFile = allFilesFromDir.first()
-}
-
-android {
- testOptions {
- unitTests.returnDefaultValues = true
- }
- signingConfigs {
- prerelease {
- if (prereleaseStoreFile != null) {
- storeFile = file(prereleaseStoreFile)
- storePassword System.getenv("SIGNING_STORE_PASSWORD")
- keyAlias System.getenv("SIGNING_KEY_ALIAS")
- keyPassword System.getenv("SIGNING_KEY_PASSWORD")
- }
- }
- }
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
-
- defaultConfig {
- applicationId "com.killerdogeempire.aquastream"
- minSdkVersion 21
- targetSdkVersion 30
-
- versionCode 52
- versionName "1.0.1"
-
- resValue "string", "app_version",
- "${defaultConfig.versionName}${versionNameSuffix ?: ""}"
-
- resValue "string", "commit_hash",
- ("git rev-parse --short HEAD".execute().text.trim() ?: "")
-
- resValue "bool", "is_prerelease", "false"
-
- buildConfigField("String", "BUILDDATE", "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));")
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-
- kapt {
- includeCompileClasspath = true
- }
- }
-
- buildTypes {
- release {
- debuggable false
- minifyEnabled false
- shrinkResources false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- resValue "bool", "is_prerelease", "false"
- }
- prerelease {
- applicationIdSuffix ".prerelease"
- buildConfigField("boolean", "BETA", "true")
- signingConfig signingConfigs.prerelease
- versionNameSuffix '-PRE'
- debuggable false
- minifyEnabled false
- shrinkResources false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- resValue "bool", "is_prerelease", "true"
- }
- debug {
- debuggable true
- applicationIdSuffix ".debug"
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- resValue "bool", "is_prerelease", "true"
- }
- }
- compileOptions {
- coreLibraryDesugaringEnabled true
-
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = '1.8'
- freeCompilerArgs = ['-Xjvm-default=compatibility']
- }
- lintOptions {
- checkReleaseBuilds false
- abortOnError false
- }
-}
-
-repositories {
- maven { url 'https://jitpack.io' }
-}
-
-dependencies {
- implementation 'com.google.android.mediahome:video:1.0.0'
- implementation 'androidx.test.ext:junit-ktx:1.1.3'
- testImplementation 'org.json:json:20180813'
-
- implementation 'androidx.core:core-ktx:1.8.0'
- implementation 'androidx.appcompat:appcompat:1.4.2' // need target 32 for 1.5.0
-
- // dont change this to 1.6.0 it looks ugly af
- implementation 'com.google.android.material:material:1.5.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
- implementation 'androidx.navigation:navigation-fragment-ktx:2.5.1'
- implementation 'androidx.navigation:navigation-ui-ktx:2.5.1'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
- testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
-
- //implementation "io.karn:khttp-android:0.1.2" //okhttp instead
-// implementation 'org.jsoup:jsoup:1.13.1'
- implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1"
-
- implementation "androidx.preference:preference-ktx:1.2.0"
-
- implementation 'com.github.bumptech.glide:glide:4.13.1'
- kapt 'com.github.bumptech.glide:compiler:4.13.1'
- implementation 'com.github.bumptech.glide:okhttp3-integration:4.13.0'
-
- implementation 'jp.wasabeef:glide-transformations:4.3.0'
-
- implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
-
- // implementation "androidx.leanback:leanback-paging:1.1.0-alpha09"
-
- // Exoplayer
- implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
- implementation 'com.google.android.exoplayer:extension-cast:2.16.1'
- implementation "com.google.android.exoplayer:extension-mediasession:2.16.1"
- implementation 'com.google.android.exoplayer:extension-okhttp:2.16.1'
-
- //implementation "com.google.android.exoplayer:extension-leanback:2.14.0"
-
- // Bug reports
- implementation "ch.acra:acra-core:5.8.4"
- implementation "ch.acra:acra-toast:5.8.4"
-
- compileOnly "com.google.auto.service:auto-service-annotations:1.0"
- //either for java sources:
- annotationProcessor "com.google.auto.service:auto-service:1.0"
- //or for kotlin sources (requires kapt gradle plugin):
- kapt "com.google.auto.service:auto-service:1.0"
-
- // subtitle color picker
- implementation 'com.jaredrummler:colorpicker:1.1.0'
-
- //run JS
- implementation 'org.mozilla:rhino:1.7.14'
-
- // TorrentStream
- //implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0'
-
- // Downloading
- implementation "androidx.work:work-runtime:2.7.1"
- implementation "androidx.work:work-runtime-ktx:2.7.1"
-
- // Networking
-// implementation "com.squareup.okhttp3:okhttp:4.9.2"
-// implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
- implementation 'com.github.Blatzar:NiceHttp:0.3.3'
-
- // Util to skip the URI file fuckery 🙏
- implementation "com.github.tachiyomiorg:unifile:17bec43"
-
- // API because cba maintaining it myself
- implementation "com.uwetrottmann.tmdb2:tmdb-java:2.6.0"
-
- implementation 'com.github.discord:OverlappingPanels:0.1.3'
- // debugImplementation because LeakCanary should only run in debug builds.
- // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
-
- // for shimmer when loading
- implementation 'com.facebook.shimmer:shimmer:0.5.0'
-
- implementation "androidx.tvprovider:tvprovider:1.0.0"
-
- // used for subtitle decoding https://github.com/albfernandez/juniversalchardet
- implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
-
- // slow af yt
- //implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT'
-
- // newpipe yt
- implementation 'com.github.TeamNewPipe:NewPipeExtractor:dev-SNAPSHOT'
- coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
-
- // Library/extensions searching with Levenshtein distance
- implementation 'me.xdrop:fuzzywuzzy:1.4.0'
-}
-
-task androidSourcesJar(type: Jar) {
- getArchiveClassifier().set('sources')
- from android.sourceSets.main.java.srcDirs//full sources
-}
-
-// this is used by the gradlew plugin
-task makeJar(type: Copy) {
- from('build/intermediates/compile_app_classes_jar/debug')
- into('build')
- include('classes.jar')
- dependsOn('build')
-}
-
-dokkaHtml {
- moduleName.set("Cloudstream")
- dokkaSourceSets {
- main {
- sourceLink {
- // Unix based directory relative path to the root of the project (where you execute gradle respectively).
- localDirectory.set(file("src/main/java"))
-
- // URL showing where the source code can be accessed through the web browser
- remoteUrl.set(new URL(
- "https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
- // Suffix which is used to append the line number to the URL. Use #L for GitHub
- remoteLineSuffix.set("#L")
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 00000000..42ca93fd
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,250 @@
+import com.android.build.gradle.api.BaseVariantOutput
+import org.jetbrains.dokka.gradle.DokkaTask
+import java.io.ByteArrayOutputStream
+import java.net.URL
+
+plugins {
+ id("com.android.application")
+ id("kotlin-android")
+ id("kotlin-kapt")
+ id("kotlin-android-extensions")
+ id("org.jetbrains.dokka")
+}
+
+val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
+val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
+
+fun String.execute() = ByteArrayOutputStream().use { baot ->
+ if (project.exec {
+ workingDir = projectDir
+ commandLine = this@execute.split(Regex("\\s"))
+ standardOutput = baot
+ }.exitValue == 0)
+ String(baot.toByteArray()).trim()
+ else null
+}
+
+android {
+ testOptions {
+ unitTests.isReturnDefaultValues = true
+ }
+ signingConfigs {
+ create("prerelease") {
+ if (prereleaseStoreFile != null) {
+ storeFile = file(prereleaseStoreFile)
+ storePassword = System.getenv("SIGNING_STORE_PASSWORD")
+ keyAlias = System.getenv("SIGNING_KEY_ALIAS")
+ keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
+ }
+ }
+ }
+
+ compileSdk = 31
+ buildToolsVersion = "30.0.3"
+
+ defaultConfig {
+ applicationId = "com.lagradost.cloudstream3"
+ minSdk = 21
+ targetSdk = 30
+
+ versionCode = 52
+ versionName = "3.1.6"
+
+ resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
+
+ resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
+
+ resValue("bool", "is_prerelease", "false")
+
+ buildConfigField(
+ "String",
+ "BUILDDATE",
+ "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
+ )
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ kapt {
+ includeCompileClasspath = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isDebuggable = false
+ isMinifyEnabled = false
+ isShrinkResources = false
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ debug {
+ isDebuggable = true
+ applicationIdSuffix = ".debug"
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+ flavorDimensions.add("state")
+ productFlavors {
+ create("stable") {
+ dimension = "state"
+ resValue("bool", "is_prerelease", "false")
+ }
+ create("prerelease") {
+ dimension = "state"
+ resValue("bool", "is_prerelease", "true")
+ buildConfigField("boolean", "BETA", "true")
+ applicationIdSuffix = ".prerelease"
+ signingConfig = signingConfigs.getByName("prerelease")
+ versionNameSuffix = "-PRE"
+ versionCode = (System.currentTimeMillis() / 60000).toInt()
+ }
+ }
+ compileOptions {
+ isCoreLibraryDesugaringEnabled = true
+
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ freeCompilerArgs = listOf("-Xjvm-default=compatibility")
+ }
+ lint {
+ abortOnError = false
+ checkReleaseBuilds = false
+ }
+ namespace = "com.lagradost.cloudstream3"
+}
+
+repositories {
+ maven("https://jitpack.io")
+}
+
+dependencies {
+ implementation("com.google.android.mediahome:video:1.0.0")
+ implementation("androidx.test.ext:junit-ktx:1.1.3")
+ testImplementation("org.json:json:20180813")
+
+ implementation("androidx.core:core-ktx:1.8.0")
+ implementation("androidx.appcompat:appcompat:1.4.2") // need target 32 for 1.5.0
+
+ // dont change this to 1.6.0 it looks ugly af
+ implementation("com.google.android.material:material:1.5.0")
+ implementation("androidx.constraintlayout:constraintlayout:2.1.4")
+ implementation("androidx.navigation:navigation-fragment-ktx:2.5.1")
+ implementation("androidx.navigation:navigation-ui-ktx:2.5.1")
+ implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.3")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
+
+ //implementation("io.karn:khttp-android:0.1.2") //okhttp instead
+// implementation("org.jsoup:jsoup:1.13.1")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
+
+ implementation("androidx.preference:preference-ktx:1.2.0")
+
+ implementation("com.github.bumptech.glide:glide:4.13.1")
+ kapt("com.github.bumptech.glide:compiler:4.13.1")
+ implementation("com.github.bumptech.glide:okhttp3-integration:4.13.0")
+
+ implementation("jp.wasabeef:glide-transformations:4.3.0")
+
+ implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
+
+ // implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
+
+ // Exoplayer
+ implementation("com.google.android.exoplayer:exoplayer:2.16.1")
+ implementation("com.google.android.exoplayer:extension-cast:2.16.1")
+ implementation("com.google.android.exoplayer:extension-mediasession:2.16.1")
+ implementation("com.google.android.exoplayer:extension-okhttp:2.16.1")
+
+ //implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
+
+ // Bug reports
+ implementation("ch.acra:acra-core:5.8.4")
+ implementation("ch.acra:acra-toast:5.8.4")
+
+ compileOnly("com.google.auto.service:auto-service-annotations:1.0")
+ //either for java sources:
+ annotationProcessor("com.google.auto.service:auto-service:1.0")
+ //or for kotlin sources (requires kapt gradle plugin):
+ kapt("com.google.auto.service:auto-service:1.0")
+
+ // subtitle color picker
+ implementation("com.jaredrummler:colorpicker:1.1.0")
+
+ //run JS
+ implementation("org.mozilla:rhino:1.7.14")
+
+ // TorrentStream
+ //implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
+
+ // Downloading
+ implementation("androidx.work:work-runtime:2.7.1")
+ implementation("androidx.work:work-runtime-ktx:2.7.1")
+
+ // Networking
+// implementation("com.squareup.okhttp3:okhttp:4.9.2")
+// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
+ implementation("com.github.Blatzar:NiceHttp:0.3.3")
+
+ // Util to skip the URI file fuckery 🙏
+ implementation("com.github.tachiyomiorg:unifile:17bec43")
+
+ // API because cba maintaining it myself
+ implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
+
+ implementation("com.github.discord:OverlappingPanels:0.1.3")
+ // debugImplementation because LeakCanary should only run in debug builds.
+ // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
+
+ // for shimmer when loading
+ implementation("com.facebook.shimmer:shimmer:0.5.0")
+
+ implementation("androidx.tvprovider:tvprovider:1.0.0")
+
+ // used for subtitle decoding https://github.com/albfernandez/juniversalchardet
+ implementation("com.github.albfernandez:juniversalchardet:2.4.0")
+
+ // slow af yt
+ //implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
+
+ // newpipe yt
+ implementation("com.github.TeamNewPipe:NewPipeExtractor:dev-SNAPSHOT")
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
+
+ // Library/extensions searching with Levenshtein distance
+ implementation("me.xdrop:fuzzywuzzy:1.4.0")
+}
+
+tasks.register("androidSourcesJar", Jar::class) {
+ archiveClassifier.set("sources")
+ from(android.sourceSets.getByName("main").java.srcDirs) //full sources
+}
+
+// this is used by the gradlew plugin
+tasks.register("makeJar", Copy::class) {
+ from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
+ into("build")
+ include("classes.jar")
+ dependsOn("build")
+}
+
+tasks.withType().configureEach {
+ moduleName.set("Cloudstream")
+ dokkaSourceSets {
+ named("main") {
+ sourceLink {
+ // Unix based directory relative path to the root of the project (where you execute gradle respectively).
+ localDirectory.set(file("src/main/java"))
+
+ // URL showing where the source code can be accessed through the web browser
+ remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
+ // Suffix which is used to append the line number to the URL. Use #L for GitHub
+ remoteLineSuffix.set("#L")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 481bb434..ff59496d 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
+# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 216a5f21..47676059 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,7 +1,6 @@
+ xmlns:tools="http://schemas.android.com/tools">
@@ -27,6 +26,7 @@
+
(Regex("data='(\\S+?)'").first(eval)) ?: return
+ val data = tryParseJson(Regex("data='(\\S+?)'").first(eval)) ?: return
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
?.split(Regex("\\D+"))
?.joinToString("") {
Char(it.toInt()).toString()
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
?: throw ErrorLoadingException("can't find password")
- val decryptedData =
- cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "")
- ?.substringAfter("sources:[")?.substringBefore("],")
+ val decryptedData = cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "")
- Regex("\"file\":\"(\\S+?)\".*?res=(\\d+)").findAll(decryptedData ?: return).map {
+ val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
+ val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
+
+ Regex("\"file\":\"(\\S+?)\".*?res=(\\d+)").findAll(sourceData ?: return).map {
it.groupValues[1] to it.groupValues[2]
}.toList().distinctBy { it.second }.map { (link, quality) ->
callback.invoke(
@@ -171,6 +181,17 @@ open class Gdriveplayer : ExtractorApi() {
)
}
+ subData?.addMarks("file")?.addMarks("kind")?.addMarks("label").let { dataSub ->
+ tryParseJson>("[$dataSub]")?.map { sub ->
+ subtitleCallback.invoke(
+ SubtitleFile(
+ sub.label,
+ httpsify(sub.file)
+ )
+ )
+ }
+ }
+
}
data class AesData(
@@ -179,4 +200,10 @@ open class Gdriveplayer : ExtractorApi() {
@JsonProperty("s") val s: String
)
+ data class Tracks(
+ @JsonProperty("file") val file: String,
+ @JsonProperty("kind") val kind: String,
+ @JsonProperty("label") val label: String
+ )
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt
new file mode 100644
index 00000000..e2eb7bf0
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt
@@ -0,0 +1,40 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.lagradost.cloudstream3.SubtitleFile
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.M3u8Helper
+
+class Moviehab : ExtractorApi() {
+ override var name = "Moviehab"
+ override var mainUrl = "https://play.moviehab.com"
+ override val requiresReferer = false
+
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+ val res = app.get(url)
+ res.document.select("video#player").let {
+ //should redirect first for making it works
+ val link = app.get("$mainUrl/${it.select("source").attr("src")}", referer = url).url
+ M3u8Helper.generateM3u8(
+ this.name,
+ link,
+ url
+ ).forEach(callback)
+
+ Regex("src[\"|'],\\s[\"|'](\\S+)[\"|']\\)").find(res.text)?.groupValues?.get(1).let {sub ->
+ subtitleCallback.invoke(
+ SubtitleFile(
+ it.select("track").attr("label"),
+ "$mainUrl/$sub"
+ )
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt
index 6153a7c1..8ef6c463 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt
@@ -7,7 +7,11 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
-class SpeedoStream : ExtractorApi() {
+class SpeedoStream1 : SpeedoStream() {
+ override val mainUrl = "https://speedostream.nl"
+}
+
+open class SpeedoStream : ExtractorApi() {
override val name = "SpeedoStream"
override val mainUrl = "https://speedostream.com"
override val requiresReferer = true
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt
index 30a0496d..913b410b 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt
@@ -7,6 +7,11 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
+class Sbspeed : StreamSB() {
+ override var name = "Sbspeed"
+ override var mainUrl = "https://sbspeed.com"
+}
+
class Streamsss : StreamSB() {
override var mainUrl = "https://streamsss.net"
}
@@ -93,15 +98,15 @@ open class StreamSB : ExtractorApi() {
}
data class Subs (
- @JsonProperty("file") val file: String,
- @JsonProperty("label") val label: String,
+ @JsonProperty("file") val file: String? = null,
+ @JsonProperty("label") val label: String? = null,
)
data class StreamData (
@JsonProperty("file") val file: String,
@JsonProperty("cdn_img") val cdnImg: String,
@JsonProperty("hash") val hash: String,
- @JsonProperty("subs") val subs: List?,
+ @JsonProperty("subs") val subs: ArrayList? = arrayListOf(),
@JsonProperty("length") val length: String,
@JsonProperty("id") val id: String,
@JsonProperty("title") val title: String,
@@ -141,5 +146,14 @@ open class StreamSB : ExtractorApi() {
url,
headers = headers
).forEach(callback)
+
+ mapped.streamData.subs?.map {sub ->
+ subtitleCallback.invoke(
+ SubtitleFile(
+ sub.label.toString(),
+ sub.file ?: return@map null,
+ )
+ )
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt
index 63634704..287fe4d4 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt
@@ -85,18 +85,12 @@ open class VidSrcExtractor : ExtractorApi() {
this.name,
this.name,
srcm3u8,
- this.mainUrl,
+ "https://vidsrc.stream/",
Qualities.Unknown.value,
extractorData = pass,
isM3u8 = true
)
)
-
-// M3u8Helper.generateM3u8(
-// name,
-// srcm3u8,
-// absoluteUrl
-// ).forEach(callback)
} else {
loadExtractor(linkfixed, url, subtitleCallback, callback)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt
index 9e3585ae..15ff0436 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt
@@ -1,12 +1,23 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
-import com.lagradost.cloudstream3.utils.AppUtils
+import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
+class Cdnplayer: XStreamCdn() {
+ override val name: String = "Cdnplayer"
+ override val mainUrl: String = "https://cdnplayer.online"
+}
+
+class Kotakajair: XStreamCdn() {
+ override val name: String = "Kotakajair"
+ override val mainUrl: String = "https://kotakajair.xyz"
+}
+
class FEnet: XStreamCdn() {
override val name: String = "FEnet"
override val mainUrl: String = "https://fembed.net"
@@ -59,44 +70,67 @@ open class XStreamCdn : ExtractorApi() {
//val type: String // Mp4
)
+ private data class Player(
+ @JsonProperty("poster_file") val poster_file: String? = null,
+ )
+
private data class ResponseJson(
@JsonProperty("success") val success: Boolean,
- @JsonProperty("data") val data: List?
+ @JsonProperty("player") val player: Player? = null,
+ @JsonProperty("data") val data: List?,
+ @JsonProperty("captions") val captions: List?,
+ )
+
+ private data class Captions(
+ @JsonProperty("id") val id: String,
+ @JsonProperty("hash") val hash: String,
+ @JsonProperty("language") val language: String,
+ @JsonProperty("extension") val extension: String
)
override fun getExtractorUrl(id: String): String {
return "$domainUrl/api/source/$id"
}
- override suspend fun getUrl(url: String, referer: String?): List {
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
val headers = mapOf(
"Referer" to url,
"User-Agent" to "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0",
)
val id = url.trimEnd('/').split("/").last()
val newUrl = "https://${domainUrl}/api/source/${id}"
- val extractedLinksList: MutableList = mutableListOf()
- with(app.post(newUrl, headers = headers)) {
- if (this.code != 200) return listOf()
- val text = this.text
- if (text.isEmpty()) return listOf()
- if (text == """{"success":false,"data":"Video not found or has been removed"}""") return listOf()
- AppUtils.parseJson(text)?.let {
+ app.post(newUrl, headers = headers).let { res ->
+ val sources = tryParseJson(res.text)
+ sources?.let {
if (it.success && it.data != null) {
- it.data.forEach { data ->
- extractedLinksList.add(
+ it.data.map { source ->
+ callback.invoke(
ExtractorLink(
name,
name = name,
- data.file,
+ source.file,
url,
- getQualityFromName(data.label),
+ getQualityFromName(source.label),
)
)
}
}
}
+
+ val userData = sources?.player?.poster_file?.split("/")?.get(2)
+ sources?.captions?.map {
+ subtitleCallback.invoke(
+ SubtitleFile(
+ it?.language.toString(),
+ "$mainUrl/asset/userdata/$userData/caption/${it?.hash}/${it?.id}.${it?.extension}"
+ )
+ )
+ }
}
- return extractedLinksList
}
}
\ No newline at end of file
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 0e5e544b..ce695873 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt
@@ -26,6 +26,8 @@ class APIRepository(val api: MainAPI) {
fun isInvalidData(data: String): Boolean {
return data.isEmpty() || data == "[]" || data == "about:blank"
}
+
+ private val cacheHash: HashMap, LoadResponse> = hashMapOf()
}
val hasMainPage = api.hasMainPage
@@ -39,7 +41,13 @@ class APIRepository(val api: MainAPI) {
suspend fun load(url: String): Resource {
return safeApiCall {
if (isInvalidData(url)) throw ErrorLoadingException()
- api.load(api.fixUrl(url)) ?: throw ErrorLoadingException()
+ val fixedUrl = api.fixUrl(url)
+ val key = Pair(api.name,url)
+ cacheHash[key] ?: api.load(fixedUrl)?.also {
+ // we cache 20 responses because ppl often go back to the same shit + 20 because I dont want to cause too much memory leak
+ if (cacheHash.size > 20) cacheHash.remove(cacheHash.keys.random())
+ cacheHash[key] = it
+ } ?: throw ErrorLoadingException()
}
}
@@ -75,7 +83,12 @@ class APIRepository(val api: MainAPI) {
api.lastHomepageRequest = unixTimeMS
nameIndex?.let { api.mainPage.getOrNull(it) }?.let { data ->
- listOf(api.getMainPage(page, MainPageRequest(data.name, data.data, data.horizontalImages)))
+ listOf(
+ api.getMainPage(
+ page,
+ MainPageRequest(data.name, data.data, data.horizontalImages)
+ )
+ )
} ?: run {
if (api.sequentialMainPage) {
var first = true
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt
index 417b4408..eb4eb666 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt
@@ -5,13 +5,12 @@ import androidx.annotation.StringRes
import com.lagradost.cloudstream3.R
enum class WatchType(val internalId: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int) {
- // FIX ICONS
- WATCHING(0, R.string.type_watching, R.drawable.ic_baseline_remove_red_eye_24),
- COMPLETED(1, R.string.type_completed, R.drawable.ic_baseline_check_24),
- ONHOLD(2, R.string.type_on_hold, R.drawable.ic_baseline_pause_24),
- DROPPED(3, R.string.type_dropped, R.drawable.ic_baseline_close_24),
- PLANTOWATCH(4, R.string.type_plan_to_watch, R.drawable.ic_baseline_close_24),
- NONE(5, R.string.type_none, R.drawable.ic_baseline_remove_red_eye_24);
+ WATCHING(0, R.string.type_watching, R.drawable.ic_baseline_bookmark_24),
+ COMPLETED(1, R.string.type_completed, R.drawable.ic_baseline_bookmark_24),
+ ONHOLD(2, R.string.type_on_hold, R.drawable.ic_baseline_bookmark_24),
+ DROPPED(3, R.string.type_dropped, R.drawable.ic_baseline_bookmark_24),
+ PLANTOWATCH(4, R.string.type_plan_to_watch, R.drawable.ic_baseline_bookmark_24),
+ NONE(5, R.string.type_none, R.drawable.ic_baseline_add_24);
companion object {
fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE
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 0d571b76..2f539a33 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
@@ -14,6 +14,8 @@ import android.view.ViewGroup
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
+import androidx.core.content.ContextCompat
+import androidx.core.content.ContextCompat.getDrawable
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
@@ -26,11 +28,14 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
+import com.google.android.material.chip.Chip
+import com.google.android.material.chip.ChipGroup
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
+import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.MainActivity.Companion.mainPluginsLoadedEvent
@@ -43,6 +48,7 @@ import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
+import com.lagradost.cloudstream3.ui.result.ResultViewModel2.Companion.updateWatchStatus
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.search.*
@@ -50,6 +56,7 @@ import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallba
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
+import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.setMaxViewPoolSize
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
@@ -61,11 +68,11 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllResumeStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeLastWatched
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
import com.lagradost.cloudstream3.utils.Event
+import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
-import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.UIHelper.setImage
@@ -82,14 +89,12 @@ import kotlinx.android.synthetic.main.fragment_home.home_loading
import kotlinx.android.synthetic.main.fragment_home.home_loading_error
import kotlinx.android.synthetic.main.fragment_home.home_loading_shimmer
import kotlinx.android.synthetic.main.fragment_home.home_loading_statusbar
-import kotlinx.android.synthetic.main.fragment_home.home_main_poster_recyclerview
import kotlinx.android.synthetic.main.fragment_home.home_master_recycler
import kotlinx.android.synthetic.main.fragment_home.home_plan_to_watch_btt
import kotlinx.android.synthetic.main.fragment_home.home_provider_meta_info
import kotlinx.android.synthetic.main.fragment_home.home_provider_name
import kotlinx.android.synthetic.main.fragment_home.home_reload_connection_open_in_browser
import kotlinx.android.synthetic.main.fragment_home.home_reload_connectionerror
-import kotlinx.android.synthetic.main.fragment_home.home_statusbar
import kotlinx.android.synthetic.main.fragment_home.home_type_completed_btt
import kotlinx.android.synthetic.main.fragment_home.home_type_dropped_btt
import kotlinx.android.synthetic.main.fragment_home.home_type_on_hold_btt
@@ -100,6 +105,8 @@ import kotlinx.android.synthetic.main.fragment_home.home_watch_parent_item_title
import kotlinx.android.synthetic.main.fragment_home.result_error_text
import kotlinx.android.synthetic.main.fragment_home_tv.*
import kotlinx.android.synthetic.main.home_episodes_expanded.*
+import kotlinx.android.synthetic.main.tvtypes_chips.*
+import kotlinx.android.synthetic.main.tvtypes_chips.view.*
import java.util.*
const val HOME_BOOKMARK_VALUE_LIST = "home_bookmarked_last_list"
@@ -247,16 +254,16 @@ class HomeFragment : Fragment() {
}
fun getPairList(
- anime: MaterialButton?,
- cartoons: MaterialButton?,
- tvs: MaterialButton?,
- docs: MaterialButton?,
- movies: MaterialButton?,
- asian: MaterialButton?,
- livestream: MaterialButton?,
- nsfw: MaterialButton?,
- others: MaterialButton?,
- ): List>> {
+ anime: Chip?,
+ cartoons: Chip?,
+ tvs: Chip?,
+ docs: Chip?,
+ movies: Chip?,
+ asian: Chip?,
+ livestream: Chip?,
+ nsfw: Chip?,
+ others: Chip?,
+ ): List>> {
// This list should be same order as home screen to aid navigation
return listOf(
Pair(movies, listOf(TvType.Movie, TvType.Torrent)),
@@ -271,6 +278,59 @@ class HomeFragment : Fragment() {
)
}
+ private fun getPairList(header: ChipGroup) = getPairList(
+ header.home_select_anime,
+ header.home_select_cartoons,
+ header.home_select_tv_series,
+ header.home_select_documentaries,
+ header.home_select_movies,
+ header.home_select_asian,
+ header.home_select_livestreams,
+ header.home_select_nsfw,
+ header.home_select_others
+ )
+
+ fun validateChips(header: ChipGroup?, validTypes: List) {
+ if (header == null) return
+ val pairList = getPairList(header)
+ for ((button, types) in pairList) {
+ val isValid = validTypes.any { types.contains(it) }
+ button?.isVisible = isValid
+ }
+ }
+
+ fun updateChips(header: ChipGroup?, selectedTypes: List) {
+ if (header == null) return
+ val pairList = getPairList(header)
+ for ((button, types) in pairList) {
+ button?.isChecked =
+ button?.isVisible == true && selectedTypes.any { types.contains(it) }
+ }
+ }
+
+ fun bindChips(
+ header: ChipGroup?,
+ selectedTypes: List,
+ validTypes: List,
+ callback: (List) -> Unit
+ ) {
+ if (header == null) return
+ val pairList = getPairList(header)
+ for ((button, types) in pairList) {
+ val isValid = validTypes.any { types.contains(it) }
+ button?.isVisible = isValid
+ button?.isChecked = isValid && selectedTypes.any { types.contains(it) }
+ button?.setOnCheckedChangeListener { _, _ ->
+ val list = ArrayList()
+ for ((sbutton, vvalidTypes) in pairList) {
+ if (sbutton?.isChecked == true)
+ list.addAll(vvalidTypes)
+ }
+ callback(list)
+ }
+ }
+ }
+
fun Context.selectHomepage(selectedApiName: String?, callback: (String) -> Unit) {
val validAPIs = filterProviderByPreferredMedia().toMutableList()
@@ -296,21 +356,9 @@ class HomeFragment : Fragment() {
?.toMutableList()
?: mutableListOf(TvType.Movie, TvType.TvSeries)
- val anime = dialog.findViewById(R.id.home_select_anime)
- val cartoons = dialog.findViewById(R.id.home_select_cartoons)
- val tvs = dialog.findViewById(R.id.home_select_tv_series)
- val docs = dialog.findViewById(R.id.home_select_documentaries)
- val movies = dialog.findViewById(R.id.home_select_movies)
- val asian = dialog.findViewById(R.id.home_select_asian)
- val livestream = dialog.findViewById(R.id.home_select_livestreams)
- val nsfw = dialog.findViewById(R.id.home_select_nsfw)
- val others = dialog.findViewById(R.id.home_select_others)
val cancelBtt = dialog.findViewById(R.id.cancel_btt)
val applyBtt = dialog.findViewById(R.id.apply_btt)
- val pairList =
- getPairList(anime, cartoons, tvs, docs, movies, asian, livestream, nsfw, others)
-
cancelBtt?.setOnClickListener {
dialog.dismissSafe()
}
@@ -355,52 +403,14 @@ class HomeFragment : Fragment() {
arrayAdapter.notifyDataSetChanged()
}
- /**
- * Since fire tv is fucked we need to manually define the focus layout.
- * Since visible buttons are only known in runtime this is required.
- **/
- var lastButton: MaterialButton? = null
-
- for ((button, validTypes) in pairList) {
- val isValid =
- validAPIs.any { api -> validTypes.any { api.supportedTypes.contains(it) } }
- button?.isVisible = isValid
- if (isValid) {
-
- // Set focus navigation
- button?.let { currentButton ->
- lastButton?.nextFocusRightId = currentButton.id
- lastButton?.id?.let { currentButton.nextFocusLeftId = it }
- lastButton = currentButton
- }
-
- fun buttonContains(): Boolean {
- return preSelectedTypes.any { validTypes.contains(it) }
- }
-
- button?.isSelected = buttonContains()
- button?.setOnClickListener {
- preSelectedTypes.clear()
- preSelectedTypes.addAll(validTypes)
- for ((otherButton, _) in pairList) {
- otherButton?.isSelected = false
- }
- button.isSelected = true
- updateList()
- }
-
- button?.setOnLongClickListener {
- if (!buttonContains()) {
- button.isSelected = true
- preSelectedTypes.addAll(validTypes)
- } else {
- button.isSelected = false
- preSelectedTypes.removeAll(validTypes)
- }
- updateList()
- return@setOnLongClickListener true
- }
- }
+ bindChips(
+ dialog.home_select_group,
+ preSelectedTypes,
+ validAPIs.flatMap { it.supportedTypes }.distinct()
+ ) { list ->
+ preSelectedTypes.clear()
+ preSelectedTypes.addAll(list)
+ updateList()
}
updateList()
}
@@ -422,7 +432,6 @@ class HomeFragment : Fragment() {
}
private fun toggleMainVisibility(visible: Boolean) {
- home_main_holder?.isVisible = visible
home_main_poster_recyclerview?.isVisible = visible
}
@@ -531,6 +540,51 @@ class HomeFragment : Fragment() {
home_random?.visibility = View.GONE
}
+ observe(homeViewModel.preview) { preview ->
+ when (preview) {
+ is Resource.Success -> {
+ home_preview?.isVisible = true
+ preview.value.apply {
+ home_preview_tags?.text = tags?.joinToString(" • ") ?: ""
+ home_preview_tags?.isGone = tags.isNullOrEmpty()
+ home_preview_image?.setImage(posterUrl, posterHeaders)
+ home_preview_title?.text = name
+ home_preview_play?.setOnClickListener {
+ activity?.loadResult(url, apiName, START_ACTION_RESUME_LATEST)
+ //activity.loadSearchResult(url, START_ACTION_RESUME_LATEST)
+ }
+ home_preview_info?.setOnClickListener {
+ activity?.loadResult(url, apiName)
+ //activity.loadSearchResult(random)
+ }
+ // very ugly code, but I dont care
+ val watchType = DataStoreHelper.getResultWatchState(preview.value.getId())
+ home_preview_bookmark?.setText(watchType.stringRes)
+ home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(null,getDrawable(home_preview_bookmark.context, watchType.iconRes),null,null)
+ home_preview_bookmark?.setOnClickListener { fab ->
+ activity?.showBottomDialog(
+ WatchType.values().map { fab.context.getString(it.stringRes) }
+ .toList(),
+ DataStoreHelper.getResultWatchState(preview.value.getId()).ordinal,
+ fab.context.getString(R.string.action_add_to_bookmarks),
+ showApply = false,
+ {}) {
+ val newValue = WatchType.values()[it]
+ home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(null,getDrawable(home_preview_bookmark.context, newValue.iconRes),null,null)
+ home_preview_bookmark?.setText(newValue.stringRes)
+
+ updateWatchStatus(preview.value, newValue)
+ reloadStored()
+ }
+ }
+ }
+ }
+ else -> {
+ home_preview?.isVisible = false
+ }
+ }
+ }
+
observe(homeViewModel.apiName) { apiName ->
currentApiName = apiName
// setKey(USER_SELECTED_HOMEPAGE_API, apiName)
@@ -563,17 +617,17 @@ class HomeFragment : Fragment() {
HomeChildItemAdapter(
mutableListOf(),
R.layout.home_result_big_grid,
- nextFocusUp = home_main_poster_recyclerview.nextFocusUpId,
- nextFocusDown = home_main_poster_recyclerview.nextFocusDownId
+ nextFocusUp = home_main_poster_recyclerview?.nextFocusUpId,
+ nextFocusDown = home_main_poster_recyclerview?.nextFocusDownId
) { callback ->
homeHandleSearch(callback)
}
- home_main_poster_recyclerview.setLinearListLayout()
+ home_main_poster_recyclerview?.setLinearListLayout()
observe(homeViewModel.randomItems) { items ->
if (items.isNullOrEmpty()) {
toggleMainVisibility(false)
} else {
- val tempAdapter = home_main_poster_recyclerview.adapter as HomeChildItemAdapter?
+ val tempAdapter = home_main_poster_recyclerview?.adapter as? HomeChildItemAdapter?
// no need to reload if it has the same data
if (tempAdapter != null && tempAdapter.cardList == items) {
toggleMainVisibility(true)
@@ -938,7 +992,7 @@ class HomeFragment : Fragment() {
}
}
- context?.fixPaddingStatusbarView(home_statusbar)
+ //context?.fixPaddingStatusbarView(home_statusbar)
context?.fixPaddingStatusbar(home_loading_statusbar)
home_master_recycler.adapter =
@@ -959,33 +1013,6 @@ class HomeFragment : Fragment() {
}
} // GridLayoutManager(context, 1).also { it.supportsPredictiveItemAnimations() }
- if (!isTvSettings()) {
- LinearSnapHelper().attachToRecyclerView(home_main_poster_recyclerview) // snap
- val centerLayoutManager =
- CenterZoomLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
- centerLayoutManager.setOnSizeListener { index ->
- (home_main_poster_recyclerview?.adapter as HomeChildItemAdapter?)?.cardList?.get(
- index
- )?.let { random ->
- home_main_play?.setOnClickListener {
- activity.loadSearchResult(random, START_ACTION_RESUME_LATEST)
- }
- home_main_info?.setOnClickListener {
- activity.loadSearchResult(random)
- }
-
- home_main_text?.text =
- random.name + if (random is AnimeSearchResponse && !random.dubStatus.isNullOrEmpty()) {
- random.dubStatus?.joinToString(
- prefix = " • ",
- separator = " | "
- ) { it.name }
- } else ""
- }
- }
- home_main_poster_recyclerview?.layoutManager = centerLayoutManager // scale
- }
-
reloadStored()
loadHomePage()
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 30fd45c1..0b928a55 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
@@ -13,6 +13,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.HomePageList
+import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.mvvm.*
@@ -32,7 +33,6 @@ import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
import kotlin.collections.set
@@ -58,7 +58,9 @@ class HomeViewModel : ViewModel() {
val bookmarks: LiveData>> = _bookmarks
private val _resumeWatching = MutableLiveData>()
+ private val _preview = MutableLiveData>()
val resumeWatching: LiveData> = _resumeWatching
+ val preview: LiveData> = _preview
fun loadResumeWatching() = viewModelScope.launchSafe {
val resumeWatching = withContext(Dispatchers.IO) {
@@ -207,6 +209,7 @@ class HomeViewModel : ViewModel() {
expandAndReturn(name)
}
+
private fun load(api: MainAPI?) = viewModelScope.launchSafe {
repo = if (api != null) {
APIRepository(api)
@@ -219,6 +222,7 @@ class HomeViewModel : ViewModel() {
if (repo?.hasMainPage == true) {
_page.postValue(Resource.Loading())
+ _preview.postValue(Resource.Loading())
when (val data = repo?.getMainPage(1, null)) {
is Resource.Success -> {
@@ -232,8 +236,38 @@ class HomeViewModel : ViewModel() {
ExpandableHomepageList(filteredList, 1, home.hasNext)
}
}
- _page.postValue(Resource.Success(expandable))
val items = data.value.mapNotNull { it?.items }.flatten()
+ items.randomOrNull()?.list?.randomOrNull()?.url?.let { url ->
+ // backup request in case first fails
+ var first = repo?.load(url)
+ if(first == null ||first is Resource.Failure) {
+ first = repo?.load(items.random().list.random().url)
+ }
+ first?.let {
+ _preview.postValue(it)
+ } ?: run {
+ _preview.postValue(
+ Resource.Failure(
+ false,
+ null,
+ null,
+ "No repo found, this should never happen"
+ )
+ )
+ }
+ } ?: run {
+ _preview.postValue(
+ Resource.Failure(
+ false,
+ null,
+ null,
+ "No homepage items"
+ )
+ )
+ }
+
+ _page.postValue(Resource.Success(expandable))
+
//val home = data.value
if (items.isNotEmpty()) {
@@ -263,6 +297,7 @@ class HomeViewModel : ViewModel() {
}
} else {
_page.postValue(Resource.Success(emptyMap()))
+ _preview.postValue(Resource.Failure(false, null, null, "No homepage"))
}
}
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 ea466120..a838c85c 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
@@ -17,10 +17,7 @@ import com.google.android.exoplayer2.text.TextRenderer
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.trackselection.TrackSelector
import com.google.android.exoplayer2.ui.SubtitleView
-import com.google.android.exoplayer2.upstream.DataSource
-import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
-import com.google.android.exoplayer2.upstream.HttpDataSource
+import com.google.android.exoplayer2.upstream.*
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache
@@ -880,7 +877,16 @@ class CS3IPlayer : IPlayer {
}
override fun onPlayerError(error: PlaybackException) {
- playerError?.invoke(error)
+ // If the Network fails then ignore the exception if the duration is set.
+ // This is to switch mirrors automatically if the stream has not been fetched, but
+ // allow playing the buffer without internet as then the duration is fetched.
+ if (error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED
+ && exoPlayer?.duration != C.TIME_UNSET
+ ) {
+ exoPlayer?.prepare()
+ } else {
+ playerError?.invoke(error)
+ }
super.onPlayerError(error)
}
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 906b652d..7c099793 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
@@ -412,6 +412,29 @@ class ResultViewModel2 : ViewModel() {
return this?.firstOrNull { it.season == season }
}
+ fun updateWatchStatus(currentResponse : LoadResponse, status: WatchType) {
+ val currentId = currentResponse.getId()
+ val resultPage = currentResponse
+
+ DataStoreHelper.setResultWatchState(currentId, status.internalId)
+ val current = DataStoreHelper.getBookmarkedData(currentId)
+ val currentTime = System.currentTimeMillis()
+ DataStoreHelper.setBookmarkedData(
+ currentId,
+ DataStoreHelper.BookmarkedData(
+ currentId,
+ current?.bookmarkedTime ?: currentTime,
+ currentTime,
+ resultPage.name,
+ resultPage.url,
+ resultPage.apiName,
+ resultPage.type,
+ resultPage.posterUrl,
+ resultPage.year
+ )
+ )
+ }
+
private fun filterName(name: String?): String? {
if (name == null) return null
Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let {
@@ -764,28 +787,10 @@ class ResultViewModel2 : ViewModel() {
private val _selectPopup: MutableLiveData> = MutableLiveData(Some.None)
val selectPopup: LiveData> get() = _selectPopup
- fun updateWatchStatus(status: WatchType) {
- val currentId = currentId ?: return
- val resultPage = currentResponse ?: return
- _watchStatus.postValue(status)
- DataStoreHelper.setResultWatchState(currentId, status.internalId)
- val current = DataStoreHelper.getBookmarkedData(currentId)
- val currentTime = System.currentTimeMillis()
- DataStoreHelper.setBookmarkedData(
- currentId,
- DataStoreHelper.BookmarkedData(
- currentId,
- current?.bookmarkedTime ?: currentTime,
- currentTime,
- resultPage.name,
- resultPage.url,
- resultPage.apiName,
- resultPage.type,
- resultPage.posterUrl,
- resultPage.year
- )
- )
+ fun updateWatchStatus(status: WatchType) {
+ updateWatchStatus(currentResponse ?: return,status)
+ _watchStatus.postValue(status)
}
private fun startChromecast(
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 5f108cd1..8f588bf7 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
@@ -26,15 +26,19 @@ import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.APIHolder.getApiSettings
+import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.home.HomeFragment
+import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.currentSpan
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList
+import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.updateChips
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.Coroutines.main
@@ -46,6 +50,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import kotlinx.android.synthetic.main.fragment_search.*
+import kotlinx.android.synthetic.main.tvtypes_chips.*
import java.util.concurrent.locks.ReentrantLock
const val SEARCH_PREF_TAGS = "search_pref_tags"
@@ -130,65 +135,26 @@ class SearchFragment : Fragment() {
// Null if defined as a variable
// This needs to be run after view created
- private fun getPairList(): List>> {
- return HomeFragment.getPairList(
- search_select_anime,
- search_select_cartoons,
- search_select_tv_series,
- search_select_documentaries,
- search_select_movies,
- search_select_asian,
- search_select_livestreams,
- search_select_nsfw,
- search_select_others
- )
- }
private fun reloadRepos(success: Boolean = false) = main {
- val pairList = getPairList()
-
searchViewModel.reloadRepos()
context?.filterProviderByPreferredMedia()?.let { validAPIs ->
- for ((button, validTypes) in pairList) {
- val isValid =
- validAPIs.any { api -> validTypes.any { api.supportedTypes.contains(it) } }
- button?.isVisible = isValid
- if (isValid) {
- fun buttonContains(): Boolean {
- return selectedSearchTypes.any { validTypes.contains(it) }
- }
-
- button?.isSelected = buttonContains()
- button?.setOnClickListener {
- val last = selectedSearchTypes.toSet()
- selectedSearchTypes.clear()
- selectedSearchTypes.addAll(validTypes)
- for ((otherButton, _) in pairList) {
- otherButton?.isSelected = false
- }
- it?.context?.setKey(SEARCH_PREF_TAGS, selectedSearchTypes)
- it?.isSelected = true
- if (last != selectedSearchTypes.toSet()) // if you click the same button again the it does nothing
- search(main_search?.query?.toString())
- }
-
- button?.setOnLongClickListener {
- if (!buttonContains()) {
- it?.isSelected = true
- selectedSearchTypes.addAll(validTypes)
- } else {
- it?.isSelected = false
- selectedSearchTypes.removeAll(validTypes)
- }
- it?.context?.setKey(SEARCH_PREF_TAGS, selectedSearchTypes)
- search(main_search?.query?.toString())
- return@setOnLongClickListener true
- }
+ bindChips(
+ home_select_group,
+ selectedSearchTypes,
+ validAPIs.flatMap { api -> api.supportedTypes }.distinct()
+ ) { list ->
+ if (selectedSearchTypes.toSet() != list.toSet()) {
+ setKey(SEARCH_PREF_TAGS, selectedSearchTypes)
+ selectedSearchTypes.clear()
+ selectedSearchTypes.addAll(list)
+ search(main_search?.query?.toString())
}
}
}
}
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -239,31 +205,67 @@ class SearchFragment : Fragment() {
builder.let { dialog ->
val isMultiLang = ctx.getApiProviderLangSettings().size > 1
- val anime = dialog.findViewById(R.id.home_select_anime)
- val cartoons = dialog.findViewById(R.id.home_select_cartoons)
- val tvs = dialog.findViewById(R.id.home_select_tv_series)
- val docs = dialog.findViewById(R.id.home_select_documentaries)
- val movies = dialog.findViewById(R.id.home_select_movies)
- val asian = dialog.findViewById(R.id.home_select_asian)
- val livestream =
- dialog.findViewById(R.id.home_select_livestreams)
- val nsfw = dialog.findViewById(R.id.home_select_nsfw)
- val other = dialog.findViewById(R.id.home_select_others)
val cancelBtt = dialog.findViewById(R.id.cancel_btt)
val applyBtt = dialog.findViewById(R.id.apply_btt)
- val pairList =
- HomeFragment.getPairList(
- anime,
- cartoons,
- tvs,
- docs,
- movies,
- asian,
- livestream,
- nsfw,
- other
- )
+ val listView = dialog.findViewById(R.id.listview1)
+ val arrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice)
+ listView?.adapter = arrayAdapter
+ listView?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
+
+ listView?.setOnItemClickListener { _, _, i, _ ->
+ if (currentValidApis.isNotEmpty()) {
+ val api = currentValidApis[i].name
+ if (currentSelectedApis.contains(api)) {
+ listView.setItemChecked(i, false)
+ currentSelectedApis -= api
+ } else {
+ listView.setItemChecked(i, true)
+ currentSelectedApis += api
+ }
+ }
+ }
+
+ fun updateList(types: List) {
+ setKey(SEARCH_PREF_TAGS, types.map {it.name})
+
+ arrayAdapter.clear()
+ currentValidApis = validAPIs.filter { api ->
+ api.supportedTypes.any {
+ types.contains(it)
+ }
+ }.sortedBy { it.name.lowercase() }
+
+ val names = currentValidApis.map {
+ if (isMultiLang) "${
+ SubtitleHelper.getFlagFromIso(
+ it.lang
+ )?.plus(" ") ?: ""
+ }${it.name}" else it.name
+ }
+ for ((index, api) in currentValidApis.map { it.name }.withIndex()) {
+ listView?.setItemChecked(index, currentSelectedApis.contains(api))
+ }
+
+ //arrayAdapter.notifyDataSetChanged()
+ arrayAdapter.addAll(names)
+ arrayAdapter.notifyDataSetChanged()
+ }
+
+ val selectedSearchTypes = getKey>(SEARCH_PREF_TAGS)
+ ?.mapNotNull { listName ->
+ TvType.values().firstOrNull { it.name == listName }
+ }
+ ?.toMutableList()
+ ?: mutableListOf(TvType.Movie, TvType.TvSeries)
+
+ bindChips(
+ dialog.home_select_group,
+ selectedSearchTypes,
+ TvType.values().toList()
+ ) { list ->
+ updateList(list)
+ }
cancelBtt?.setOnClickListener {
dialog.dismissSafe()
@@ -284,90 +286,7 @@ class SearchFragment : Fragment() {
context?.setKey(SEARCH_PREF_PROVIDERS, currentSelectedApis.toList())
selectedApis = currentSelectedApis
}
-
- val selectedSearchTypes = context?.getKey>(SEARCH_PREF_TAGS)
- ?.mapNotNull { listName ->
- TvType.values().firstOrNull { it.name == listName }
- }
- ?.toMutableList()
- ?: mutableListOf(TvType.Movie, TvType.TvSeries)
-
- val listView = dialog.findViewById(R.id.listview1)
- val arrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice)
- listView?.adapter = arrayAdapter
- listView?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
-
- listView?.setOnItemClickListener { _, _, i, _ ->
- if (currentValidApis.isNotEmpty()) {
- val api = currentValidApis[i].name
- if (currentSelectedApis.contains(api)) {
- listView.setItemChecked(i, false)
- currentSelectedApis -= api
- } else {
- listView.setItemChecked(i, true)
- currentSelectedApis += api
- }
- }
- }
-
- fun updateList() {
- arrayAdapter.clear()
- currentValidApis = validAPIs.filter { api ->
- api.supportedTypes.any {
- selectedSearchTypes.contains(it)
- }
- }.sortedBy { it.name.lowercase() }
-
- val names = currentValidApis.map {
- if (isMultiLang) "${
- SubtitleHelper.getFlagFromIso(
- it.lang
- )?.plus(" ") ?: ""
- }${it.name}" else it.name
- }
- for ((index, api) in currentValidApis.map { it.name }.withIndex()) {
- listView?.setItemChecked(index, currentSelectedApis.contains(api))
- }
-
- //arrayAdapter.notifyDataSetChanged()
- arrayAdapter.addAll(names)
- arrayAdapter.notifyDataSetChanged()
- }
-
- for ((button, validTypes) in pairList) {
- val isValid =
- validAPIs.any { api -> validTypes.any { api.supportedTypes.contains(it) } }
- button?.isVisible = isValid
- if (isValid) {
- fun buttonContains(): Boolean {
- return selectedSearchTypes.any { validTypes.contains(it) }
- }
-
- button?.isSelected = buttonContains()
- button?.setOnClickListener {
- selectedSearchTypes.clear()
- selectedSearchTypes.addAll(validTypes)
- for ((otherButton, _) in pairList) {
- otherButton?.isSelected = false
- }
- button.isSelected = true
- updateList()
- }
-
- button?.setOnLongClickListener {
- if (!buttonContains()) {
- button.isSelected = true
- selectedSearchTypes.addAll(validTypes)
- } else {
- button.isSelected = false
- selectedSearchTypes.removeAll(validTypes)
- }
- updateList()
- return@setOnLongClickListener true
- }
- }
- }
- updateList()
+ updateList(selectedSearchTypes.toList())
}
}
}
@@ -380,14 +299,6 @@ class SearchFragment : Fragment() {
?.toMutableList()
?: mutableListOf(TvType.Movie, TvType.TvSeries)
- val pairList = getPairList()
- fun updateSelectedList(list: MutableList) {
- selectedSearchTypes = list
- for ((button, validTypes) in pairList) {
- button?.isSelected = selectedSearchTypes.any { validTypes.contains(it) }
- }
- }
-
if (isTrueTvSettings()) {
search_filter.isFocusable = true
search_filter.isFocusableInTouchMode = true
@@ -500,7 +411,7 @@ class SearchFragment : Fragment() {
SEARCH_HISTORY_OPEN -> {
searchViewModel.clearSearch()
if (searchItem.type.isNotEmpty())
- updateSelectedList(searchItem.type.toMutableList())
+ updateChips(home_select_group, searchItem.type.toMutableList())
main_search?.setQuery(searchItem.searchText, true)
}
SEARCH_HISTORY_REMOVE -> {
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 57074e74..8ea76cda 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
@@ -79,6 +79,7 @@ val appLanguages = arrayListOf(
Triple("\uD83C\uDDEE\uD83C\uDDE9", "Indonesian", "in"),
Triple("", "Czech", "cs"),
Triple("", "Croatian", "hr"),
+ Triple("", "Bulgarian", "bg"),
).sortedBy { it.second } //ye, we go alphabetical, so ppl don't put their lang on top
class SettingsGeneral : PreferenceFragmentCompat() {
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 aa302c5a..bacd26c8 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
@@ -9,8 +9,9 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.lagradost.cloudstream3.R
+import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.mvvm.observe
-import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.getPairList
+import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.ui.settings.appLanguages
@@ -18,6 +19,8 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.fragment_plugins.*
+import kotlinx.android.synthetic.main.tvtypes_chips.*
+import kotlinx.android.synthetic.main.tvtypes_chips_scroll.*
const val PLUGINS_BUNDLE_NAME = "name"
const val PLUGINS_BUNDLE_URL = "url"
@@ -145,56 +148,10 @@ class PluginsFragment : Fragment() {
pluginViewModel.updatePluginList(context, url)
tv_types_scroll_view?.isVisible = true
- // 💀💀💀💀💀💀💀 Recyclerview when
- val pairList = getPairList(
- home_select_anime,
- home_select_cartoons,
- home_select_tv_series,
- home_select_documentaries,
- home_select_movies,
- home_select_asian,
- home_select_livestreams,
- home_select_nsfw,
- home_select_others
- )
-
-// val supportedTypes: Array =
-// pluginViewModel.filteredPlugins.value!!.second.flatMap { it -> it.plugin.second.tvTypes ?: listOf("Other") }.distinct().toTypedArray()
-
- // Copy pasted code
- for ((button, validTypes) in pairList) {
- val validTypesMapped = validTypes.map { it.name }
- val isValid = true
- //validTypes.any { it -> supportedTypes.contains(it.name) }
- button?.isVisible = isValid
- if (isValid) {
- fun buttonContains(): Boolean {
- return pluginViewModel.tvTypes.any { validTypesMapped.contains(it) }
- }
-
- button?.isSelected = buttonContains()
- button?.setOnClickListener {
- pluginViewModel.tvTypes.clear()
- pluginViewModel.tvTypes.addAll(validTypesMapped)
- for ((otherButton, _) in pairList) {
- otherButton?.isSelected = false
- }
- button.isSelected = true
- pluginViewModel.updateFilteredPlugins()
- }
-
- button?.setOnLongClickListener {
- if (!buttonContains()) {
- button.isSelected = true
- pluginViewModel.tvTypes.addAll(validTypesMapped)
- } else {
- button.isSelected = false
- pluginViewModel.tvTypes.removeAll(validTypesMapped)
- }
- pluginViewModel.updateFilteredPlugins()
- return@setOnLongClickListener true
- }
- }
+ bindChips(home_select_group, emptyList(), TvType.values().toList()) { list ->
+ pluginViewModel.tvTypes.clear()
+ pluginViewModel.tvTypes.addAll(list.map { it.name })
+ pluginViewModel.updateFilteredPlugins()
}
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
index 8d6b1b82..ca612385 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
@@ -235,6 +235,7 @@ val extractorApis: MutableList = arrayListOf(
Vidgomunime(),
Sbflix(),
Streamsss(),
+ Sbspeed(),
Fastream(),
@@ -246,6 +247,8 @@ val extractorApis: MutableList = arrayListOf(
LayarKaca(),
Rasacintaku(),
FEnet(),
+ Kotakajair(),
+ Cdnplayer(),
// WatchSB(), 'cause StreamSB.kt works
Uqload(),
Uqload1(),
@@ -317,6 +320,7 @@ val extractorApis: MutableList = arrayListOf(
Linkbox(),
Acefile(),
SpeedoStream(),
+ SpeedoStream1(),
Zorofile(),
Embedgram(),
Mvidoo(),
@@ -324,6 +328,7 @@ val extractorApis: MutableList = arrayListOf(
Vidmoly(),
Vidmolyme(),
Voe(),
+ Moviehab(),
Gdriveplayerapi(),
Gdriveplayerapp(),
@@ -336,6 +341,7 @@ val extractorApis: MutableList = arrayListOf(
Gdriveplayerco(),
Gdriveplayer(),
DatabaseGdrive(),
+ DatabaseGdrive2(),
YoutubeExtractor(),
YoutubeShortLinkExtractor(),
diff --git a/app/src/main/res/color/chip_color.xml b/app/src/main/res/color/chip_color.xml
new file mode 100644
index 00000000..f4190551
--- /dev/null
+++ b/app/src/main/res/color/chip_color.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index f8ca8cbd..4905d454 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,79 +1,83 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/homeRoot"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard|navigation"
+ android:paddingTop="0dp">
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ app:itemTextColor="@color/item_select_color"
+ app:labelVisibilityMode="unlabeled"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:menu="@menu/bottom_nav_menu"
+ app:menuGravity="center">
+
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:menu="@menu/bottom_nav_menu" />
+ android:id="@+id/nav_host_fragment"
+ android:name="androidx.navigation.fragment.NavHostFragment"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:defaultNavHost="true"
+ app:layout_constraintBottom_toTopOf="@+id/cast_mini_controller_holder"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/nav_rail_view"
+ app:layout_constraintTop_toTopOf="parent"
+ app:navGraph="@navigation/mobile_navigation" />
+ android:id="@+id/cast_mini_controller_holder"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toTopOf="@+id/nav_view"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/nav_rail_view"
+ tools:layout_height="100dp">
+ android:id="@+id/cast_mini_controller"
+ class="com.lagradost.cloudstream3.ui.MyMiniControllerFragment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+
+ app:castControlButtons="@array/cast_mini_controller_control_buttons"
+ app:customCastBackgroundColor="?attr/primaryGrayBackground"
+ tools:ignore="FragmentTagUsage" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index 4abdef0b..9d430e3d 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -1,87 +1,87 @@
+ android:layout_height="match_parent"
+ tools:context=".ui.home.HomeFragment">
+ android:id="@+id/home_loading"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"
+ tools:visibility="gone">
+ android:layout_width="50dp"
+ android:layout_height="50dp"
+ android:layout_gravity="center"
+ android:visibility="gone"
+ tools:visibility="gone" />
+ android:id="@+id/home_loading_shimmer"
+ 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"
+ app:shimmer_highlight_alpha="0.3">
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ android:layout_width="125dp"
+ android:layout_height="200dp"
+ android:layout_gravity="center"
+ android:layout_margin="@dimen/loading_margin"
+ android:background="@color/grayShimmer"
+ android:translationX="-164dp"
+ app:cardCornerRadius="@dimen/loading_radius" />
+ android:layout_width="148dp"
+ android:layout_height="234dp"
+ android:layout_gravity="center"
+ android:layout_margin="@dimen/loading_margin"
+ android:background="@color/grayShimmer"
+ app:cardCornerRadius="@dimen/loading_radius" />
+ android:layout_width="125dp"
+ android:layout_height="200dp"
+ android:layout_gravity="center"
+ android:layout_margin="@dimen/loading_margin"
+ android:background="@color/grayShimmer"
+ android:translationX="164dp"
+ app:cardCornerRadius="@dimen/loading_radius" />
+ android:layout_marginEnd="@dimen/result_padding"
+ android:orientation="vertical">
@@ -93,168 +93,170 @@
+ android:id="@+id/home_loading_statusbar"
+ android:layout_width="match_parent"
+ android:layout_height="70dp">
+ android:layout_margin="10dp"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/home_change_provider_img_des"
+ android:src="@drawable/ic_baseline_keyboard_arrow_down_24" />
+
+
+ android:layout_margin="5dp"
+ android:minWidth="200dp"
+ android:text="@string/reload_error"
+ app:icon="@drawable/ic_baseline_autorenew_24" />
-
-
+ android:layout_gravity="center"
+ android:layout_margin="5dp"
+ android:minWidth="200dp"
+ android:text="@string/result_open_in_browser"
+ app:icon="@drawable/ic_baseline_public_24" />
+ android:id="@+id/result_error_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_margin="5dp"
+ android:gravity="center"
+ android:textColor="?attr/textColor" />
+ android:id="@+id/home_loaded"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/primaryBlackBackground"
+ android:visibility="gone"
+ tools:visibility="visible">
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ android:id="@+id/home_statusbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?attr/primaryGrayBackground" />
+ android:id="@+id/home_settings_bar"
+
+ android:layout_width="match_parent"
+ android:layout_height="70dp"
+ android:background="?attr/primaryGrayBackground"
+ android:visibility="gone">
+ android:paddingTop="10dp"
+ android:paddingEnd="10dp"
+ android:paddingBottom="10dp">
+ android:id="@+id/home_profile_picture_holder"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_gravity="center_vertical"
+ android:layout_marginEnd="10dp"
+ app:cardCornerRadius="100dp">
+ android:id="@+id/home_profile_picture"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"
+ tools:ignore="ContentDescription" />
+ android:layout_width="match_parent"
+ android:layout_height="40dp"
+ android:layout_gravity="center_vertical"
+ android:background="@drawable/search_background"
+ android:visibility="visible">
+ android:layout_height="match_parent"
+ android:layout_gravity="center_vertical"
+ android:iconifiedByDefault="false"
+ android:paddingStart="-10dp"
+ app:iconifiedByDefault="false"
+ app:queryBackground="@color/transparent"
+ app:queryHint="@string/search_hint"
+ app:searchIcon="@drawable/search_icon"
+ tools:ignore="RtlSymmetry" />
+
+
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center_vertical"
+ android:textColor="?attr/textColor"
+ android:textSize="20sp"
+
+ tools:text="Hello World" />
-
-
+ android:id="@+id/home_provider_meta_info"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center_vertical"
+ android:textColor="?attr/grayTextColor"
+ android:textSize="14sp"
+ tools:text="Hello World" />
@@ -278,273 +280,309 @@
-->
-
+
+
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"
+ tools:src="@drawable/example_poster" />
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="300dp"
+ android:layout_gravity="bottom"
+ android:background="@drawable/background_shadow" />
+
+
+
+ android:layout_height="match_parent"
+ android:layout_gravity="center_vertical"
+ android:iconifiedByDefault="false"
+ android:paddingStart="-10dp"
+ app:iconifiedByDefault="false"
+
+ app:queryBackground="@color/transparent"
+ app:queryHint="@string/search_hint"
+
+ app:searchIcon="@drawable/search_icon"
+ tools:ignore="RtlSymmetry" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:id="@+id/home_watch_parent_item_title"
+ style="@style/WatchHeaderText"
+ android:layout_gravity="center_vertical"
+ android:text="@string/continue_watching" />
+ android:layout_width="30dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="end|center_vertical"
+ android:layout_marginEnd="5dp"
+ android:contentDescription="@string/home_more_info"
+ android:src="@drawable/ic_baseline_arrow_forward_24"
+ app:tint="?attr/textColor" />
+ android:id="@+id/home_watch_child_recyclerview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false"
+ android:descendantFocusability="afterDescendants"
+ android:orientation="horizontal"
+ android:paddingHorizontal="5dp"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ tools:listitem="@layout/home_result_grid" />
+ android:id="@+id/home_bookmarked_holder"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:visibility="gone"
+ tools:visibility="visible">
+ android:foreground="?android:attr/selectableItemBackgroundBorderless"
+ android:nextFocusLeft="@id/nav_rail_view"
+ android:nextFocusUp="@id/home_watch_child_recyclerview"
+ android:nextFocusForward="@id/home_bookmarked_child_recyclerview"
+ android:paddingStart="12dp"
+ android:paddingTop="5dp"
+ android:paddingEnd="12dp"
+ android:paddingBottom="5dp">
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="50dp"
+ android:fadingEdge="horizontal"
+ android:requiresFadingEdge="horizontal">
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ android:nextFocusLeft="@id/nav_rail_view"
+ android:nextFocusRight="@id/home_plan_to_watch_btt"
+ android:text="@string/type_watching" />
+ android:nextFocusLeft="@id/home_type_watching_btt"
+ android:nextFocusRight="@id/home_type_on_hold_btt"
+ android:text="@string/type_plan_to_watch" />
+ android:nextFocusLeft="@id/home_plan_to_watch_btt"
+ android:nextFocusRight="@id/home_type_dropped_btt"
+ android:text="@string/type_on_hold" />
+ android:nextFocusLeft="@id/home_type_on_hold_btt"
+ android:nextFocusRight="@id/home_type_completed_btt"
+ android:text="@string/type_dropped" />
+ style="@style/RoundedSelectableButton"
+ android:nextFocusLeft="@id/home_type_dropped_btt"
+ android:text="@string/type_completed" />
+ android:layout_width="30dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="end|center_vertical"
+ android:layout_marginEnd="5dp"
+ android:contentDescription="@string/home_more_info"
+ android:src="@drawable/ic_baseline_arrow_forward_24"
+ app:tint="?attr/textColor" />
+ android:clipToPadding="false"
+ android:descendantFocusability="afterDescendants"
+ android:orientation="horizontal"
+ android:paddingHorizontal="5dp"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ tools:listitem="@layout/home_result_grid" />
+ android:id="@+id/home_master_recycler"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:descendantFocusability="afterDescendants"
+ android:nextFocusLeft="@id/nav_rail_view"
+ tools:listitem="@layout/homepage_parent" />
+ android:id="@+id/home_api_fab"
+ style="@style/ExtendedFloatingActionButton"
+ android:text="@string/home_source"
+ android:textColor="?attr/textColor"
+ android:visibility="gone"
+ app:icon="@drawable/ic_baseline_filter_list_24"
+ tools:ignore="ContentDescription"
+ tools:visibility="visible" />
+ android:id="@+id/home_random"
+ style="@style/ExtendedFloatingActionButton"
+ android:layout_gravity="bottom|start"
+ android:text="@string/home_random"
+ android:textColor="?attr/textColor"
+ android:visibility="gone"
+ app:icon="@drawable/ic_baseline_play_arrow_24"
+ tools:ignore="ContentDescription"
+ tools:visibility="visible" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_plugins.xml b/app/src/main/res/layout/fragment_plugins.xml
index 15e0d2f9..40a0299c 100644
--- a/app/src/main/res/layout/fragment_plugins.xml
+++ b/app/src/main/res/layout/fragment_plugins.xml
@@ -25,108 +25,7 @@
app:titleTextColor="?attr/textColor"
tools:title="Overlord" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_width="match_parent"
+ android:layout_height="60dp">
+
+
-
+
-
+
+
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/tvtypes_chips.xml b/app/src/main/res/layout/tvtypes_chips.xml
new file mode 100644
index 00000000..33ca7762
--- /dev/null
+++ b/app/src/main/res/layout/tvtypes_chips.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/tvtypes_chips_scroll.xml b/app/src/main/res/layout/tvtypes_chips_scroll.xml
new file mode 100644
index 00000000..45b27dbc
--- /dev/null
+++ b/app/src/main/res/layout/tvtypes_chips_scroll.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
new file mode 100644
index 00000000..e32d3d93
--- /dev/null
+++ b/app/src/main/res/values-bg/strings.xml
@@ -0,0 +1,533 @@
+
+
+
+
+ %s еп. %d
+ Актьори: %s
+ Епизод %d ще бъде пуснат след
+ %DD %dh %dm
+ %dh %dm
+ %dm
+
+
+ Плакат
+ @string/result_poster_img_des
+ Плакат на епизод
+ Основен плакат
+ Следващ произволен
+ Върни се
+ Смяна на доставчика
+ Визуализация на фона
+
+
+ Скорост (%.2fx)
+ Оценка: %.1f
+ Намерена е нова актуализация!\n%s -> %s
+ Шаблон
+ %d мин
+
+ CloudStream
+ Пусни с CloudStream
+ Начало
+ Търсене
+ Изтегляния
+ Настройки
+
+ Търсене…
+ Търсете %s...
+
+ Няма данни
+ Още опций
+ Следващ епизод
+ Жанрове
+ Сподели
+ Отвори в браузъра
+ Пропусни зареждането
+ Зареждане…
+
+ Гледане
+ На изчакване
+ Завършено
+ Изпуснат
+ План за гледане
+ Нито един
+ Повторно гледане
+
+ Пускане на филм
+ Възпроизвеждане на живо
+ Поточно предаване на торент
+ Източници
+ субтитри
+ Повторен опит за свързване...
+ Върни се
+ Пусни епизод
+
+
+ Изтегли
+ Изтеглено
+ Изтегля се
+ Изтеглянето е на пауза
+ Изтеглянето започна
+ Изтеглянето се провали
+ Изтеглянето е отменено
+ Изтеглянето е завършено
+ Поток
+
+ Грешка при зареждане на връзки
+ Вътрешна памет
+
+ Дублаж
+ Субтитри
+
+ Изтрий файла
+ Възпроизвеждане на файл
+ Възобновете изтеглянето
+ Пауза на изтеглянето
+
+ Деактивирайте автоматичното докладване на грешки
+ Повече информация
+ Скрий
+ Пусни
+ Информация
+ Филтриране на отметки
+ Отметки
+ Премахване
+ Задайте статус на гледане
+ Приложи
+ Отказ
+ Копирай
+ Затвори
+ Изчисти
+ Запазване
+
+ Скорост на възпроизвеждане
+
+ Настройки на субтитрите
+ Цвят на текста
+ Цвят на контура
+ Цвят на фона
+ Цвят на прозореца
+ Тип ръб
+ Височина на субтитрите
+ Шрифт
+ Размер на шрифта
+
+ Търсене чрез доставчици
+ Търсене с помощта на типове
+
+ %d Банан/а даден/и на разработчиците
+ Не е/са даден/и Банан/и
+
+ Автоматичен избор на език
+ Изтегляне на езици
+ Език на субтитрите
+ Задръжте, за да нулирате по подразбиране
+ Импортирайте шрифтове, като ги поставите в %s
+ Продължете да гледате
+
+ Премахване
+ Повече информация
+ @string/home_play
+
+ Може да е необходим VPN, за да работи правилно този доставчик
+ Този доставчик е торент, препоръчва се VPN
+
+ Метаданните не се предоставят от сайта, зареждането на видео ще бъде неуспешно, ако не съществува на сайта.
+
+ Описание
+ Няма намерен съдържание
+ Няма намерено описание
+
+ Покажи logcat 🐈
+
+ Картина в картина
+ Продължава възпроизвеждането в миниатюрен плейър върху други приложения
+ Бутон за преоразмеряване на плейъра
+ Премахнете черните граници
+ Субтитри
+ Настройки на субтитрите на плейъра
+ Chromecast субтитри
+ Настройки за субтитри на Chromecast
+
+ Режим Eigengravy (промяна скорост на възпроизвеждане)
+ Добавя опция за скорост в плейъра
+ Плъзнете за преместване
+ Плъзнете наляво или надясно, за да контролирате времето във видеоплейъра
+ Плъзнете, за да промените настройките
+ Плъзнете наляво или надясно, за да промените яркостта или силата на звука
+
+ Автоматично пускане на следващ епизод
+ Започнете следващия епизод, когато текущият приключи
+
+ Докоснете двукратно за превъртане
+ Докоснете два пъти за пауза
+ Размер на превъртане
+ Докоснете два пъти от дясната или лявата страна, за да превъртите напред или назад
+ Докоснете в средата, за да направите пауза
+ Използвайте яркостта на системата
+ Използвайте системна яркост в плейъра на приложението вместо тъмно
+ наслагване
+
+ Актуализирайте прогреса на гледане
+ Автоматично синхронизирайте прогреса на текущия си епизод
+
+ Възстановете данните от архив
+
+ Архивиране на данни
+ Зареден архивен файл
+ Неуспешно възстановяване на данни от файл %s
+ Успешно съхранени данни
+ Липсват разрешения за съхранение, моля, опитайте отново
+ Грешка при архивирането на %s
+
+ Търсене
+ Акаунти
+ Актуализации и архивиране
+
+ Информация
+ Подробно търсене
+ Дава ви резултатите от търсенето, разделени по доставчик
+ Изпраща данни само за сривове
+ Не изпраща данни
+ Показване заместващ епизод за аниме
+ Показване на трейлъри
+ Покажете плакати от kitsu
+ Скриване на избраното видео качество в резултатите от търсенето
+
+ Автоматични актуализации на плъгини
+ Показвай актуализации на приложението
+ Автоматично търси нови актуализации при стартиране
+ Актуализация до експериментални версии
+ Търсете експериментални актуализации вместо само пълни версии
+ Github
+ Light novel - приложение от същите разработчици
+ Приложение за аниме от същите разработчици
+ Присъединете се към Discord
+ Дайте банан/и разработчиците
+ Даден/и банан/и
+
+ Език на приложението
+
+ Този доставчик няма поддръжка за Chromecast
+ Няма намерени връзки
+ Връзката е копирана в клипборда
+ Пусни епизода
+ Възстановяване на стойността по подразбиране
+ За съжаление приложението се срина. Анонимен доклад за грешка ще бъде изпратен до
+ разработчиците
+
+ Сезон
+ %s %d%s
+ Без сезон
+ Епизод
+ Епизоди
+ %d-%d
+ %d %s
+ С
+ Е
+ Няма намерени епизоди
+
+ Изтрий файла
+ Изтрий
+ Пауза
+ Продължи
+ -30
+ 30
+ Това ще изтрие за постоянно %s\nСигурни ли сте?
+ %dm\nостава
+
+
+ Продължава
+ Завършен
+ Статус
+ Година
+ Рейтинг
+ Продължителност
+ Уебсайт
+ Синопсис
+
+ На опашката
+ Без субтитри
+ По подразбиране
+
+ Безплатно
+ Използвано
+ Приложения
+
+
+ Филми
+ Телевизионен сериал
+ Анимационни филми
+ Аниме
+ Торенти
+ Документални
+ OVA
+ Азиатски драми
+ На живо
+ Разпоретини
+ други
+
+
+ Филм
+ Серия
+ Анимационен филм
+ @string/anime
+ @string/ova
+ Торент
+ Документален филм
+ Азиатска драма
+ Поток на живо
+ Разпоретинa
+ Видео
+
+ Грешка в източника
+ Дистанционна грешка
+ Грешка в рендъра
+ Неочаквана грешка на плеъра
+ Грешка при изтегляне, проверете разрешенията за съхранение
+
+ Епизод за Chromecast
+ Chromecast огледало
+ Пусни в приложението
+ Пусни в %s
+ Пусни в браузър
+ Копирай връзка
+ Автоматично изтегляне
+ Изтегляне на огледало
+ Презареждане на връзки
+ Изтегляне на субтитри
+
+ покажи качество
+ Покажи дублаж
+ Покажи субтитри
+ Заглавие
+ Превключване на елементите на потребителския интерфейс на плаката
+
+ Няма намерена актуализация
+ Проверка за актуализация
+
+ Заключен
+ Преоразмеряване
+ Източник
+ Пропусни
+
+ Не показвай отново
+ Пропуснете тази актуализация
+ Актуализация
+ Предпочитано качество за гледане
+ Максимален брой знаци за заглавие във видеоплейъра
+ Разделителна способност на видео плейъра
+
+ Размер на видео буфера
+ Дължина на видео буфера
+ Видео кеш на диск
+ Изчистете кеша за видео и изображения
+
+ Ще предизвика случайни сривове, ако е зададено твърде високо. Не променяйте, ако имате малко количество RAM, като Android TV или стар телефон
+ Може да причини проблеми на системи с малко място за съхранение, като устройства с Android TV, ако го зададете твърде високо
+
+ DNS през HTTPS
+ Полезно за заобикаляне на блокирания от ISP доставчик
+
+ Сайт за клониране
+ Премахване на сайта
+ Добавяне на клонинг на съществуващ сайт с различен URL адрес
+
+ Път за изтегляне
+
+ URL адрес на сървъра на Nginx
+
+ Показване на дублирани/субирани аниме
+
+ Побиране в екрана
+ Разтягане
+ Мащабиране
+
+ Опровержение
+
+ Общ
+ Случаен бутон
+ Показване на произволен бутон на началната страница
+ Езици на доставчика
+ Оформление на приложението
+ Предпочитана медия
+ Активирайте разпоретините на поддържани доставчици
+ Кодиране на субтитрите
+ Доставчици
+ Оформление
+
+ Автоматично
+ ТВ оформление
+ Оформление като телефон
+ Оформление като емулатор
+
+ Основен цвят
+ Тема на приложението
+ Местоположение на заглавието на плаката
+ Поставете заглавието под плаката
+
+ парола123
+ MyCoolUsername
+ hello@world.com
+ 127.0.0.1
+ MyCoolSite
+ example.com
+ Езиков код (en)
+
+
+ %s %s
+ Акаунт
+ Излизане
+ Влизане
+ Превключване на акаунт
+ Добавяне на акаунт
+ Създай акаунт
+ Добавете проследяване
+ Добавен %s
+ Синхронизиране
+ Оценен
+ %d / 10
+ /??
+ /%d
+ Удостоверен %s
+ Неуспешно удостоверяване на %s
+
+
+ Нито един
+ Нормално
+ Всичко
+ Макс
+ Мин
+ Контур
+ Удвоени
+ Сянка
+ Повдигнати
+ Синхронизиране на суб
+ 1000ms
+ Забавяне на субтитрите
+ Използвайте това, ако субтитрите се показват %dms твърде рано
+ Използвайте това, ако субтитрите се показват %dms твърде късно
+ Без забавяне на субтитрите
+
+
+
+ Бързата кафява лисица прескача мързеливото куче
+
+ Препоръчва се
+ Заредено %s
+ Зареди от файл
+ Зареди от Интернет
+ Изтеглен файл
+ Главен
+ Поддържащ
+ Заден план
+
+ Източник
+ Случаен
+
+ Очаквайте скоро…
+
+ Cam
+ Cam
+ Cam
+ HQ
+ HD
+ TS
+ TC
+ BlueRay
+ WP
+ DVD
+ 4K
+ SD
+ UHD
+ HDR
+ SDR
+ Web
+
+ Снимка на плакат
+ Плеър
+ Резолюция и заглавие
+ Заглавие
+ Резолюция
+ Невалиден идентификатор
+ Невалидни данни
+ Невалиден адрес
+ Грешка
+ Премахнете затворените надписи от субтитрите
+ Премахнете рекламирането в субтитрите
+ Филтриране по предпочитан медиен език
+ Екстри
+ Трейлър
+ Връзка към потока
+ Обратно към
+ Следващ
+ Гледайте видеоклипове на тези езици
+ Предишен
+ Пропуснете настройката
+ Променете външния вид на приложението, за да отговаря на вашето устройство
+ Докладване за сривове
+ Какво искате да видите
+ Край
+ Разширения
+ Добавяне на хранилище
+ Име на хранилище
+ URL адрес на хранилището
+ Приставката е заредена
+ Приставката е изтрита
+ Неуспешно зареждане %s
+ 18+
+ Започна да изтегля %d %s
+ Изтеглено %d %s успешно
+ Всички %s вече са изтеглени
+ Пакетно изтегляне
+ Плъгин
+ Плъгини
+ Това също ще изтрие всички хранилища за плъгини
+ Изтриване на хранилище
+ Изтеглете списъка със сайтове, които искате да използвате
+ Изтеглено: %d
+ Деактивирано: %d
+ Не е изтеглено: %d
+ Актуализирани %d плъгини
+ CloudStream няма инсталирани сайтове по подразбиране. Трябва да инсталирате сайтовете от хранилища.\n\nПоради безмозъчно премахване на DMCA от Sky Uk Limited 🤮 не можем да свържем сайтовете на хранилищата в приложението.\n\nПрисъединете се към нашия дискорд за връзки или търсете онлайн.
+ Вижте хранилищата на общността
+ Публичен списък
+ Всички субтитри с главни букви
+
+ Изтегляне на всички добавки от това хранилище?
+ %s (Disabled)
+ Потоци
+ Аудио потоци
+ Видео потоци
+ Приложете при рестартиране
+
+ Безопасният режим е активиран
+ Възникна непоправим срив и ние автоматично деактивирахме всички разширения, така че можете да намерите и премахнете разширението, което причинява проблеми.
+ Вижте информация за срива
+
+ Оценка: %s
+ Описание
+ Версия
+ Статус
+ Размер
+ Автори
+ Поддържани
+ език
+ Първо инсталирайте разширението
+
+ HLS плейлист
+
+ Предпочитан видео плеър
+ Вътрешен плеър
+ VLC
+ MPV
+ Уеб видео предаване
+ Браузър
+ Приложението не е намерено
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index e771b2e0..dbb680b8 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -75,6 +75,10 @@
- @color/amoledModeLight
+
+