diff --git a/.idea/gradle.xml b/.idea/gradle.xml index c5c0ff3b..d7c08c9c 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,17 +4,16 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3a77f3af..2244edf4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import org.jetbrains.dokka.gradle.DokkaTask +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.utils.provider import org.jetbrains.kotlin.konan.properties.Properties @@ -15,6 +16,7 @@ plugins { val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/" val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first() +var isLibraryDebug = false fun String.execute() = ByteArrayOutputStream().use { baot -> if (project.exec { @@ -72,9 +74,9 @@ android { val localProperties = gradleLocalProperties(rootDir, providers) buildConfigField( - "String", - "BUILDDATE", - "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));" + "long", + "BUILD_DATE", + "${System.currentTimeMillis()}" ) buildConfigField( "String", @@ -105,6 +107,7 @@ android { ) } debug { + isLibraryDebug = true isDebuggable = true applicationIdSuffix = ".debug" proguardFiles( @@ -201,7 +204,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:6dc25f7") /* For Trailers + implementation("com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:6dc25f7b97") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding @@ -234,18 +237,37 @@ dependencies { implementation("androidx.work:work-runtime:2.9.0") implementation("androidx.work:work-runtime-ktx:2.9.0") implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib + + implementation(project(":library") { + this.extra.set("isDebug", isLibraryDebug) + }) } -tasks.register("androidSourcesJar", Jar::class) { +tasks.register("androidSourcesJar") { archiveClassifier.set("sources") from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources } -// For GradLew Plugin -tasks.register("makeJar", Copy::class) { - from("build/intermediates/compile_app_classes_jar/prereleaseDebug") - into("build") - include("classes.jar") +tasks.register("copyJar") { + from( + "build/intermediates/compile_app_classes_jar/prereleaseDebug", + "../library/build/libs" + ) + into("build/app-classes") + include("classes.jar", "library-jvm*.jar") + // Remove the version + rename("library-jvm.*.jar", "library-jvm.jar") +} + +// Merge the app classes and the library classes into classes.jar +tasks.register("makeJar") { + dependsOn(tasks.getByName("copyJar")) + from( + zipTree("build/app-classes/classes.jar"), + zipTree("build/app-classes/library-jvm.jar") + ) + destinationDirectory.set(layout.buildDirectory) + archivesName = "classes" } tasks.withType { diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 273e267b..699159b5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -743,8 +743,6 @@ fun base64Encode(array: ByteArray): String { } } -class ErrorLoadingException(message: String? = null) : Exception(message) - fun MainAPI.fixUrlNull(url: String?): String? { if (url.isNullOrEmpty()) { return null @@ -865,7 +863,11 @@ enum class TvType(value: Int?) { AsianDrama(9), Live(10), NSFW(11), - Others(12) + Others(12), + Music(13), + AudioBook(14), + /** Wont load the built in player, make your own interaction */ + CustomMedia(15), } public enum class AutoDownloadMode(val value: Int) { @@ -1446,11 +1448,24 @@ fun TvType?.isEpisodeBased(): Boolean { return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama) } - data class NextAiring( val episode: Int, val unixTime: Long, -) + val season: Int? = null, +) { + /** + * Secondary constructor for backwards compatibility without season. + * TODO Remove this constructor after there is a new stable release and extensions are updated to support season. + */ + constructor( + episode: Int, + unixTime: Long, + ) : this ( + episode, + unixTime, + null + ) +} /** * @param season To be mapped with episode season, not shown in UI if displaySeason is defined diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 5a7e72ef..56322b73 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -135,7 +135,10 @@ import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -158,6 +161,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API +import com.lagradost.cloudstream3.utils.fcast.FcastManager import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser import com.lagradost.safefile.SafeFile @@ -1231,18 +1235,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, changeStatusBarState(isLayout(EMULATOR)) /** Biometric stuff for users without accounts **/ - val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) val noAccounts = settingsManager.getBoolean( getString(R.string.skip_startup_account_select_key), false ) || accounts.count() <= 1 - if (isLayout(PHONE) && authEnabled && noAccounts) { + if (isLayout(PHONE) && isAuthEnabled(this) && noAccounts) { if (deviceHasPasswordPinLock(this)) { startBiometricAuthentication(this, R.string.biometric_authentication_title, false) - BiometricAuthenticator.promptInfo?.let { promt -> - BiometricAuthenticator.biometricPrompt?.authenticate(promt) + promptInfo?.let { prompt -> + biometricPrompt?.authenticate(prompt) } // hide background while authenticating, Sorry moms & dads 🙏 @@ -1754,6 +1757,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, runAutoUpdate() } + FcastManager().init(this, false) + APIRepository.dubStatusActive = getApiDubstatusSettings() try { @@ -1790,8 +1795,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, } } catch (e: Exception) { logError(e) - } finally { - setKey(HAS_DONE_SETUP_KEY, true) } // Used to check current focus for TV @@ -1827,6 +1830,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, binding?.navHostFragment?.isInvisible = false } + override fun onAuthenticationError() { + finish() + } + private var backPressedCallback: OnBackPressedCallback? = null private fun attachBackPressedCallback() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index f03a5525..26567c7a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -2,9 +2,7 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.extractors.helper.* import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler -import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper @@ -28,30 +26,39 @@ open class Chillx : ExtractorApi() { override val name = "Chillx" override val mainUrl = "https://chillx.top" override val requiresReferer = true - private var key: String? = null + companion object { + private var key: String? = null + + suspend fun fetchKey(): String { + return if (key != null) { + key!! + } else { + val fetch = app.get("https://raw.githubusercontent.com/rushi-chavan/multi-keys/keys/keys.json").parsedSafe()?.key?.get(0) ?: throw ErrorLoadingException("Unable to get key") + key = fetch + key!! + } + } + } + + @Suppress("NAME_SHADOWING") override suspend fun getUrl( url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val master = Regex("\\s*=\\s*'([^']+)").find( + val master = Regex("""JScript[\w+]?\s*=\s*'([^']+)""").find( app.get( url, - referer = referer ?: "", - headers = mapOf( - "Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", - "Accept-Language" to "en-US,en;q=0.5", - ) + referer = url, ).text )?.groupValues?.get(1) - val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") - + val key = fetchKey() + val decrypt = cryptoAESHandler(master ?: "", key.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val subtitlePattern = """\[(.*?)\](https?://[^\s,]+)""".toRegex() + val subtitlePattern = """\[(.*?)](https?://[^\s,]+)""".toRegex() val matches = subtitlePattern.findAll(subtitles ?: "") val languageUrlPairs = matches.map { matchResult -> val (language, url) = matchResult.destructured @@ -83,23 +90,18 @@ open class Chillx : ExtractorApi() { headers = headers ).forEach(callback) } - + private fun decodeUnicodeEscape(input: String): String { val regex = Regex("u([0-9a-fA-F]{4})") return regex.replace(input) { it.groupValues[1].toInt(16).toChar().toString() } } - - suspend fun getKey() = key ?: fetchKey().also { key = it } - private suspend fun fetchKey(): String { - return app.get("https://raw.githubusercontent.com/Sofie99/Resources/main/chillix_key.json").parsed() - } - data class Tracks( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, - @JsonProperty("kind") val kind: String? = null, + + data class Keys( + @JsonProperty("chillx") val key: List ) + } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt index 0df93dc5..2343a92e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt @@ -9,10 +9,16 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import java.net.URL +class Geodailymotion : Dailymotion() { + override val name = "GeoDailymotion" + override val mainUrl = "https://geo.dailymotion.com" +} + open class Dailymotion : ExtractorApi() { override val mainUrl = "https://www.dailymotion.com" override val name = "Dailymotion" override val requiresReferer = false + private val baseUrl = "https://www.dailymotion.com" @Suppress("RegExpSimplifiable") private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex() @@ -34,7 +40,7 @@ open class Dailymotion : ExtractorApi() { val dmV1st = config.dmInternalData.v1st val dmTs = config.dmInternalData.ts val embedder = config.context.embedder - val metaDataUrl = "$mainUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" + val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies) .parsedSafe() ?: return metaData.qualities.forEach { (_, video) -> @@ -45,16 +51,19 @@ open class Dailymotion : ExtractorApi() { } private fun getEmbedUrl(url: String): String? { - if (url.contains("/embed/")) { - return url - } - val vid = getVideoId(url) ?: return null - return "$mainUrl/embed/video/$vid" + if (url.contains("/embed/") || url.contains("/video/")) { + return url } + if (url.contains("geo.dailymotion.com")) { + val videoId = url.substringAfter("video=") + return "$baseUrl/embed/video/$videoId" + } + return null + } private fun getVideoId(url: String): String? { val path = URL(url).path - val id = path.substringAfter("video/") + val id = path.substringAfter("/video/") if (id.matches(videoIdRegex)) { return id } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt new file mode 100644 index 00000000..2cb12e16 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt @@ -0,0 +1,27 @@ +package com.lagradost.cloudstream3.extractors + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson + +open class EPlayExtractor : ExtractorApi() { + override var name = "EPlay" + override var mainUrl = "https://eplayvid.net" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?): List? { + val response = app.get(url).document + val trueUrl = response.select("source").attr("src") + return listOf( + ExtractorLink( + this.name, + this.name, + trueUrl, + mainUrl, + getQualityFromName(""), // this needs to be auto + false + ) + ) + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt new file mode 100644 index 00000000..b9065688 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -0,0 +1,65 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import java.net.URLDecoder +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec + +class VidSrcTo : ExtractorApi() { + override val name = "VidSrcTo" + override val mainUrl = "https://vidsrc.to" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return + val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return + if (res.status != 200) return + res.result?.amap { source -> + val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap + val finalUrl = DecryptUrl(embedRes.result.encUrl) + if(finalUrl.equals(embedRes.result.encUrl)) return@amap + when (source.title) { + "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + } + } + } + + private fun DecryptUrl(encUrl: String): String { + var data = encUrl.toByteArray() + data = Base64.decode(data, Base64.URL_SAFE) + val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters) + data = cipher.doFinal(data) + return URLDecoder.decode(data.toString(Charsets.UTF_8), "utf-8") + } + + data class VidsrctoEpisodeSources( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: List? + ) + + data class VidsrctoResult( + @JsonProperty("id") val id: String, + @JsonProperty("title") val title: String + ) + + data class VidsrctoEmbedSource( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: VidsrctoUrl + ) + + data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt new file mode 100644 index 00000000..230a9e1a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt @@ -0,0 +1,101 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE +import com.lagradost.cloudstream3.utils.Qualities +import org.mozilla.javascript.Context +import org.mozilla.javascript.NativeJSON +import org.mozilla.javascript.NativeObject +import org.mozilla.javascript.Scriptable +import java.util.Base64 + +open class Vidguardto : ExtractorApi() { + override val name = "Vidguard" + override val mainUrl = "https://vidguard.to" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get(url) + val resc = res.document.select("script:containsData(eval)").firstOrNull()?.data() + resc?.let { + val jsonStr2 = AppUtils.parseJson(runJS2(it)) + val watchlink = sigDecode(jsonStr2.stream) + + callback.invoke( + ExtractorLink( + this.name, + name, + watchlink, + this.mainUrl, + Qualities.Unknown.value, + INFER_TYPE + ) + ) + } + } + + private fun sigDecode(url: String): String { + val sig = url.split("sig=")[1].split("&")[0] + var t = "" + for (v in sig.chunked(2)) { + val byteValue = Integer.parseInt(v, 16) xor 2 + t += byteValue.toChar() + } + val padding = when (t.length % 4) { + 2 -> "==" + 3 -> "=" + else -> "" + } + val decoded = Base64.getDecoder().decode((t + padding).toByteArray(Charsets.UTF_8)) + t = String(decoded).dropLast(5).reversed() + val charArray = t.toCharArray() + for (i in 0 until charArray.size - 1 step 2) { + val temp = charArray[i] + charArray[i] = charArray[i + 1] + charArray[i + 1] = temp + } + val modifiedSig = String(charArray).dropLast(5) + return url.replace(sig, modifiedSig) + } + + private fun runJS2(hideMyHtmlContent: String): String { + Log.d("runJS", "start") + val rhino = Context.enter() + rhino.initSafeStandardObjects() + rhino.optimizationLevel = -1 + val scope: Scriptable = rhino.initSafeStandardObjects() + scope.put("window", scope, scope) + var result = "" + try { + Log.d("runJS", "Executing JavaScript: $hideMyHtmlContent") + rhino.evaluateString(scope, hideMyHtmlContent, "JavaScript", 1, null) + val svgObject = scope.get("svg", scope) + result = if (svgObject is NativeObject) { + NativeJSON.stringify(Context.getCurrentContext(), scope, svgObject, null, null).toString() + } else { + Context.toString(svgObject) + } + Log.d("runJS", "Result: $result") + } catch (e: Exception) { + Log.e("runJS", "Error executing JavaScript", e) + } finally { + Context.exit() + } + return result + } + + data class SvgObject( + val stream: String, + val hash: String + ) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt index 615cfd74..979fd8c5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt @@ -25,9 +25,13 @@ open class Vidmoly : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - + val headers = mapOf( + "User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + "Sec-Fetch-Dest" to "iframe" + ) val script = app.get( url, + headers = headers, referer = referer, ).document.select("script") .find { it.data().contains("sources:") }?.data() @@ -66,4 +70,4 @@ open class Vidmoly : ExtractorApi() { @JsonProperty("kind") val kind: String? = null, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt index b9a07a6d..c5e01552 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -13,6 +13,10 @@ import javax.crypto.spec.SecretKeySpec // Code found in https://github.com/KillerDogeEmpire/vidplay-keys // special credits to @KillerDogeEmpire for providing key +class AnyVidplay(hostUrl: String) : Vidplay() { + override val mainUrl = hostUrl +} + class MyCloud : Vidplay() { override val name = "MyCloud" override val mainUrl = "https://mcloud.bz" @@ -66,7 +70,7 @@ open class Vidplay : ExtractorApi() { } private suspend fun callFutoken(id: String, url: String): String? { - val script = app.get("$mainUrl/futoken").text + val script = app.get("$mainUrl/futoken", referer = url).text val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null val a = mutableListOf(k) for (i in id.indices) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt index 2c6998de..67fd7eea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt @@ -1,19 +1,46 @@ package com.lagradost.cloudstream3.extractors +import android.util.Base64 +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.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper class Tubeless : Voe() { - override var mainUrl = "https://tubelessceliolymph.com" + override val name = "Tubeless" + override val mainUrl = "https://tubelessceliolymph.com" +} + +class Simpulumlamerop : Voe() { + override val name = "Simplum" + override var mainUrl = "https://simpulumlamerop.com" +} + +class Urochsunloath : Voe() { + override val name = "Uroch" + override var mainUrl = "https://urochsunloath.com" +} + +class Yipsu : Voe() { + override val name = "Yipsu" + override var mainUrl = "https://yip.su" +} + +class MetaGnathTuggers : Voe() { + override val name = "Metagnath" + override val mainUrl = "https://metagnathtuggers.com" } open class Voe : ExtractorApi() { override val name = "Voe" override val mainUrl = "https://voe.sx" override val requiresReferer = true + + private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex() + private val base64Regex = Regex("'.*'") override suspend fun getUrl( url: String, @@ -25,12 +52,33 @@ open class Voe : ExtractorApi() { val script = res.select("script").find { it.data().contains("sources =") }?.data() val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1) - M3u8Helper.generateM3u8( - name, - link ?: return, - "$mainUrl/", - headers = mapOf("Origin" to "$mainUrl/") - ).forEach(callback) - + val videoLinks = mutableListOf() + + if (!link.isNullOrBlank()) { + videoLinks.add( + when { + linkRegex.matches(link) -> link + else -> String(Base64.decode(link, Base64.DEFAULT)) + } + ) + } else { + val link2 = base64Regex.find(script)?.value ?: return + val decoded = Base64.decode(link2, Base64.DEFAULT).toString() + val videoLinkDTO = AppUtils.parseJson(decoded) + videoLinkDTO.let { videoLinks.add(it.toString()) } + } + + videoLinks.forEach { videoLink -> + M3u8Helper.generateM3u8( + name, + videoLink, + "$mainUrl/", + headers = mapOf("Origin" to "$mainUrl/") + ).forEach(callback) + } } -} \ No newline at end of file + + data class WcoSources( + @JsonProperty("VideoLinkDTO") val VideoLinkDTO: String, + ) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt new file mode 100644 index 00000000..919a9cbd --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt @@ -0,0 +1,39 @@ +package com.lagradost.cloudstream3.extractors + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.JsUnpacker +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.getQualityFromName +import java.net.URI + + +open class Vtbe : ExtractorApi() { + override var name = "Vtbe" + override var mainUrl = "https://vtbe.to" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?): List? { + val response = app.get(url,referer=mainUrl).document + val extractedpack =response.selectFirst("script:containsData(function(p,a,c,k,e,d))")?.data().toString() + JsUnpacker(extractedpack).unpack()?.let { unPacked -> + Regex("sources:\\[\\{file:\"(.*?)\"").find(unPacked)?.groupValues?.get(1)?.let { link -> + return listOf( + ExtractorLink( + this.name, + this.name, + link, + referer ?: "", + Qualities.Unknown.value, + URI(link).path.endsWith(".m3u8") + ) + ) + } + } + return null + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt index 50301e22..c5b4d453 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt @@ -105,6 +105,7 @@ open class TmdbProvider : MainAPI() { this.id, episode.episode_number, episode.season_number, + this.name ?: this.original_name, ).toJson(), episode.name, episode.season_number, @@ -122,6 +123,7 @@ open class TmdbProvider : MainAPI() { this.id, episodeNum, season.season_number, + this.name ?: this.original_name, ).toJson(), season = season.season_number ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt new file mode 100644 index 00000000..37c6be1b --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -0,0 +1,440 @@ +package com.lagradost.cloudstream3.metaproviders + +import android.net.Uri +import com.lagradost.cloudstream3.* +import com.fasterxml.jackson.annotation.JsonAlias +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import java.util.Locale +import java.text.SimpleDateFormat +import kotlin.math.roundToInt + +open class TraktProvider : MainAPI() { + override var name = "Trakt" + override val hasMainPage = true + override val providerType = ProviderType.MetaProvider + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + TvType.Anime, + ) + + private val traktClientId = base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==") + private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2") + + override val mainPage = mainPageOf( + "$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now + "$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time + "$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now + "$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + + val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page") + + val results = parseJson>(apiResponse).map { element -> + element.toSearchResponse() + } + return newHomePageResponse(request.name, results) + } + + private fun MediaDetails.toSearchResponse(): SearchResponse { + + val media = this.media ?: this + val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries + val poster = media.images?.poster?.firstOrNull() + + if (mediaType == TvType.Movie) { + return newMovieSearchResponse( + name = media.title!!, + url = Data( + type = mediaType, + mediaDetails = media, + ).toJson(), + type = TvType.Movie, + ) { + posterUrl = fixPath(poster) + } + } else { + return newTvSeriesSearchResponse( + name = media.title!!, + url = Data( + type = mediaType, + mediaDetails = media, + ).toJson(), + type = TvType.TvSeries, + ) { + this.posterUrl = fixPath(poster) + } + } + } + + override suspend fun search(query: String): List? { + val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query") + + val results = parseJson>(apiResponse).map { element -> + element.toSearchResponse() + } + + return results + } + override suspend fun load(url: String): LoadResponse { + + val data = parseJson(url) + val mediaDetails = data.mediaDetails + val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows" + + val posterUrl = mediaDetails?.images?.poster?.firstOrNull() + val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull() + + val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full") + + val actors = parseJson(resActor).cast?.map { + ActorData( + Actor( + name = it.person?.name!!, + image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500") + ), + roleString = it.character + ) + } + + val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20") + + val relatedMedia = parseJson>(resRelated).map { it.toSearchResponse() } + + val isCartoon = mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true + val isAnime = isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja") + val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko") + val isBollywood = mediaDetails?.country == "in" + + if (data.type == TvType.Movie) { + + val linkData = LinkData( + id = mediaDetails?.ids?.tmdb, + traktId = mediaDetails?.ids?.trakt, + traktSlug = mediaDetails?.ids?.slug, + tmdbId = mediaDetails?.ids?.tmdb, + imdbId = mediaDetails?.ids?.imdb.toString(), + tvdbId = mediaDetails?.ids?.tvdb, + tvrageId = mediaDetails?.ids?.tvrage, + type = data.type.toString(), + title = mediaDetails?.title, + year = mediaDetails?.year, + orgTitle = mediaDetails?.title, + isAnime = isAnime, + //jpTitle = later if needed as it requires another network request, + airedDate = mediaDetails?.released + ?: mediaDetails?.firstAired, + isAsian = isAsian, + isBollywood = isBollywood, + ).toJson() + + return newMovieLoadResponse( + name = mediaDetails?.title!!, + url = data.toJson(), + dataUrl = linkData.toJson(), + type = if (isAnime) TvType.AnimeMovie else TvType.Movie, + ) { + this.name = mediaDetails.title + this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie + this.posterUrl = getOriginalWidthImageUrl(posterUrl) + this.year = mediaDetails.year + this.plot = mediaDetails.overview + this.rating = mediaDetails.rating?.times(1000)?.roundToInt() + this.tags = mediaDetails.genres + this.duration = mediaDetails.runtime + this.recommendations = relatedMedia + this.actors = actors + this.comingSoon = isUpcoming(mediaDetails.released) + //posterHeaders + this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) + this.contentRating = mediaDetails.certification + addTrailer(mediaDetails.trailer) + addImdbId(mediaDetails.ids?.imdb) + addTMDbId(mediaDetails.ids?.tmdb.toString()) + } + } else { + + val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") + val episodes = mutableListOf() + val seasons = parseJson>(resSeasons) + val seasonsNames = mutableListOf() + + seasons.forEach { season -> + + seasonsNames.add( + SeasonData( + season.number!!, + season.title + ) + ) + + season.episodes?.map { episode -> + + val linkData = LinkData( + id = mediaDetails?.ids?.tmdb, + traktId = mediaDetails?.ids?.trakt, + traktSlug = mediaDetails?.ids?.slug, + tmdbId = mediaDetails?.ids?.tmdb, + imdbId = mediaDetails?.ids?.imdb.toString(), + tvdbId = mediaDetails?.ids?.tvdb, + tvrageId = mediaDetails?.ids?.tvrage, + type = data.type.toString(), + season = episode.season, + episode = episode.number, + title = mediaDetails?.title, + year = mediaDetails?.year, + orgTitle = mediaDetails?.title, + isAnime = isAnime, + airedYear = mediaDetails?.year, + lastSeason = seasons.size, + epsTitle = episode.title, + //jpTitle = later if needed as it requires another network request, + date = episode.firstAired, + airedDate = episode.firstAired, + isAsian = isAsian, + isBollywood = isBollywood, + isCartoon = isCartoon + ).toJson() + + episodes.add( + Episode( + data = linkData.toJson(), + name = episode.title, + season = episode.season, + episode = episode.number, + posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()), + rating = episode.rating?.times(10)?.roundToInt(), + description = episode.overview, + ).apply { + this.addDate(episode.firstAired) + } + ) + } + } + + return newTvSeriesLoadResponse( + name = mediaDetails?.title!!, + url = data.toJson(), + type = if (isAnime) TvType.Anime else TvType.TvSeries, + episodes = episodes + ) { + this.name = mediaDetails.title + this.type = if (isAnime) TvType.Anime else TvType.TvSeries + this.episodes = episodes + this.posterUrl = getOriginalWidthImageUrl(posterUrl) + this.year = mediaDetails.year + this.plot = mediaDetails.overview + this.showStatus = getStatus(mediaDetails.status) + this.rating = mediaDetails.rating?.times(1000)?.roundToInt() + this.tags = mediaDetails.genres + this.duration = mediaDetails.runtime + this.recommendations = relatedMedia + this.actors = actors + this.comingSoon = isUpcoming(mediaDetails.released) + //posterHeaders + this.seasonNames = seasonsNames + this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) + this.contentRating = mediaDetails.certification + addTrailer(mediaDetails.trailer) + addImdbId(mediaDetails.ids?.imdb) + addTMDbId(mediaDetails.ids?.tmdb.toString()) + } + } + } + + private suspend fun getApi(url: String) : String { + return app.get( + url = url, + headers = mapOf( + "Content-Type" to "application/json", + "trakt-api-version" to "2", + "trakt-api-key" to traktClientId, + ) + ).toString() + } + + private fun isUpcoming(dateString: String?): Boolean { + return try { + val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val dateTime = dateString?.let { format.parse(it)?.time } ?: return false + APIHolder.unixTimeMS < dateTime + } catch (t: Throwable) { + logError(t) + false + } + } + + private fun getStatus(t: String?): ShowStatus { + return when (t) { + "returning series" -> ShowStatus.Ongoing + "continuing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + + private fun fixPath(url: String?): String? { + url ?: return null + return "https://$url" + } + + private fun getWidthImageUrl(path: String?, width: String) : String? { + if (path == null) return null + if (!path.contains("image.tmdb.org")) return fixPath(path) + val fileName = Uri.parse(path).lastPathSegment ?: return null + return "https://image.tmdb.org/t/p/${width}/${fileName}" + } + + private fun getOriginalWidthImageUrl(path: String?) : String? { + if (path == null) return null + if (!path.contains("image.tmdb.org")) return fixPath(path) + return getWidthImageUrl(path, "original") + } + + data class Data( + val type: TvType? = null, + val mediaDetails: MediaDetails? = null, + ) + + data class MediaDetails( + @JsonProperty("title") val title: String? = null, + @JsonProperty("year") val year: Int? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("tagline") val tagline: String? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("released") val released: String? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("country") val country: String? = null, + @JsonProperty("updatedAt") val updatedAt: String? = null, + @JsonProperty("trailer") val trailer: String? = null, + @JsonProperty("homepage") val homepage: String? = null, + @JsonProperty("status") val status: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("votes") val votes: Long? = null, + @JsonProperty("comment_count") val commentCount: Long? = null, + @JsonProperty("language") val language: String? = null, + @JsonProperty("languages") val languages: List? = null, + @JsonProperty("available_translations") val availableTranslations: List? = null, + @JsonProperty("genres") val genres: List? = null, + @JsonProperty("certification") val certification: String? = null, + @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("airs") val airs: Airs? = null, + @JsonProperty("network") val network: String? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null + ) + + data class Airs( + @JsonProperty("day") val day: String? = null, + @JsonProperty("time") val time: String? = null, + @JsonProperty("timezone") val timezone: String? = null, + ) + + data class Ids( + @JsonProperty("trakt") val trakt: Int? = null, + @JsonProperty("slug") val slug: String? = null, + @JsonProperty("tvdb") val tvdb: Int? = null, + @JsonProperty("imdb") val imdb: String? = null, + @JsonProperty("tmdb") val tmdb: Int? = null, + @JsonProperty("tvrage") val tvrage: String? = null, + ) + + data class Images( + @JsonProperty("fanart") val fanart: List? = null, + @JsonProperty("poster") val poster: List? = null, + @JsonProperty("logo") val logo: List? = null, + @JsonProperty("clearart") val clearart: List? = null, + @JsonProperty("banner") val banner: List? = null, + @JsonProperty("thumb") val thumb: List? = null, + @JsonProperty("screenshot") val screenshot: List? = null, + @JsonProperty("headshot") val headshot: List? = null, + ) + + data class People( + @JsonProperty("cast") val cast: List? = null, + ) + + data class Cast( + @JsonProperty("character") val character: String? = null, + @JsonProperty("characters") val characters: List? = null, + @JsonProperty("episode_count") val episodeCount: Long? = null, + @JsonProperty("person") val person: Person? = null, + @JsonProperty("images") val images: Images? = null, + ) + + data class Person( + @JsonProperty("name") val name: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + ) + + data class Seasons( + @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, + @JsonProperty("episode_count") val episodeCount: Int? = null, + @JsonProperty("episodes") val episodes: List? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("network") val network: String? = null, + @JsonProperty("number") val number: Int? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("updated_at") val updatedAt: String? = null, + @JsonProperty("votes") val votes: Int? = null, + ) + + data class TraktEpisode( + @JsonProperty("available_translations") val availableTranslations: List? = null, + @JsonProperty("comment_count") val commentCount: Int? = null, + @JsonProperty("episode_type") val episodeType: String? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("number") val number: Int? = null, + @JsonProperty("number_abs") val numberAbs: Int? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("updated_at") val updatedAt: String? = null, + @JsonProperty("votes") val votes: Int? = null, + ) + + data class LinkData( + val id: Int? = null, + val traktId: Int? = null, + val traktSlug: String? = null, + val tmdbId: Int? = null, + val imdbId: String? = null, + val tvdbId: Int? = null, + val tvrageId: String? = null, + val type: String? = null, + val season: Int? = null, + val episode: Int? = null, + val aniId: String? = null, + val animeId: String? = null, + val title: String? = null, + val year: Int? = null, + val orgTitle: String? = null, + val isAnime: Boolean = false, + val airedYear: Int? = null, + val lastSeason: Int? = null, + val epsTitle: String? = null, + val jpTitle: String? = null, + val date: String? = null, + val airedDate: String? = null, + val isAsian: Boolean = false, + val isBollywood: Boolean = false, + val isCartoon: Boolean = false, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt new file mode 100644 index 00000000..3df5197c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt @@ -0,0 +1,16 @@ +package com.lagradost.cloudstream3.mvvm + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData + +/** NOTE: Only one observer at a time per value */ +fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) + liveData.observe(this) { it?.let { t -> action(t) } } +} + +/** NOTE: Only one observer at a time per value */ +fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) + liveData.observe(this) { action(it) } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 025e6fb6..a30af11c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -429,7 +429,6 @@ object PluginManager { **/ fun loadAllLocalPlugins(context: Context, forceReload: Boolean) { val dir = File(LOCAL_PLUGINS_PATH) - removeKey(PLUGINS_KEY_LOCAL) if (!dir.exists()) { val res = dir.mkdirs() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt new file mode 100644 index 00000000..d90177f5 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt @@ -0,0 +1,250 @@ +package com.lagradost.cloudstream3.ui + +import android.view.View +import android.view.ViewGroup +import androidx.core.view.children +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModel +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import androidx.viewbinding.ViewBinding +import java.util.concurrent.CopyOnWriteArrayList + +open class ViewHolderState(val view: ViewBinding) : ViewHolder(view.root) { + open fun save(): T? = null + open fun restore(state: T) = Unit + open fun onViewAttachedToWindow() = Unit + open fun onViewDetachedFromWindow() = Unit + open fun onViewRecycled() = Unit +} + + +// Based of the concept https://github.com/brahmkshatriya/echo/blob/main/app%2Fsrc%2Fmain%2Fjava%2Fdev%2Fbrahmkshatriya%2Fecho%2Fui%2Fadapters%2FMediaItemsContainerAdapter.kt#L108-L154 +class StateViewModel : ViewModel() { + val layoutManagerStates = hashMapOf>() +} + +abstract class NoStateAdapter(fragment: Fragment) : BaseAdapter(fragment, 0) + +/** + * BaseAdapter is a persistent state stored adapter that supports headers and footers. + * This should be used for restoring eg scroll or focus related to a view when it is recreated. + * + * Id is a per fragment based unique id used to store the underlying data done in an internal ViewModel. + * + * diffCallback is how the view should be handled when updating, override onUpdateContent for updates + * + * NOTE: + * + * By default it should save automatically, but you can also call save(recycle) + * + * By default no state is stored, but doing an id != 0 will store + * + * By default no headers or footers exist, override footers and headers count + */ +abstract class BaseAdapter< + T : Any, + S : Any>( + fragment: Fragment, + val id: Int = 0, + diffCallback: DiffUtil.ItemCallback = BaseDiffCallback() +) : RecyclerView.Adapter>() { + open val footers: Int = 0 + open val headers: Int = 0 + + fun getItem(position: Int): T { + return mDiffer.currentList[position] + } + + fun getItemOrNull(position: Int): T? { + return mDiffer.currentList.getOrNull(position) + } + + private val mDiffer: AsyncListDiffer = AsyncListDiffer( + object : NonFinalAdapterListUpdateCallback(this) { + override fun onMoved(fromPosition: Int, toPosition: Int) { + super.onMoved(fromPosition + headers, toPosition + headers) + } + + override fun onRemoved(position: Int, count: Int) { + super.onRemoved(position + headers, count) + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + super.onChanged(position + headers, count, payload) + } + + override fun onInserted(position: Int, count: Int) { + super.onInserted(position + headers, count) + } + }, + AsyncDifferConfig.Builder(diffCallback).build() + ) + + open fun submitList(list: List?) { + // deep copy at least the top list, because otherwise adapter can go crazy + mDiffer.submitList(list?.let { CopyOnWriteArrayList(it) }) + } + + override fun getItemCount(): Int { + return mDiffer.currentList.size + footers + headers + } + + open fun onUpdateContent(holder: ViewHolderState, item: T, position: Int) = + onBindContent(holder, item, position) + + open fun onBindContent(holder: ViewHolderState, item: T, position: Int) = Unit + open fun onBindFooter(holder: ViewHolderState) = Unit + open fun onBindHeader(holder: ViewHolderState) = Unit + open fun onCreateContent(parent: ViewGroup): ViewHolderState = throw NotImplementedError() + open fun onCreateFooter(parent: ViewGroup): ViewHolderState = throw NotImplementedError() + open fun onCreateHeader(parent: ViewGroup): ViewHolderState = throw NotImplementedError() + + override fun onViewAttachedToWindow(holder: ViewHolderState) { + holder.onViewAttachedToWindow() + } + + override fun onViewDetachedFromWindow(holder: ViewHolderState) { + holder.onViewDetachedFromWindow() + } + + fun save(recyclerView: RecyclerView) { + for (child in recyclerView.children) { + val holder = + recyclerView.findContainingViewHolder(child) as? ViewHolderState ?: continue + setState(holder) + } + } + + fun clear() { + stateViewModel.layoutManagerStates[id]?.clear() + } + + private fun getState(holder: ViewHolderState): S? = + stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S + + private fun setState(holder: ViewHolderState) { + if(id == 0) return + + if (!stateViewModel.layoutManagerStates.contains(id)) { + stateViewModel.layoutManagerStates[id] = HashMap() + } + stateViewModel.layoutManagerStates[id]?.let { map -> + map[holder.absoluteAdapterPosition] = holder.save() + } + } + + private val attachListener = object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) = Unit + override fun onViewDetachedFromWindow(v: View) { + if (v !is RecyclerView) return + save(v) + } + } + + final override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + recyclerView.addOnAttachStateChangeListener(attachListener) + super.onAttachedToRecyclerView(recyclerView) + } + + final override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + recyclerView.removeOnAttachStateChangeListener(attachListener) + super.onDetachedFromRecyclerView(recyclerView) + } + + final override fun getItemViewType(position: Int): Int { + if (position < headers) { + return HEADER + } + if (position - headers >= mDiffer.currentList.size) { + return FOOTER + } + + return CONTENT + } + + private val stateViewModel: StateViewModel by fragment.viewModels() + + final override fun onViewRecycled(holder: ViewHolderState) { + setState(holder) + holder.onViewRecycled() + super.onViewRecycled(holder) + } + + final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderState { + return when (viewType) { + CONTENT -> onCreateContent(parent) + HEADER -> onCreateHeader(parent) + FOOTER -> onCreateFooter(parent) + else -> throw NotImplementedError() + } + } + + // https://medium.com/@domen.lanisnik/efficiently-updating-recyclerview-items-using-payloads-1305f65f3068 + override fun onBindViewHolder( + holder: ViewHolderState, + position: Int, + payloads: MutableList + ) { + if (payloads.isEmpty()) { + super.onBindViewHolder(holder, position, payloads) + return + } + when (getItemViewType(position)) { + CONTENT -> { + val realPosition = position - headers + val item = getItem(realPosition) + onUpdateContent(holder, item, realPosition) + } + + FOOTER -> { + onBindFooter(holder) + } + + HEADER -> { + onBindHeader(holder) + } + } + } + + final override fun onBindViewHolder(holder: ViewHolderState, position: Int) { + when (getItemViewType(position)) { + CONTENT -> { + val realPosition = position - headers + val item = getItem(realPosition) + onBindContent(holder, item, realPosition) + } + + FOOTER -> { + onBindFooter(holder) + } + + HEADER -> { + onBindHeader(holder) + } + } + + getState(holder)?.let { state -> + holder.restore(state) + } + } + + companion object { + private const val HEADER: Int = 1 + private const val FOOTER: Int = 2 + private const val CONTENT: Int = 0 + } +} + +class BaseDiffCallback( + val itemSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() }, + val contentSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() } +) : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = itemSame(oldItem, newItem) + override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = contentSame(oldItem, newItem) + override fun getChangePayload(oldItem: T, newItem: T): Any = Any() +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt new file mode 100644 index 00000000..f721401e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt @@ -0,0 +1,39 @@ +package com.lagradost.cloudstream3.ui + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListUpdateCallback +import androidx.recyclerview.widget.RecyclerView + + +/** + * ListUpdateCallback that dispatches update events to the given adapter. + * + * @see DiffUtil.DiffResult.dispatchUpdatesTo + */ +open class NonFinalAdapterListUpdateCallback +/** + * Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter. + * + * @param adapter The Adapter to send updates to. + */(private var mAdapter: RecyclerView.Adapter<*>) : + ListUpdateCallback { + + override fun onInserted(position: Int, count: Int) { + mAdapter.notifyItemRangeInserted(position, count) + } + + override fun onRemoved(position: Int, count: Int) { + mAdapter.notifyItemRangeRemoved(position, count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + mAdapter.notifyItemMoved(fromPosition, toPosition) + } + + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + override fun onChanged(position: Int, count: Int, payload: Any?) { + mAdapter.notifyItemRangeChanged(position, count, payload) + } +} + diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt index 41aef176..0b0d83db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -23,7 +23,10 @@ import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex @@ -48,7 +51,6 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet ) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) val skipStartup = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false ) || accounts.count() <= 1 @@ -56,7 +58,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet fun askBiometricAuth() { - if (isLayout(PHONE) && authEnabled) { + if (isLayout(PHONE) && isAuthEnabled(this)) { if (deviceHasPasswordPinLock(this)) { startBiometricAuthentication( this, @@ -64,8 +66,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet false ) - BiometricAuthenticator.promptInfo?.let { promt -> - BiometricAuthenticator.biometricPrompt?.authenticate(promt) + promptInfo?.let { prompt -> + biometricPrompt?.authenticate(prompt) } } } @@ -189,4 +191,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet override fun onAuthenticationSuccess() { Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity") } + + override fun onAuthenticationError() { + finish() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index c3ec2bbd..f54c8698 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -11,10 +11,14 @@ import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar +import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import kotlinx.coroutines.Dispatchers @@ -85,13 +89,15 @@ class DownloadChildFragment : Fragment() { binding?.downloadChildToolbar?.apply { title = name - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } + setAppBarNoScrollFlagsOnTV() } - val adapter: RecyclerView.Adapter = DownloadChildAdapter( ArrayList(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index e08eb772..31790b0f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -41,6 +41,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import java.net.URI @@ -97,6 +98,8 @@ class DownloadFragment : Fragment() { super.onViewCreated(view, savedInstanceState) hideKeyboard() + binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() + observe(downloadsViewModel.noDownloadsText) { binding?.textNoDownloads?.text = it } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index a729f33a..f1031c24 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -13,6 +13,8 @@ import androidx.annotation.MainThread import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE @@ -25,6 +27,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager +import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES open class PieFetchButton(context: Context, attributeSet: AttributeSet) : @@ -167,6 +170,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : this.setPersistentId(card.id) view.setOnClickListener { if (isZeroBytes) { + removeKey(KEY_RESUME_PACKAGES, card.id.toString()) callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card)) //callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index f84966eb..ebed901f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -2,31 +2,58 @@ package com.lagradost.cloudstream3.ui.home import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView +import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.databinding.HomeResultGridBinding import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding +import com.lagradost.cloudstream3.ui.BaseAdapter +import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder -import com.lagradost.cloudstream3.utils.AppUtils.isRtl +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx -class HomeChildItemAdapter( - val cardList: MutableList, +class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState(view) { + /*private fun recursive(view : View) : Boolean { + if (view.isFocused) { + println("VIEW: $view | id=${view.id}") + } + return (view as? ViewGroup)?.children?.any { recursive(it) } ?: false + }*/ + // very shitty that we cant store the state when the view clears, + // but this is because the focus clears before the view is removed + // so we have to manually store it + var wasFocused: Boolean = false + override fun save(): Boolean = wasFocused + override fun restore(state: Boolean) { + if (state) { + wasFocused = false + // only refocus if tv + if(isLayout(TV)) { + itemView.requestFocus() + } + } + } +} + +class HomeChildItemAdapter( + fragment: Fragment, + id: Int, private val nextFocusUp: Int? = null, private val nextFocusDown: Int? = null, private val clickCallback: (SearchClickCallback) -> Unit, ) : - RecyclerView.Adapter() { + BaseAdapter(fragment, id) { var isHorizontal: Boolean = false var hasNext: Boolean = false - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + override fun onCreateContent(parent: ViewGroup): ViewHolderState { val expanded = parent.context.IsBottomLayout() /* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid @@ -39,164 +66,78 @@ class HomeChildItemAdapter( parent, false ) else HomeResultGridBinding.inflate(inflater, parent, false) + return HomeScrollViewHolderState(binding) + } + override fun onBindContent( + holder: ViewHolderState, + item: SearchResponse, + position: Int + ) { + when (val binding = holder.view) { + is HomeResultGridBinding -> { + binding.backgroundCard.apply { + val min = 114.toPx + val max = 180.toPx - return CardViewHolder( - binding, - clickCallback, - itemCount, + layoutParams = + layoutParams.apply { + width = if (!isHorizontal) { + min + } else { + max + } + height = if (!isHorizontal) { + max + } else { + min + } + } + } + } + + is HomeResultGridExpandedBinding -> { + binding.backgroundCard.apply { + val min = 114.toPx + val max = 180.toPx + + layoutParams = + layoutParams.apply { + width = if (!isHorizontal) { + min + } else { + max + } + height = if (!isHorizontal) { + max + } else { + min + } + } + } + + if (position == 0) { // to fix tv + binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view + } + } + } + + SearchResultBuilder.bind( + clickCallback = { click -> + // ok, so here we hijack the callback to fix the focus + when (click.action) { + SEARCH_ACTION_LOAD -> (holder as? HomeScrollViewHolderState)?.wasFocused = true + } + clickCallback(click) + }, + item, + position, + holder.itemView, + null, // nextFocusBehavior, nextFocusUp, - nextFocusDown, - isHorizontal, - parent.isRtl() - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is CardViewHolder -> { - holder.itemCount = itemCount // i know ugly af - holder.bind(cardList[position], position) - } - } - } - - override fun getItemCount(): Int { - return cardList.size - } - - override fun getItemId(position: Int): Long { - return (cardList[position].id ?: position).toLong() - } - - fun updateList(newList: List) { - val diffResult = DiffUtil.calculateDiff( - HomeChildDiffCallback(this.cardList, newList) + nextFocusDown ) - cardList.clear() - cardList.addAll(newList) - - diffResult.dispatchUpdatesTo(this) - } - - class CardViewHolder - constructor( - val binding: ViewBinding, - private val clickCallback: (SearchClickCallback) -> Unit, - var itemCount: Int, - private val nextFocusUp: Int? = null, - private val nextFocusDown: Int? = null, - private val isHorizontal: Boolean = false, - private val isRtl: Boolean - ) : - RecyclerView.ViewHolder(binding.root) { - - fun bind(card: SearchResponse, position: Int) { - - // TV focus fixing - /*val nextFocusBehavior = when (position) { - 0 -> true - itemCount - 1 -> false - else -> null - } - - if (position == 0) { // to fix tv - if (isRtl) { - itemView.nextFocusRightId = R.id.nav_rail_view - itemView.nextFocusLeftId = -1 - } - else { - itemView.nextFocusLeftId = R.id.nav_rail_view - itemView.nextFocusRightId = -1 - } - } else { - itemView.nextFocusRightId = -1 - itemView.nextFocusLeftId = -1 - }*/ - - - when (binding) { - is HomeResultGridBinding -> { - binding.backgroundCard.apply { - val min = 114.toPx - val max = 180.toPx - - layoutParams = - layoutParams.apply { - width = if (!isHorizontal) { - min - } else { - max - } - height = if (!isHorizontal) { - max - } else { - min - } - } - } - - - } - - is HomeResultGridExpandedBinding -> { - binding.backgroundCard.apply { - val min = 114.toPx - val max = 180.toPx - - layoutParams = - layoutParams.apply { - width = if (!isHorizontal) { - min - } else { - max - } - height = if (!isHorizontal) { - max - } else { - min - } - } - } - - if (position == 0) { // to fix tv - binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view - } - } - } - - SearchResultBuilder.bind( - clickCallback, - card, - position, - itemView, - null, // nextFocusBehavior, - nextFocusUp, - nextFocusDown - ) - itemView.tag = position - - //val ani = ScaleAnimation(0.9f, 1.0f, 0.9f, 1f) - //ani.fillAfter = true - //ani.duration = 200 - //itemView.startAnimation(ani) - } + holder.itemView.tag = position } } - -class HomeChildDiffCallback( - private val oldList: List, - private val newList: List -) : - DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition].name == newList[newItemPosition].name - - override fun getOldListSize() = oldList.size - - override fun getNewListSize() = newList.size - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition] == newList[newItemPosition] && oldItemPosition < oldList.size - 1 // always update the last item -} \ No newline at end of file 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 7a68330f..12185cbf 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 @@ -451,10 +451,6 @@ class HomeFragment : Fragment() { } override fun onDestroyView() { - homeMasterAdapter?.onSaveInstanceState( - instanceState, - binding?.homeMasterRecycler - ) bottomSheetDialog?.ownHide() binding = null @@ -517,11 +513,9 @@ class HomeFragment : Fragment() { } } homeMasterAdapter = HomeParentItemAdapterPreview( - mutableListOf(), + fragment = this@HomeFragment, homeViewModel, - ).apply { - onRestoreInstanceState(instanceState) - } + ) homeMasterRecycler.adapter = homeMasterAdapter //fixPaddingStatusbar(homeLoadingStatusbar) @@ -572,10 +566,11 @@ class HomeFragment : Fragment() { val mutableListOfResponse = mutableListOf() listHomepageItems.clear() - (homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList( - d.values.toMutableList(), - homeMasterRecycler - ) + (homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(d.values.map { + it.copy( + list = it.list.copy(list = it.list.list.toMutableList()) + ) + }.toMutableList()) homeLoading.isVisible = false homeLoadingError.isVisible = false @@ -624,7 +619,7 @@ class HomeFragment : Fragment() { } is Resource.Loading -> { - (homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(listOf()) + (homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(listOf()) homeLoadingShimmer.startShimmer() homeLoading.isVisible = true homeLoadingError.isVisible = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index 50111428..4b0360d7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -1,24 +1,23 @@ package com.lagradost.cloudstream3.ui.home import android.os.Bundle -import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListUpdateCallback +import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.ViewHolder +import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.HomepageParentBinding import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.ui.BaseAdapter +import com.lagradost.cloudstream3.ui.BaseDiffCallback +import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SearchClickCallback -import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -33,256 +32,89 @@ class LoadClickCallback( ) open class ParentItemAdapter( - private var items: MutableList, - //private val viewModel: HomeViewModel, + open val fragment: Fragment, + id: Int, private val clickCallback: (SearchClickCallback) -> Unit, private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, private val expandCallback: ((String) -> Unit)? = null, -) : RecyclerView.Adapter() { - // Ok, this is fucked, but there is a reason for this as we want to resume 1. when scrolling up and down - // and 2. when doing into a thing and coming back. 1 is always active, but 2 requires doing it in the fragment - // as OnCreateView is called and this adapter is recreated losing the internal state to the GC - // - // 1. This works by having the adapter having a internal state "scrollStates" that keeps track of the states - // when a view recycles, it looks up this internal state - // 2. To solve the the coming back shit we have to save "scrollStates" to a Bundle inside the - // fragment via onSaveInstanceState, because this cant be easy for some reason as the adapter does - // not have a state but the layout-manager for no reason, then it is resumed via onRestoreInstanceState - // - // Even when looking at a real example they do this :skull: - // https://github.com/vivchar/RendererRecyclerViewAdapter/blob/185251ee9d94fb6eb3e063b00d646b745186c365/example/src/main/java/com/github/vivchar/example/pages/github/GithubFragment.kt#L32 - private val scrollStates = mutableMapOf() - - companion object { - private const val SCROLL_KEY: String = "ParentItemAdapter::scrollStates.keys" - private const val SCROLL_VALUE: String = "ParentItemAdapter::scrollStates.values" - } - - open fun onRestoreInstanceState(savedInstanceState: Bundle?) { - try { - val keys = savedInstanceState?.getIntArray(SCROLL_KEY) ?: intArrayOf() - val values = savedInstanceState?.getParcelableArray(SCROLL_VALUE) ?: arrayOf() - for ((k, v) in keys.zip(values)) { - this.scrollStates[k] = v - } - } catch (t: Throwable) { - logError(t) - } - } - - open fun onSaveInstanceState(outState: Bundle, recyclerView: RecyclerView? = null) { - if (recyclerView != null) { - for (position in items.indices) { - val holder = recyclerView.findViewHolderForAdapterPosition(position) ?: continue - saveHolder(holder) - } - } - - outState.putIntArray(SCROLL_KEY, scrollStates.keys.toIntArray()) - outState.putParcelableArray(SCROLL_VALUE, scrollStates.values.toTypedArray()) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - when (holder) { - is ParentViewHolder -> { - holder.bind(items[position]) - scrollStates[holder.absoluteAdapterPosition]?.let { - holder.binding.homeChildRecyclerview.layoutManager?.onRestoreInstanceState(it) - } - } - } - } - - private fun saveHolder(holder : ViewHolder) { - when (holder) { - is ParentViewHolder -> { - scrollStates[holder.absoluteAdapterPosition] = - holder.binding.homeChildRecyclerview.layoutManager?.onSaveInstanceState() - } - } - } - - override fun onViewRecycled(holder: ViewHolder) { - saveHolder(holder) - super.onViewRecycled(holder) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val layoutResId = when { - isLayout(TV) -> R.layout.homepage_parent_tv - isLayout(EMULATOR) -> R.layout.homepage_parent_emulator - else -> R.layout.homepage_parent - } - - val inflater = LayoutInflater.from(parent.context) - val binding = try { - HomepageParentBinding.bind(inflater.inflate(layoutResId, parent, false)) - } catch (t : Throwable) { - logError(t) - // just in case someone forgot we don't want to crash - HomepageParentBinding.inflate(inflater) - } - - return ParentViewHolder( - binding, - clickCallback, - moreInfoClickCallback, - expandCallback - ) - } - - override fun getItemCount(): Int { - return items.size - } - - override fun getItemId(position: Int): Long { - return items[position].list.name.hashCode().toLong() - } - - @JvmName("updateListHomePageList") - fun updateList(newList: List) { - updateList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) } - .toMutableList()) - } - - @JvmName("updateListExpandableHomepageList") - fun updateList( - newList: MutableList, - recyclerView: RecyclerView? = null - ) { - // this - // 1. prevents deep copy that makes this.items == newList - // 2. filters out undesirable results - // 3. moves empty results to the bottom (sortedBy is a stable sort) - val new = - newList.map { it.copy(list = it.list.copy(list = it.list.list.filterSearchResponse())) } - .sortedBy { it.list.list.isEmpty() } - - val diffResult = DiffUtil.calculateDiff( - SearchDiffCallback(items, new) - ) - items.clear() - items.addAll(new) - - //val mAdapter = this - val delta = if (this@ParentItemAdapter is HomeParentItemAdapterPreview) { - headItems - } else { - 0 - } - - diffResult.dispatchUpdatesTo(object : ListUpdateCallback { - override fun onInserted(position: Int, count: Int) { - //notifyItemRangeChanged(position + delta, count) - notifyItemRangeInserted(position + delta, count) - } - - override fun onRemoved(position: Int, count: Int) { - notifyItemRangeRemoved(position + delta, count) - } - - override fun onMoved(fromPosition: Int, toPosition: Int) { - notifyItemMoved(fromPosition + delta, toPosition + delta) - } - - override fun onChanged(_position: Int, count: Int, payload: Any?) { - val position = _position + delta - - // I know kinda messy, what this does is using the update or bind instead of onCreateViewHolder -> bind - recyclerView?.apply { - // this loops every viewHolder in the recycle view and checks the position to see if it is within the update range - val missingUpdates = (position until (position + count)).toMutableSet() - for (i in 0 until itemCount) { - val child = getChildAt(i) ?: continue - val viewHolder = getChildViewHolder(child) ?: continue - if (viewHolder !is ParentViewHolder) continue - - val absolutePosition = viewHolder.bindingAdapterPosition - if (absolutePosition >= position && absolutePosition < position + count) { - val expand = items.getOrNull(absolutePosition - delta) ?: continue - missingUpdates -= absolutePosition - //println("Updating ${viewHolder.title.text} ($absolutePosition $position) -> ${expand.list.name}") - if (viewHolder.title.text == expand.list.name) { - viewHolder.update(expand) - } else { - viewHolder.bind(expand) - } - } - } - - // just in case some item did not get updated - for (i in missingUpdates) { - notifyItemChanged(i, payload) - } - } ?: run { - // in case we don't have a nice - notifyItemRangeChanged(position, count, payload) - } - } +) : BaseAdapter( + fragment, + id, + diffCallback = BaseDiffCallback( + itemSame = { a, b -> a.list.name == b.list.name }, + contentSame = { a, b -> + a.list.list == b.list.list }) - - //diffResult.dispatchUpdatesTo(this) - } - - - class ParentViewHolder( - val binding: HomepageParentBinding, - // val viewModel: HomeViewModel, - private val clickCallback: (SearchClickCallback) -> Unit, - private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, - private val expandCallback: ((String) -> Unit)? = null, - ) : - ViewHolder(binding.root) { - val title: TextView = binding.homeChildMoreInfo - private val recyclerView: RecyclerView = binding.homeChildRecyclerview - private val startFocus = R.id.nav_rail_view - private val endFocus = FOCUS_SELF - fun update(expand: HomeViewModel.ExpandableHomepageList) { - val info = expand.list - (recyclerView.adapter as? HomeChildItemAdapter?)?.apply { - updateList(info.list.toMutableList()) - hasNext = expand.hasNext - } ?: run { - recyclerView.adapter = HomeChildItemAdapter( - info.list.toMutableList(), - clickCallback = clickCallback, - nextFocusUp = recyclerView.nextFocusUpId, - nextFocusDown = recyclerView.nextFocusDownId, - ).apply { - isHorizontal = info.isHorizontalImages - hasNext = expand.hasNext - } - recyclerView.setLinearListLayout( - isHorizontal = true, - nextLeft = startFocus, - nextRight = endFocus, - ) - } +) { + data class ParentItemHolder(val binding: ViewBinding) : ViewHolderState(binding) { + override fun save(): Bundle = Bundle().apply { + val recyclerView = (binding as? HomepageParentBinding)?.homeChildRecyclerview + putParcelable( + "value", + recyclerView?.layoutManager?.onSaveInstanceState() + ) + (recyclerView?.adapter as? BaseAdapter<*,*>)?.save(recyclerView) } - fun bind(expand: HomeViewModel.ExpandableHomepageList) { - val info = expand.list - recyclerView.adapter = HomeChildItemAdapter( - info.list.toMutableList(), + override fun restore(state: Bundle) { + (binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState( + state.getParcelable("value") + ) + } + } + + override fun submitList(list: List?) { + super.submitList(list?.sortedBy { it.list.list.isEmpty() }) + } + + override fun onUpdateContent( + holder: ViewHolderState, + item: HomeViewModel.ExpandableHomepageList, + position: Int + ) { + val binding = holder.view + if (binding !is HomepageParentBinding) return + (binding.homeChildRecyclerview.adapter as? HomeChildItemAdapter)?.submitList(item.list.list) + } + + override fun onBindContent( + holder: ViewHolderState, + item: HomeViewModel.ExpandableHomepageList, + position: Int + ) { + val startFocus = R.id.nav_rail_view + val endFocus = FOCUS_SELF + val binding = holder.view + if (binding !is HomepageParentBinding) return + val info = item.list + binding.apply { + homeChildRecyclerview.adapter = HomeChildItemAdapter( + fragment = fragment, + id = id + position + 100, clickCallback = clickCallback, - nextFocusUp = recyclerView.nextFocusUpId, - nextFocusDown = recyclerView.nextFocusDownId, + nextFocusUp = homeChildRecyclerview.nextFocusUpId, + nextFocusDown = homeChildRecyclerview.nextFocusDownId, ).apply { isHorizontal = info.isHorizontalImages - hasNext = expand.hasNext + hasNext = item.hasNext + submitList(item.list.list) } - recyclerView.setLinearListLayout( + homeChildRecyclerview.setLinearListLayout( isHorizontal = true, nextLeft = startFocus, nextRight = endFocus, ) - title.text = info.name + homeChildMoreInfo.text = info.name - recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + homeChildRecyclerview.addOnScrollListener(object : + RecyclerView.OnScrollListener() { var expandCount = 0 - val name = expand.list.name + val name = item.list.name - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + override fun onScrollStateChanged( + recyclerView: RecyclerView, + newState: Int + ) { super.onScrollStateChanged(recyclerView, newState) val adapter = recyclerView.adapter @@ -307,26 +139,34 @@ open class ParentItemAdapter( //(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged() if (isLayout(PHONE)) { - title.setOnClickListener { - moreInfoClickCallback.invoke(expand) + homeChildMoreInfo.setOnClickListener { + moreInfoClickCallback.invoke(item) } } } } -} -class SearchDiffCallback( - private val oldList: List, - private val newList: List -) : - DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition].list.name == newList[newItemPosition].list.name + override fun onCreateContent(parent: ViewGroup): ParentItemHolder { + val layoutResId = when { + isLayout(TV) -> R.layout.homepage_parent_tv + isLayout(EMULATOR) -> R.layout.homepage_parent_emulator + else -> R.layout.homepage_parent + } - override fun getOldListSize() = oldList.size + val inflater = LayoutInflater.from(parent.context) + val binding = try { + HomepageParentBinding.bind(inflater.inflate(layoutResId, parent, false)) + } catch (t: Throwable) { + logError(t) + // just in case someone forgot we don't want to crash + HomepageParentBinding.inflate(inflater) + } - override fun getNewListSize() = newList.size + return ParentItemHolder(binding) + } - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = - oldList[oldItemPosition] == newList[newItemPosition] + fun updateList(newList: List) { + submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) } + .toMutableList()) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 7ad15e4e..52ec06db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -1,5 +1,7 @@ package com.lagradost.cloudstream3.ui.home +import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -7,6 +9,7 @@ import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.fragment.app.Fragment import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding @@ -26,6 +29,7 @@ import com.lagradost.cloudstream3.databinding.FragmentHomeHeadTvBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage @@ -47,114 +51,87 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView import com.lagradost.cloudstream3.utils.UIHelper.populateChips class HomeParentItemAdapterPreview( - items: MutableList, + override val fragment: Fragment, private val viewModel: HomeViewModel, -) : ParentItemAdapter(items, +) : ParentItemAdapter(fragment, id = "HomeParentItemAdapterPreview".hashCode(), clickCallback = { - viewModel.click(it) -}, moreInfoClickCallback = { - viewModel.popup(it) -}, expandCallback = { - viewModel.expand(it) -}) { - val headItems = 1 + viewModel.click(it) + }, moreInfoClickCallback = { + viewModel.popup(it) + }, expandCallback = { + viewModel.expand(it) + }) { + override val headers = 1 + override fun onCreateHeader(parent: ViewGroup): ViewHolderState { + val inflater = LayoutInflater.from(parent.context) + val binding = if (isLayout(TV or EMULATOR)) FragmentHomeHeadTvBinding.inflate( + inflater, + parent, + false + ) else FragmentHomeHeadBinding.inflate(inflater, parent, false) - companion object { - private const val VIEW_TYPE_HEADER = 2 - private const val VIEW_TYPE_ITEM = 1 - } + if (binding is FragmentHomeHeadTvBinding && isLayout(EMULATOR)) { + binding.homeBookmarkParentItemMoreInfo.isVisible = true - override fun getItemViewType(position: Int) = when (position) { - 0 -> VIEW_TYPE_HEADER - else -> VIEW_TYPE_ITEM - } + val marginInDp = 50 + val density = binding.horizontalScrollChips.context.resources.displayMetrics.density + val marginInPixels = (marginInDp * density).toInt() - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is HeaderViewHolder -> {} - else -> super.onBindViewHolder(holder, position - headItems) + val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams + params.marginEnd = marginInPixels + binding.horizontalScrollChips.layoutParams = params + binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds( + null, + null, + ContextCompat.getDrawable( + parent.context, + R.drawable.ic_baseline_arrow_forward_24 + ), + null + ) } + + return HeaderViewHolder(binding, viewModel, fragment = fragment) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - VIEW_TYPE_HEADER -> { - val inflater = LayoutInflater.from(parent.context) - val binding = if (isLayout(TV or EMULATOR)) FragmentHomeHeadTvBinding.inflate( - inflater, - parent, - false - ) else FragmentHomeHeadBinding.inflate(inflater, parent, false) + override fun onBindHeader(holder: ViewHolderState) { + (holder as? HeaderViewHolder)?.bind() + } - if (binding is FragmentHomeHeadTvBinding && isLayout(EMULATOR)) { - binding.homeBookmarkParentItemMoreInfo.isVisible = true + private class HeaderViewHolder( + val binding: ViewBinding, val viewModel: HomeViewModel, fragment: Fragment, + ) : + ViewHolderState(binding) { - val marginInDp = 50 - val density = binding.horizontalScrollChips.context.resources.displayMetrics.density - val marginInPixels = (marginInDp * density).toInt() - - val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams - params.marginEnd = marginInPixels - binding.horizontalScrollChips.layoutParams = params - binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds( - null, - null, - ContextCompat.getDrawable( - parent.context, - R.drawable.ic_baseline_arrow_forward_24 - ), - null - ) - } - - HeaderViewHolder( - binding, - viewModel, + override fun save(): Bundle = + Bundle().apply { + putParcelable( + "resumeRecyclerView", + resumeRecyclerView.layoutManager?.onSaveInstanceState() ) + putParcelable( + "bookmarkRecyclerView", + bookmarkRecyclerView.layoutManager?.onSaveInstanceState() + ) + //putInt("previewViewpager", previewViewpager.currentItem) } - VIEW_TYPE_ITEM -> super.onCreateViewHolder(parent, viewType) - else -> error("Unhandled viewType=$viewType") - } - } - - override fun getItemCount(): Int { - return super.getItemCount() + headItems - } - - override fun getItemId(position: Int): Long { - if (position == 0) return 0//previewData.hashCode().toLong() - return super.getItemId(position - headItems) - } - - override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { - when (holder) { - is HeaderViewHolder -> { - holder.onViewDetachedFromWindow() + override fun restore(state: Bundle) { + state.getParcelable("resumeRecyclerView")?.let { recycle -> + resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - - else -> super.onViewDetachedFromWindow(holder) - } - } - - override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) { - when (holder) { - is HeaderViewHolder -> { - holder.onViewAttachedToWindow() + state.getParcelable("bookmarkRecyclerView")?.let { recycle -> + bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - - else -> super.onViewAttachedToWindow(holder) + //state.getInt("previewViewpager").let { recycle -> + // previewViewpager.setCurrentItem(recycle,true) + //} } - } - class HeaderViewHolder - constructor( - val binding: ViewBinding, - val viewModel: HomeViewModel, - ) : RecyclerView.ViewHolder(binding.root) { - private var previewAdapter: HomeScrollAdapter = HomeScrollAdapter() - private var resumeAdapter: HomeChildItemAdapter = HomeChildItemAdapter( - ArrayList(), + val previewAdapter = HomeScrollAdapter(fragment = fragment) + private val resumeAdapter = HomeChildItemAdapter( + fragment, + id = "resumeAdapter".hashCode(), nextFocusUp = itemView.nextFocusUpId, nextFocusDown = itemView.nextFocusDownId ) { callback -> @@ -209,8 +186,9 @@ class HomeParentItemAdapterPreview( } } } - private var bookmarkAdapter: HomeChildItemAdapter = HomeChildItemAdapter( - ArrayList(), + private val bookmarkAdapter = HomeChildItemAdapter( + fragment, + id = "bookmarkAdapter".hashCode(), nextFocusUp = itemView.nextFocusUpId, nextFocusDown = itemView.nextFocusDownId ) { callback -> @@ -219,7 +197,10 @@ class HomeParentItemAdapterPreview( return@HomeChildItemAdapter } - (callback.view.context?.getActivity() as? MainActivity)?.loadPopup(callback.card, load = false) + (callback.view.context?.getActivity() as? MainActivity)?.loadPopup( + callback.card, + load = false + ) /* callback.view.context?.getActivity()?.showOptionSelectStringRes( callback.view, @@ -269,7 +250,6 @@ class HomeParentItemAdapterPreview( */ } - private val previewViewpager: ViewPager2 = itemView.findViewById(R.id.home_preview_viewpager) @@ -277,38 +257,24 @@ class HomeParentItemAdapterPreview( itemView.findViewById(R.id.home_preview_viewpager_text) // private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview) - private var resumeHolder: View = itemView.findViewById(R.id.home_watch_holder) - private var resumeRecyclerView: RecyclerView = + private val resumeHolder: View = itemView.findViewById(R.id.home_watch_holder) + private val resumeRecyclerView: RecyclerView = itemView.findViewById(R.id.home_watch_child_recyclerview) - private var bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder) - private var bookmarkRecyclerView: RecyclerView = + private val bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder) + private val bookmarkRecyclerView: RecyclerView = itemView.findViewById(R.id.home_bookmarked_child_recyclerview) - private var homeAccount: View? = - itemView.findViewById(R.id.home_preview_switch_account) - private var alternativeHomeAccount: View? = + private val homeAccount: View? = itemView.findViewById(R.id.home_preview_switch_account) + private val alternativeHomeAccount: View? = itemView.findViewById(R.id.alternative_switch_account) - private var topPadding: View? = itemView.findViewById(R.id.home_padding) + private val topPadding: View? = itemView.findViewById(R.id.home_padding) - private var alternativeAccountPadding: View? = itemView.findViewById(R.id.alternative_account_padding) + private val alternativeAccountPadding: View? = + itemView.findViewById(R.id.alternative_account_padding) private val homeNonePadding: View = itemView.findViewById(R.id.home_none_padding) - private val previewCallback: ViewPager2.OnPageChangeCallback = - object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - previewAdapter.apply { - if (position >= itemCount - 1 && hasMoreItems) { - hasMoreItems = false // don't make two requests - viewModel.loadMoreHomeScrollResponses() - } - } - val item = previewAdapter.getItem(position) ?: return - onSelect(item, position) - } - } - fun onSelect(item: LoadResponse, position: Int) { (binding as? FragmentHomeHeadTvBinding)?.apply { homePreviewDescription.isGone = @@ -381,14 +347,14 @@ class HomeParentItemAdapterPreview( homePreviewBookmark.setOnClickListener { fab -> fab.context.getActivity()?.showBottomDialog( - WatchType.values() + WatchType.entries .map { fab.context.getString(it.stringRes) } .toList(), DataStoreHelper.getResultWatchState(id).ordinal, fab.context.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - val newValue = WatchType.values()[it] + val newValue = WatchType.entries[it] ResultViewModel2().updateWatchStatus( newValue, @@ -413,38 +379,22 @@ class HomeParentItemAdapterPreview( } } - fun onViewDetachedFromWindow() { - previewViewpager.unregisterOnPageChangeCallback(previewCallback) - } - - fun onViewAttachedToWindow() { - previewViewpager.registerOnPageChangeCallback(previewCallback) - - binding.root.findViewTreeLifecycleOwner()?.apply { - observe(viewModel.preview) { - updatePreview(it) - } - if (binding is FragmentHomeHeadTvBinding) { - observe(viewModel.apiName) { name -> - binding.homePreviewChangeApi.text = name - } - } - observe(viewModel.resumeWatching) { - updateResume(it) - } - observe(viewModel.bookmarks) { - updateBookmarks(it) - } - observe(viewModel.availableWatchStatusTypes) { (checked, visible) -> - for ((chip, watch) in toggleList) { - chip.apply { - isVisible = visible.contains(watch) - isChecked = checked.contains(watch) + private val previewCallback: ViewPager2.OnPageChangeCallback = + object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + previewAdapter.apply { + if (position >= itemCount - 1 && hasMoreItems) { + hasMoreItems = false // don't make two requests + viewModel.loadMoreHomeScrollResponses() } } - toggleListHolder?.isGone = visible.isEmpty() + val item = previewAdapter.getItemOrNull(position) ?: return + onSelect(item, position) } - } ?: debugException { "Expected findViewTreeLifecycleOwner" } + } + + override fun onViewDetachedFromWindow() { + previewViewpager.unregisterOnPageChangeCallback(previewCallback) } private val toggleList = listOf>( @@ -457,6 +407,8 @@ class HomeParentItemAdapterPreview( private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder) + fun bind() = Unit + init { previewViewpager.setPageTransformer(HomeScrollTransformer()) @@ -563,7 +515,9 @@ class HomeParentItemAdapterPreview( when (preview) { is Resource.Success -> { - if (!previewAdapter.setItems( + previewAdapter.submitList(preview.value.second) + previewAdapter.hasMoreItems = preview.value.first + /*if (!.setItems( preview.value.second, preview.value.first ) @@ -575,15 +529,16 @@ class HomeParentItemAdapterPreview( previewViewpager.fakeDragBy(1f) previewViewpager.endFakeDrag() previewCallback.onPageSelected(0) - previewViewpager.isVisible = true - previewViewpagerText.isVisible = true - alternativeAccountPadding?.isVisible = false //previewHeader.isVisible = true - } + }*/ + + previewViewpager.isVisible = true + previewViewpagerText.isVisible = true + alternativeAccountPadding?.isVisible = false } else -> { - previewAdapter.setItems(listOf(), false) + previewAdapter.submitList(listOf()) previewViewpager.setCurrentItem(0, false) previewViewpager.isVisible = false previewViewpagerText.isVisible = false @@ -595,7 +550,7 @@ class HomeParentItemAdapterPreview( private fun updateResume(resumeWatching: List) { resumeHolder.isVisible = resumeWatching.isNotEmpty() - resumeAdapter.updateList(resumeWatching) + resumeAdapter.submitList(resumeWatching) if ( binding is FragmentHomeHeadBinding || @@ -625,7 +580,7 @@ class HomeParentItemAdapterPreview( private fun updateBookmarks(data: Pair>) { val (visible, list) = data bookmarkHolder.isVisible = visible - bookmarkAdapter.updateList(list) + bookmarkAdapter.submitList(list) if ( binding is FragmentHomeHeadBinding || @@ -655,5 +610,35 @@ class HomeParentItemAdapterPreview( } } } + + override fun onViewAttachedToWindow() { + previewViewpager.registerOnPageChangeCallback(previewCallback) + + binding.root.findViewTreeLifecycleOwner()?.apply { + observe(viewModel.preview) { + updatePreview(it) + } + if (binding is FragmentHomeHeadTvBinding) { + observe(viewModel.apiName) { name -> + binding.homePreviewChangeApi.text = name + } + } + observe(viewModel.resumeWatching) { + updateResume(it) + } + observe(viewModel.bookmarks) { + updateBookmarks(it) + } + observe(viewModel.availableWatchStatusTypes) { (checked, visible) -> + for ((chip, watch) in toggleList) { + chip.apply { + isVisible = visible.contains(watch) + isChecked = checked.contains(watch) + } + } + toggleListHolder?.isGone = visible.isEmpty() + } + } ?: debugException { "Expected findViewTreeLifecycleOwner" } + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt index f0542b77..29186e83 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt @@ -4,43 +4,23 @@ import android.content.res.Configuration import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.isGone -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import androidx.viewbinding.ViewBinding +import androidx.fragment.app.Fragment import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding +import com.lagradost.cloudstream3.ui.NoStateAdapter +import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.UIHelper.setImage -class HomeScrollAdapter : RecyclerView.Adapter() { - private var items: MutableList = mutableListOf() +class HomeScrollAdapter( + fragment: Fragment +) : NoStateAdapter(fragment) { var hasMoreItems: Boolean = false - fun getItem(position: Int): LoadResponse? { - return items.getOrNull(position) - } - - fun setItems(newItems: List, hasNext: Boolean): Boolean { - val isSame = newItems.firstOrNull()?.url == items.firstOrNull()?.url - hasMoreItems = hasNext - - val diffResult = DiffUtil.calculateDiff( - HomeScrollDiffCallback(this.items, newItems) - ) - - items.clear() - items.addAll(newItems) - - - diffResult.dispatchUpdatesTo(this) - - return isSame - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + override fun onCreateContent(parent: ViewGroup): ViewHolderState { val inflater = LayoutInflater.from(parent.context) val binding = if (isLayout(TV or EMULATOR)) { HomeScrollViewTvBinding.inflate(inflater, parent, false) @@ -48,70 +28,37 @@ class HomeScrollAdapter : RecyclerView.Adapter() { HomeScrollViewBinding.inflate(inflater, parent, false) } - return CardViewHolder( - binding, - //forceHorizontalPosters - ) + return ViewHolderState(binding) } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is CardViewHolder -> { - holder.bind(items[position]) + override fun onBindContent( + holder: ViewHolderState, + item: LoadResponse, + position: Int, + ) { + val binding = holder.view + val itemView = holder.itemView + val isHorizontal = + binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + + val posterUrl = + if (isHorizontal) item.backgroundPosterUrl ?: item.posterUrl else item.posterUrl + ?: item.backgroundPosterUrl + + when (binding) { + is HomeScrollViewBinding -> { + binding.homeScrollPreview.setImage(posterUrl) + binding.homeScrollPreviewTags.apply { + text = item.tags?.joinToString(" • ") ?: "" + isGone = item.tags.isNullOrEmpty() + maxLines = 2 + } + binding.homeScrollPreviewTitle.text = item.name + } + + is HomeScrollViewTvBinding -> { + binding.homeScrollPreview.setImage(posterUrl) } } } - - class CardViewHolder - constructor( - val binding: ViewBinding, - //private val forceHorizontalPosters: Boolean? = null - ) : - RecyclerView.ViewHolder(binding.root) { - - fun bind(card: LoadResponse) { - val isHorizontal = - binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - - val posterUrl = - if (isHorizontal) card.backgroundPosterUrl ?: card.posterUrl else card.posterUrl - ?: card.backgroundPosterUrl - - when (binding) { - is HomeScrollViewBinding -> { - binding.homeScrollPreview.setImage(posterUrl) - binding.homeScrollPreviewTags.apply { - text = card.tags?.joinToString(" • ") ?: "" - isGone = card.tags.isNullOrEmpty() - maxLines = 2 - } - binding.homeScrollPreviewTitle.text = card.name - } - - is HomeScrollViewTvBinding -> { - binding.homeScrollPreview.setImage(posterUrl) - } - } - } - } - - class HomeScrollDiffCallback( - private val oldList: List, - private val newList: List - ) : - DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition].url == newList[newItemPosition].url - - override fun getOldListSize() = oldList.size - - override fun getNewListSize() = newList.size - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition] == newList[newItemPosition] - } - - override fun getItemCount(): Int { - return items.size - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index b0d4bdf8..a2c7583f 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 @@ -53,6 +53,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.withContext import java.util.EnumSet +import java.util.concurrent.CopyOnWriteArrayList import kotlin.collections.set class HomeViewModel : ViewModel() { @@ -125,7 +126,7 @@ class HomeViewModel : ViewModel() { private val _resumeWatching = MutableLiveData>() private val _preview = MutableLiveData>>>() - private val previewResponses = mutableListOf() + private val previewResponses = CopyOnWriteArrayList() private val previewResponsesAdded = mutableSetOf() val resumeWatching: LiveData> = _resumeWatching @@ -327,7 +328,13 @@ class HomeViewModel : ViewModel() { val filteredList = context?.filterHomePageListByFilmQuality(list) ?: list expandable[list.name] = - ExpandableHomepageList(filteredList, 1, home.hasNext) + ExpandableHomepageList( + filteredList.copy( + list = CopyOnWriteArrayList( + filteredList.list + ) + ), 1, home.hasNext + ) } } @@ -342,8 +349,7 @@ class HomeViewModel : ViewModel() { val currentList = items.shuffled().filter { it.list.isNotEmpty() } .flatMap { it.list } - .distinctBy { it.url } - .toList() + .distinctBy { it.url }.toList() if (currentList.isNotEmpty()) { val randomItems = diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index 664946b1..90e57ef4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -49,12 +49,10 @@ import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA -import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity @@ -62,6 +60,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount +import java.util.concurrent.CopyOnWriteArrayList import kotlin.math.abs const val LIBRARY_FOLDER = "library_folder" @@ -165,7 +164,8 @@ class LibraryFragment : Fragment() { } // Set the color for the search exit icon to the correct theme text color - val searchExitIcon = binding?.mainSearch?.findViewById(androidx.appcompat.R.id.search_close_btn) + val searchExitIcon = + binding?.mainSearch?.findViewById(androidx.appcompat.R.id.search_close_btn) val searchExitIconColor = TypedValue() activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true) @@ -233,7 +233,7 @@ class LibraryFragment : Fragment() { if (listLibraryItems.isNotEmpty()) { val listLibraryItem = listLibraryItems.random() libraryViewModel.currentSyncApi?.syncIdName?.let { - loadLibraryItem(it, listLibraryItem.syncId,listLibraryItem) + loadLibraryItem(it, listLibraryItem.syncId, listLibraryItem) } } } @@ -312,44 +312,46 @@ class LibraryFragment : Fragment() { binding?.viewpager?.setPageTransformer(LibraryScrollTransformer()) - binding?.viewpager?.adapter = - binding?.viewpager?.adapter ?: ViewpagerAdapter( - mutableListOf(), - { isScrollingDown: Boolean -> - if (isScrollingDown) { - binding?.sortFab?.shrink() - binding?.libraryRandom?.shrink() - } else { - binding?.sortFab?.extend() - binding?.libraryRandom?.extend() - } - }) callback@{ searchClickCallback -> - // To prevent future accidents - debugAssert({ - searchClickCallback.card !is SyncAPI.LibraryItem - }, { - "searchClickCallback ${searchClickCallback.card} is not a LibraryItem" - }) + binding?.viewpager?.adapter = ViewpagerAdapter( + fragment = this, + { isScrollingDown: Boolean -> + if (isScrollingDown) { + binding?.sortFab?.shrink() + binding?.libraryRandom?.shrink() + } else { + binding?.sortFab?.extend() + binding?.libraryRandom?.extend() + } + }) callback@{ searchClickCallback -> + // To prevent future accidents + debugAssert({ + searchClickCallback.card !is SyncAPI.LibraryItem + }, { + "searchClickCallback ${searchClickCallback.card} is not a LibraryItem" + }) - val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId - val syncName = - libraryViewModel.currentSyncApi?.syncIdName ?: return@callback + val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId + val syncName = + libraryViewModel.currentSyncApi?.syncIdName ?: return@callback - when (searchClickCallback.action) { - SEARCH_ACTION_SHOW_METADATA -> { - (activity as? MainActivity)?.loadPopup(searchClickCallback.card, load = false) + when (searchClickCallback.action) { + SEARCH_ACTION_SHOW_METADATA -> { + (activity as? MainActivity)?.loadPopup( + searchClickCallback.card, + load = false + ) /*activity?.showPluginSelectionDialog( syncId, syncName, searchClickCallback.card.apiName )*/ - } + } - SEARCH_ACTION_LOAD -> { - loadLibraryItem(syncName, syncId, searchClickCallback.card) - } + SEARCH_ACTION_LOAD -> { + loadLibraryItem(syncName, syncId, searchClickCallback.card) } } + } binding?.apply { viewpager.offscreenPageLimit = 2 @@ -395,7 +397,11 @@ class LibraryFragment : Fragment() { } } - (viewpager.adapter as? ViewpagerAdapter)?.pages = pages + (viewpager.adapter as? ViewpagerAdapter)?.submitList(pages.map { + it.copy( + items = CopyOnWriteArrayList(it.items) + ) + }) //fix focus on the viewpager itself (viewpager.getChildAt(0) as RecyclerView).apply { tag = "tv_no_focus_tag" @@ -403,10 +409,10 @@ class LibraryFragment : Fragment() { } // Using notifyItemRangeChanged keeps the animations when sorting - viewpager.adapter?.notifyItemRangeChanged( + /*viewpager.adapter?.notifyItemRangeChanged( 0, viewpager.adapter?.itemCount ?: 0 - ) + )*/ libraryViewModel.currentPage.value?.let { page -> binding?.viewpager?.setCurrentItem(page, false) @@ -464,12 +470,14 @@ class LibraryFragment : Fragment() { } }.attach() - binding?.libraryTabLayout?.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener { + binding?.libraryTabLayout?.addOnTabSelectedListener(object : + TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab?) { binding?.libraryTabLayout?.selectedTabPosition?.let { page -> libraryViewModel.switchPage(page) } } + override fun onTabUnselected(tab: TabLayout.Tab?) = Unit override fun onTabReselected(tab: TabLayout.Tab?) = Unit }) @@ -569,8 +577,9 @@ class LibraryFragment : Fragment() { } + @SuppressLint("NotifyDataSetChanged") override fun onConfigurationChanged(newConfig: Configuration) { - (binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind() + binding?.viewpager?.adapter?.notifyDataSetChanged() super.onConfigurationChanged(newConfig) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index c983ea2f..1bd01c86 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -113,7 +113,7 @@ class LibraryViewModel : ViewModel() { } val desiredSortingMethod = - ListSorting.values().getOrNull(DataStoreHelper.librarySortingMode) + ListSorting.entries.getOrNull(DataStoreHelper.librarySortingMode) if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) { sort(desiredSortingMethod, null, pages) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt index c41ec681..cfd22220 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -1,105 +1,123 @@ package com.lagradost.cloudstream3.ui.library import android.os.Build +import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.doOnAttach -import androidx.recyclerview.widget.RecyclerView +import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView.OnFlingListener import com.google.android.material.appbar.AppBarLayout import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.ui.BaseAdapter +import com.lagradost.cloudstream3.ui.BaseDiffCallback +import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount +class ViewpagerAdapterViewHolderState(val binding: LibraryViewpagerPageBinding) : + ViewHolderState(binding) { + override fun save(): Bundle = + Bundle().apply { + putParcelable( + "pageRecyclerview", + binding.pageRecyclerview.layoutManager?.onSaveInstanceState() + ) + } + + override fun restore(state: Bundle) { + state.getParcelable("pageRecyclerview")?.let { recycle -> + binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle) + } + } +} + class ViewpagerAdapter( - var pages: List, + fragment: Fragment, val scrollCallback: (isScrollingDown: Boolean) -> Unit, val clickCallback: (SearchClickCallback) -> Unit -) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return PageViewHolder( +) : BaseAdapter(fragment, + id = "ViewpagerAdapter".hashCode(), + diffCallback = BaseDiffCallback( + itemSame = { a, b -> + a.title == b.title + }, + contentSame = { a, b -> + a.items == b.items && a.title == b.title + } +)) { + override fun onCreateContent(parent: ViewGroup): ViewHolderState { + return ViewpagerAdapterViewHolderState( LibraryViewpagerPageBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is PageViewHolder -> { - holder.bind(pages[position], position, unbound.remove(position)) - } - } + override fun onUpdateContent( + holder: ViewHolderState, + item: SyncAPI.Page, + position: Int + ) { + val binding = holder.view + if (binding !is LibraryViewpagerPageBinding) return + (binding.pageRecyclerview.adapter as? PageAdapter)?.updateList(item.items) } - private val unbound = mutableSetOf() + override fun onBindContent(holder: ViewHolderState, item: SyncAPI.Page, position: Int) { + val binding = holder.view + if (binding !is LibraryViewpagerPageBinding) return - /** - * Used to mark all pages for re-binding and forces all items to be refreshed - * Without this the pages will still use the same adapters - **/ - fun rebind() { - unbound.addAll(0..pages.size) - this.notifyItemRangeChanged(0, pages.size) - } - - inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind(page: SyncAPI.Page, position: Int, rebind: Boolean) { - binding.pageRecyclerview.tag = position - binding.pageRecyclerview.apply { - spanCount = - this@PageViewHolder.itemView.context.getSpanCount() ?: 3 - if (adapter == null || rebind) { - // Only add the items after it has been attached since the items rely on ItemWidth - // Which is only determined after the recyclerview is attached. - // If this fails then item height becomes 0 when there is only one item - doOnAttach { - adapter = PageAdapter( - page.items.toMutableList(), - this, - clickCallback - ) - } - } else { - (adapter as? PageAdapter)?.updateList(page.items) - scrollToPosition(0) + binding.pageRecyclerview.tag = position + binding.pageRecyclerview.apply { + spanCount = + binding.root.context.getSpanCount() ?: 3 + if (adapter == null) { // || rebind + // Only add the items after it has been attached since the items rely on ItemWidth + // Which is only determined after the recyclerview is attached. + // If this fails then item height becomes 0 when there is only one item + doOnAttach { + adapter = PageAdapter( + item.items.toMutableList(), + this, + clickCallback + ) } + } else { + (adapter as? PageAdapter)?.updateList(item.items) + // scrollToPosition(0) + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> - val diff = scrollY - oldScrollY + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> + val diff = scrollY - oldScrollY - //Expand the top Appbar based on scroll direction up/down, simulate phone behavior - if (isLayout(TV or EMULATOR)) { - binding.root.rootView.findViewById(R.id.search_bar) - .apply { - if (diff <= 0) - setExpanded(true) - else - setExpanded(false) - } - } - if (diff == 0) return@setOnScrollChangeListener - - scrollCallback.invoke(diff > 0) + //Expand the top Appbar based on scroll direction up/down, simulate phone behavior + if (isLayout(TV or EMULATOR)) { + binding.root.rootView.findViewById(R.id.search_bar) + .apply { + if (diff <= 0) + setExpanded(true) + else + setExpanded(false) + } } - } else { - onFlingListener = object : OnFlingListener() { - override fun onFling(velocityX: Int, velocityY: Int): Boolean { - scrollCallback.invoke(velocityY > 0) - return false - } + if (diff == 0) return@setOnScrollChangeListener + + scrollCallback.invoke(diff > 0) + } + } else { + onFlingListener = object : OnFlingListener() { + override fun onFling(velocityX: Int, velocityY: Int): Boolean { + scrollCallback.invoke(velocityY > 0) + return false } } } } } - - override fun getItemCount(): Int { - return pages.size - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index cfa6682d..0865b220 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -1,7 +1,10 @@ package com.lagradost.cloudstream3.ui.player import android.annotation.SuppressLint -import android.content.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.graphics.drawable.AnimatedImageDrawable import android.graphics.drawable.AnimatedVectorDrawable import android.media.metrics.PlaybackErrorEvent @@ -24,11 +27,7 @@ import androidx.fragment.app.Fragment import androidx.media3.common.PlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession -import androidx.media3.ui.AspectRatioFrameLayout -import androidx.media3.ui.DefaultTimeBar -import androidx.media3.ui.PlayerView -import androidx.media3.ui.SubtitleView -import androidx.media3.ui.TimeBar +import androidx.media3.ui.* import androidx.preference.PreferenceManager import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import com.github.rubensousa.previewseekbar.PreviewBar @@ -442,6 +441,9 @@ abstract class AbstractPlayerFragment( is VideoEndedEvent -> { context?.let { ctx -> + // Resets subtitle delay on ended video + player.setSubtitleOffset(0) + // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(ctx) ?.getBoolean( 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 210bfdca..31adbc87 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 @@ -1118,6 +1118,9 @@ class CS3IPlayer : IPlayer { } Player.STATE_ENDED -> { + // Resets subtitle delay on ended video + setSubtitleOffset(0) + // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(context) ?.getBoolean( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 6735a350..c357ce9c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -14,13 +14,7 @@ import android.os.Bundle import android.provider.Settings import android.text.Editable import android.text.format.DateUtils -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.Surface -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager +import android.view.* import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES import android.view.animation.AlphaAnimation import android.view.animation.Animation @@ -47,7 +41,9 @@ import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals -import com.lagradost.cloudstream3.ui.settings.SettingsFragment +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog @@ -78,7 +74,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private var isVerticalOrientation: Boolean = false protected open var lockRotation = true protected open var isFullScreenPlayer = true - protected open var isTv = false protected var playerBinding: PlayerCustomLayoutBinding? = null private var durationMode : Boolean by UserPreferenceDelegate("duration_mode", false) @@ -496,6 +491,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() { dialog.dismissSafe(activity) player.seekTime(1L) } + resetBtt.setOnClickListener { + subtitleDelay = 0 + dialog.dismissSafe(activity) + player.seekTime(1L) + } cancelBtt.setOnClickListener { subtitleDelay = beforeOffset dialog.dismissSafe(activity) @@ -1157,6 +1157,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } + KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_UP -> { if (!isShowing) { onClickChange() @@ -1205,7 +1206,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { // netflix capture back and hide ~monke KeyEvent.KEYCODE_BACK -> { - if (isShowing && isTv) { + if (isShowing && isLayout(TV or EMULATOR)) { onClickChange() return true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index af74cb57..c5de1a1c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -10,7 +10,8 @@ enum class LoadType { InAppDownload, ExternalApp, Browser, - Chromecast + Chromecast, + Fcast } fun LoadType.toSet() : Set { @@ -29,12 +30,17 @@ fun LoadType.toSet() : Set { ExtractorLinkType.VIDEO, ExtractorLinkType.M3U8 ) - LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet() + LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.entries.toSet() LoadType.Chromecast -> setOf( ExtractorLinkType.VIDEO, ExtractorLinkType.DASH, ExtractorLinkType.M3U8 ) + LoadType.Fcast -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt index 26cf9918..85e20d1c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -34,6 +34,9 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchViewModel +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.ownShow @@ -174,7 +177,7 @@ class QuickSearchFragment : Fragment() { } } else { binding?.quickSearchMasterRecycler?.adapter = - ParentItemAdapter(mutableListOf(), { callback -> + ParentItemAdapter(fragment = this, id = "quickSearchMasterRecycler".hashCode(), { callback -> SearchHelper.handleSearchClickCallback(callback) //when (callback.action) { //SEARCH_ACTION_LOAD -> { @@ -274,8 +277,13 @@ class QuickSearchFragment : Fragment() { // UIHelper.showInputMethod(view.findFocus()) // } //} - binding?.quickSearchBack?.setOnClickListener { - activity?.popCurrentPage() + if (isLayout(PHONE or EMULATOR)) { + binding?.quickSearchBack?.apply { + isVisible = true + setOnClickListener { + activity?.popCurrentPage() + } + } } if (isLayout(TV)) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index fad349c8..e4fd0559 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -9,9 +9,11 @@ import androidx.core.view.isVisible import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK import com.lagradost.cloudstream3.ui.download.DownloadClickEvent @@ -23,6 +25,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import java.text.DateFormat +import java.text.SimpleDateFormat import java.util.* const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 @@ -51,6 +55,8 @@ const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16 const val ACTION_PLAY_EPISODE_IN_MPV = 17 const val ACTION_MARK_AS_WATCHED = 18 +const val ACTION_FCAST = 19 + const val TV_EP_SIZE_LARGE = 400 const val TV_EP_SIZE_SMALL = 300 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) @@ -104,7 +110,7 @@ class EpisodeAdapter( override fun getItemViewType(position: Int): Int { val item = getItem(position) - return if (item.poster.isNullOrBlank()) 0 else 1 + return if (item.poster.isNullOrBlank() && item.description.isNullOrBlank()) 0 else 1 } @@ -260,6 +266,33 @@ class EpisodeAdapter( } } + if (card.airDate != null) { + val isUpcoming = unixTimeMS < card.airDate + + if (isUpcoming) { + episodePlayIcon.isVisible = false + episodeUpcomingIcon.isVisible = !episodePoster.isVisible + episodeDate.setText( + txt( + R.string.episode_upcoming_format, + secondsToReadable(card.airDate.minus(unixTimeMS).div(1000).toInt(), "") + ) + ) + } else { + episodeUpcomingIcon.isVisible = false + + val formattedAirDate = SimpleDateFormat.getDateInstance( + DateFormat.LONG, + Locale.getDefault() + ).apply { + }.format(Date(card.airDate)) + + episodeDate.setText(txt(formattedAirDate)) + } + } else { + episodeDate.isVisible = false + } + if (isLayout(EMULATOR or PHONE)) { episodePoster.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) @@ -271,6 +304,7 @@ class EpisodeAdapter( } } } + itemView.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index a1574eec..1d3f5a08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -50,6 +50,7 @@ data class ResultEpisode( val videoWatchState: VideoWatchState, /** Sum of all previous season episode counts + episode */ val totalEpisodeIndex: Int? = null, + val airDate: Long? = null, ) fun ResultEpisode.getRealPosition(): Long { @@ -85,6 +86,7 @@ fun buildResultEpisode( tvType: TvType, parentId: Int, totalEpisodeIndex: Int? = null, + airDate: Long? = null, ): ResultEpisode { val posDur = getViewPos(id) val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None @@ -107,7 +109,8 @@ fun buildResultEpisode( tvType, parentId, videoWatchState, - totalEpisodeIndex + totalEpisodeIndex, + airDate, ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index 8d0ca37b..fb5160a7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -30,7 +30,7 @@ import com.google.android.gms.cast.framework.CastState import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.updateHasTrailers -import com.lagradost.cloudstream3.CommonActivity +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent @@ -61,6 +61,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.openBrowser +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.openBatteryOptimizationSettings import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant @@ -442,8 +443,9 @@ open class ResultFragmentPhone : FullScreenPlayer() { val name = (viewModel.page.value as? Resource.Success)?.value?.title ?: txt(R.string.no_data).asStringNull(context) ?: "" - CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + showToast(txt(message, name), Toast.LENGTH_SHORT) } + context?.let { openBatteryOptimizationSettings(it) } } resultFavorite.setOnClickListener { viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? -> @@ -457,7 +459,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { val name = (viewModel.page.value as? Resource.Success)?.value?.title ?: txt(R.string.no_data).asStringNull(context) ?: "" - CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + showToast(txt(message, name), Toast.LENGTH_SHORT) } } mediaRouteButton.apply { @@ -465,7 +467,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { alpha = if (chromecastSupport) 1f else 0.3f if (!chromecastSupport) { setOnClickListener { - CommonActivity.showToast( + showToast( R.string.no_chromecast_support_toast, Toast.LENGTH_LONG ) @@ -640,6 +642,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { ), null ) { click -> + context?.let { openBatteryOptimizationSettings(it) } + when (click.action) { DOWNLOAD_ACTION_DOWNLOAD -> { viewModel.handleAction( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 3263ee93..13621cda 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -33,6 +33,7 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.GeneratorPlayer +import com.lagradost.cloudstream3.ui.player.NEXT_WATCH_EPISODE_PERCENTAGE import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent @@ -781,25 +782,31 @@ class ResultFragmentTv : Fragment() { // resultEpisodeLoading.isVisible = episodes is Resource.Loading if (episodes is Resource.Success) { - val first = episodes.value.firstOrNull() - if (first != null) { + + val lastWatchedIndex = episodes.value.indexOfLast { ep -> + ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f || ep.videoWatchState == VideoWatchState.Watched + } + + val firstUnwatched = episodes.value.getOrElse(lastWatchedIndex + 1) { episodes.value.firstOrNull() } + + if (firstUnwatched != null) { resultPlaySeriesText.text = when { - first.season != null -> - "${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}" - else -> "${getString(R.string.episode)} ${first.episode}" + firstUnwatched.season != null -> + "${getString(R.string.season_short)}${firstUnwatched.season}:${getString(R.string.episode_short)}${firstUnwatched.episode}" + else -> "${getString(R.string.episode)} ${firstUnwatched.episode}" } resultPlaySeriesButton.setOnClickListener { viewModel.handleAction( EpisodeClickEvent( ACTION_CLICK_DEFAULT, - first + firstUnwatched ) ) } resultPlaySeriesButton.setOnLongClickListener { viewModel.handleAction( - EpisodeClickEvent(ACTION_SHOW_OPTIONS, first) + EpisodeClickEvent(ACTION_SHOW_OPTIONS, firstUnwatched) ) return@setOnLongClickListener true } 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 c90e01d0..a32942f6 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 @@ -5,6 +5,7 @@ import android.content.* import android.net.Uri import android.os.Build import android.os.Bundle +import android.text.format.Formatter.formatFileSize import android.util.Log import android.widget.Toast import androidx.annotation.MainThread @@ -20,7 +21,6 @@ import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS -import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.getCastSession @@ -83,6 +83,10 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.fcast.FcastManager +import com.lagradost.cloudstream3.utils.fcast.FcastSession +import com.lagradost.cloudstream3.utils.fcast.Opcode +import com.lagradost.cloudstream3.utils.fcast.PlayMessage import kotlinx.coroutines.* import java.io.File import java.util.concurrent.TimeUnit @@ -197,7 +201,11 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { else -> null }?.also { - nextAiringEpisode = txt(R.string.next_episode_format, airing.episode) + nextAiringEpisode = when (airing.season) { + + null -> txt(R.string.next_episode_format, airing.episode) + else -> txt(R.string.next_season_episode_format, airing.season, airing.episode) + } } } } @@ -246,6 +254,9 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { TvType.Live -> R.string.live_singular TvType.Others -> R.string.other_singular TvType.NSFW -> R.string.nsfw_singular + TvType.Music -> R.string.music_singlar + TvType.AudioBook -> R.string.audio_book_singular + TvType.CustomMedia -> R.string.custom_media_singluar } ), yearText = txt(year?.toString()), @@ -627,6 +638,9 @@ class ResultViewModel2 : ViewModel() { TvType.Live -> "LiveStreams" TvType.NSFW -> "NSFW" TvType.Others -> "Others" + TvType.Music -> "Music" + TvType.AudioBook -> "AudioBooks" + TvType.CustomMedia -> "Media" } } @@ -1093,13 +1107,14 @@ class ResultViewModel2 : ViewModel() { val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse -> val librarySyncData = it.syncData + val yearCheck = year == it.year || year == null || it.year == null val checks = listOf( { imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId }, { tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId }, { malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId }, { aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId }, - { normalizedName == normalizeString(it.name) && year == it.year } + { normalizedName == normalizeString(it.name) && yearCheck } ) checks.any { it() } @@ -1274,9 +1289,14 @@ class ResultViewModel2 : ViewModel() { callback: (Pair) -> Unit, ) { loadLinks(result, isVisible = true, type) { links -> + // Could not find a better way to do this + val context = AcraApplication.context postPopup( text, - links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { + links.links.apmap { + val size = it.getVideoSize()?.let { size -> " " + formatFileSize(context, size) } ?: "" + txt("${it.name} ${Qualities.getStringByInt(it.quality)}$size") + }) { callback.invoke(links to (it ?: return@postPopup)) } } @@ -1503,6 +1523,13 @@ class ResultViewModel2 : ViewModel() { ) ) } + + if (FcastManager.currentDevices.isNotEmpty()) { + options.add( + txt(R.string.player_settings_play_in_fcast) to ACTION_FCAST + ) + } + options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER) for (app in apps) { @@ -1678,6 +1705,39 @@ class ResultViewModel2 : ViewModel() { } } + ACTION_FCAST -> { + val devices = FcastManager.currentDevices.toList() + postPopup( + txt(R.string.player_settings_select_cast_device), + devices.map { txt(it.name) }) { index -> + if (index == null) return@postPopup + val device = devices.getOrNull(index) + + acquireSingleLink( + click.data, + LoadType.Fcast, + txt(R.string.episode_action_cast_mirror) + ) { (result, index) -> + val host = device?.host ?: return@acquireSingleLink + val link = result.links.firstOrNull() ?: return@acquireSingleLink + + FcastSession(host).use { session -> + session.sendMessage( + Opcode.Play, + PlayMessage( + link.type.getMimeType(), + link.url, + headers = mapOf( + "referer" to link.referer, + "user-agent" to USER_AGENT + ) + link.headers + ) + ) + } + } + } + } + ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( click.data, LoadType.Browser, @@ -1759,20 +1819,28 @@ class ResultViewModel2 : ViewModel() { val data = currentResponse?.syncData?.toList() ?: emptyList() val list = HashMap().apply { putAll(data) } - - activity?.navigate( - R.id.global_to_navigation_player, - GeneratorPlayer.newInstance( - generator?.also { - it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work - ?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id } - ?.let { index -> - if (index >= 0) - it.goto(index) - } - } ?: return, list + generator?.also { + it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work + ?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id } + ?.let { index -> + if (index >= 0) + it.goto(index) + } + } + if (currentResponse?.type == TvType.CustomMedia) { + generator?.generateLinks( + clearCache = true, + LoadType.Unknown, + callback = {}, + subtitleCallback = {}) + } else { + activity?.navigate( + R.id.global_to_navigation_player, + GeneratorPlayer.newInstance( + generator ?: return, list + ) ) - ) + } } ACTION_MARK_AS_WATCHED -> { @@ -2258,7 +2326,8 @@ class ResultViewModel2 : ViewModel() { fillers.getOrDefault(episode, false), loadResponse.type, mainId, - totalIndex + totalIndex, + airDate = i.date ) val season = eps.seasonIndex ?: 0 @@ -2307,7 +2376,8 @@ class ResultViewModel2 : ViewModel() { null, loadResponse.type, mainId, - totalIndex + totalIndex, + airDate = episode.date ) val season = ep.seasonIndex ?: 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index 24d56897..0e8160db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -19,6 +19,13 @@ sealed class UiText { data class DynamicString(val value: String) : UiText() { override fun toString(): String = value + + override fun equals(other: Any?): Boolean { + if (other !is DynamicString) return false + return this.value == other.value + } + + override fun hashCode(): Int = value.hashCode() } class StringResource( @@ -27,6 +34,16 @@ sealed class UiText { ) : UiText() { override fun toString(): String = "resId = $resId\nargs = ${args.toList().map { "(${it::class} = $it)" }}" + override fun equals(other: Any?): Boolean { + if (other !is StringResource) return false + return this.resId == other.resId && this.args == other.args + } + + override fun hashCode(): Int { + var result = resId + result = 31 * result + args.hashCode() + return result + } } fun asStringNull(context: Context?): String? { 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 4b4700ce..24e87d30 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 @@ -46,6 +46,7 @@ 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.BaseAdapter 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 @@ -161,7 +162,8 @@ class SearchFragment : Fragment() { **/ fun search(query: String?) { if (query == null) return - + // don't resume state from prev search + (binding?.searchMasterRecycler?.adapter as? BaseAdapter<*,*>)?.clear() context?.let { ctx -> val default = enumValues().sorted().filter { it != TvType.NSFW } .map { it.ordinal.toString() }.toSet() @@ -506,8 +508,8 @@ class SearchFragment : Fragment() { }*/ //main_search.onActionViewExpanded()*/ - val masterAdapter: RecyclerView.Adapter = - ParentItemAdapter(mutableListOf(), { callback -> + val masterAdapter = + ParentItemAdapter(fragment = this, id = "masterAdapter".hashCode(), { callback -> SearchHelper.handleSearchClickCallback(callback) }, { item -> bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 298431ee..f0d402da 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -12,6 +12,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager +import androidx.preference.SwitchPreferenceCompat import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent @@ -30,6 +31,7 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref @@ -38,13 +40,20 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setTool import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.BackupUtils +import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage -class SettingsAccount : PreferenceFragmentCompat() { +class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback { companion object { /** Used by nginx plugin too */ fun showLoginInfo( @@ -252,6 +261,31 @@ class SettingsAccount : PreferenceFragmentCompat() { } } + private fun updateAuthPreference(enabled: Boolean) { + val biometricKey = getString(R.string.biometric_key) + + PreferenceManager.getDefaultSharedPreferences(context ?: return).edit() + .putBoolean(biometricKey, enabled).apply() + findPreference(biometricKey)?.isChecked = enabled + } + + override fun onAuthenticationError() { + updateAuthPreference(!isAuthEnabled(context ?: return)) + } + + override fun onAuthenticationSuccess() { + if (isAuthEnabled(context?: return)) { + updateAuthPreference(true) + BackupUtils.backup(activity) + activity?.showBottomDialogText( + getString(R.string.biometric_setting), + getString(R.string.biometric_warning).html() + ) { onDialogDismissedEvent } + } else { + updateAuthPreference(false) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setUpToolbar(R.string.category_account) @@ -263,22 +297,25 @@ class SettingsAccount : PreferenceFragmentCompat() { hideKeyboard() setPreferencesFromResource(R.xml.settings_account, rootKey) - getPref(R.string.biometric_key)?.setOnPreferenceClickListener { - val authEnabled = PreferenceManager.getDefaultSharedPreferences( - context ?: return@setOnPreferenceClickListener false - ) - .getBoolean(getString(R.string.biometric_key), false) + // hide preference on tvs and emulators + getPref(R.string.biometric_key)?.isEnabled = isLayout(PHONE) - if (authEnabled) { - BackupUtils.backup(activity) - val title = activity?.getString(R.string.biometric_setting) - val warning = activity?.getString(R.string.biometric_warning) - activity?.showBottomDialogText( - title as String, - warning.html() - ) { onDialogDismissedEvent } + getPref(R.string.biometric_key)?.setOnPreferenceClickListener { + val ctx = context ?: return@setOnPreferenceClickListener false + + if (deviceHasPasswordPinLock(ctx)) { + startBiometricAuthentication( + activity?: return@setOnPreferenceClickListener false, + R.string.biometric_authentication_title, + false + ) + promptInfo?.let { + authCallback = this + biometricPrompt?.authenticate(it) + } } - true + + false } val syncApis = diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index caff5df6..8ac17928 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -1,34 +1,43 @@ package com.lagradost.cloudstream3.ui.settings import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.annotation.StringRes import androidx.core.view.children -import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar +import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import java.io.File +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone class SettingsFragment : Fragment() { companion object { @@ -76,9 +85,11 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } } UIHelper.fixPaddingStatusbar(settingsToolbar) @@ -90,10 +101,12 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } } UIHelper.fixPaddingStatusbar(settingsToolbar) @@ -127,7 +140,6 @@ class SettingsFragment : Fragment() { val localBinding = MainSettingsBinding.inflate(inflater, container, false) binding = localBinding return localBinding.root - //return inflater.inflate(R.layout.main_settings, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -135,21 +147,44 @@ class SettingsFragment : Fragment() { activity?.navigate(id, Bundle()) } - // used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}") + /** used to debug leaks + showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : + ${VideoDownloadManager.downloadProgressEvent.size}") **/ - for (syncApi in accountManagers) { - val login = syncApi.loginInfo() - val pic = login?.profilePicture ?: continue - if (binding?.settingsProfilePic?.setImage( - pic, - errorImageDrawable = HomeFragment.errorProfilePic - ) == true - ) { - binding?.settingsProfileText?.text = login.name - binding?.settingsProfile?.isVisible = true - break + fun hasProfilePictureFromAccountManagers(accountManagers: List): Boolean { + for (syncApi in accountManagers) { + val login = syncApi.loginInfo() + val pic = login?.profilePicture ?: continue + + if (binding?.settingsProfilePic?.setImage( + pic, + errorImageDrawable = HomeFragment.errorProfilePic + ) == true + ) { + binding?.settingsProfileText?.text = login.name + return true // sync profile exists + } } + return false // not syncing } + + // display local account information if not syncing + if (!hasProfilePictureFromAccountManagers(accountManagers)) { + val activity = activity ?: return + val currentAccount = try { + DataStoreHelper.accounts.firstOrNull { + it.keyIndex == DataStoreHelper.selectedKeyIndex + } ?: activity.let { DataStoreHelper.getDefaultAccount(activity) } + + } catch (t: IllegalStateException) { + Log.e("AccountManager", "Activity not found", t) + null + } + + binding?.settingsProfilePic?.setImage(currentAccount?.image) + binding?.settingsProfileText?.text = currentAccount?.name + } + binding?.apply { listOf( settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general, @@ -179,9 +214,14 @@ class SettingsFragment : Fragment() { val appVersion = getString(R.string.app_version) val commitInfo = getString(R.string.commit_hash) + val buildTimestamp = SimpleDateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, + Locale.getDefault() + ).apply { timeZone = TimeZone.getTimeZone("UTC") + }.format(Date(BuildConfig.BUILD_DATE)).replace("UTC", "") - binding?.appVersionInfo?.setOnLongClickListener{ - clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo") + binding?.buildDate?.text = buildTimestamp + binding?.appVersionInfo?.setOnLongClickListener { + clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo $buildTimestamp") true } } 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 6cf00375..ff891c43 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 @@ -27,11 +27,15 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.ui.EasterEggMonke +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.isAppRestricted +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.showBatteryOptimizationDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog @@ -45,7 +49,6 @@ import com.lagradost.safefile.SafeFile // Change local language settings in the app. fun getCurrentLocale(context: Context): String { - // val dm = res.displayMetrics val res = context.resources val conf = res.configuration @@ -95,6 +98,7 @@ val appLanguages = arrayListOf( Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), Triple("", "bahasa Melayu", "ms"), + Triple("", "Malti", "mt"), Triple("", "ဗမာစာ", "my"), Triple("", "नेपाली", "ne"), Triple("", "Nederlands", "nl"), @@ -204,6 +208,20 @@ class SettingsGeneral : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + // disable preference on tvs and emulators + getPref(R.string.battery_optimisation_key)?.isEnabled = isLayout(PHONE) + getPref(R.string.battery_optimisation_key)?.setOnPreferenceClickListener { + val ctx = context ?: return@setOnPreferenceClickListener false + + if (isAppRestricted(ctx)) { + showBatteryOptimizationDialog(ctx) + } else { + showToast(R.string.app_unrestricted_toast) + } + + true + } + fun showAdd() { val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } } activity?.showDialog( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index fb24c185..4aaa5e12 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -35,6 +35,9 @@ import okhttp3.internal.closeQuietly import java.io.BufferedReader import java.io.InputStreamReader import java.io.OutputStream +import java.lang.System.currentTimeMillis +import java.text.SimpleDateFormat +import java.util.* class SettingsUpdates : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -125,12 +128,12 @@ class SettingsUpdates : PreferenceFragmentCompat() { } binding.saveBtt.setOnClickListener { + val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) var fileStream: OutputStream? = null try { - fileStream = - VideoDownloadManager.setupStream( + fileStream = VideoDownloadManager.setupStream( it.context, - "logcat", + "logcat_${date}", null, "txt", false diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt index 98803818..d8fa46e6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt @@ -9,6 +9,7 @@ import android.widget.ArrayAdapter import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentSetupLayoutBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall @@ -86,6 +87,7 @@ class SetupFragmentLayout : Fragment() { nextBtt.setOnClickListener { + setKey(HAS_DONE_SETUP_KEY, true) findNavController().navigate(R.id.navigation_home) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index 87d17a2b..279a0cb5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -40,7 +40,7 @@ import java.io.OutputStream import java.io.PrintWriter import java.lang.System.currentTimeMillis import java.text.SimpleDateFormat -import java.util.* +import java.util.Date object BackupUtils { @@ -68,7 +68,8 @@ object BackupUtils { DOWNLOAD_EPISODE_CACHE, "biometric_key", // can lock down users if backup is shared on a incompatible device - "nginx_user" // Nginx user key + "nginx_user", // Nginx user key + "download_path_key" // No access rights after restore data from backup ) /** false if key should not be contained in backup */ diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt index de9b9963..c57600ee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt @@ -12,20 +12,20 @@ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.getString import androidx.fragment.app.FragmentActivity +import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R object BiometricAuthenticator { + const val TAG = "cs3Auth" private const val MAX_FAILED_ATTEMPTS = 3 private var failedAttempts = 0 - const val TAG = "cs3Auth" - private var biometricManager: BiometricManager? = null var biometricPrompt: BiometricPrompt? = null var promptInfo: BiometricPrompt.PromptInfo? = null - var authCallback: BiometricAuthCallback? = null // listen to authentication success private fun initializeBiometrics(activity: Activity) { @@ -37,20 +37,12 @@ object BiometricAuthenticator { activity as FragmentActivity, executor, object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) showToast("$errString") Log.e(TAG, "$errorCode") - failedAttempts++ - - if (failedAttempts >= MAX_FAILED_ATTEMPTS) { - failedAttempts = 0 - activity.finish() - } else { - failedAttempts = 0 - activity.finish() - } + authCallback?.onAuthenticationError() + //activity.finish() } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { @@ -89,7 +81,6 @@ object BiometricAuthenticator { .setDescription(description) .setAllowedAuthenticators(authFlag) .build() - } else { // for apis < 30 promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -98,7 +89,6 @@ object BiometricAuthenticator { .setDeviceCredentialAllowed(true) .build() } - } else { // fallback for A12+ when both fingerprint & Face unlock is absent but PIN is set promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -114,7 +104,6 @@ object BiometricAuthenticator { var result = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - when (biometricManager?.canAuthenticate( DEVICE_CREDENTIAL or BIOMETRIC_STRONG or BIOMETRIC_WEAK )) { @@ -126,7 +115,6 @@ object BiometricAuthenticator { BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> result = true BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> result = false } - } else { @Suppress("DEPRECATION") when (biometricManager?.canAuthenticate()) { @@ -153,12 +141,11 @@ object BiometricAuthenticator { // function to start authentication in any fragment or activity fun startBiometricAuthentication(activity: Activity, title: Int, setDeviceCred: Boolean) { initializeBiometrics(activity) - + authCallback = activity as? BiometricAuthCallback if (isBiometricHardWareAvailable()) { authCallback = activity as? BiometricAuthCallback authenticationDialog(activity, title, setDeviceCred) promptInfo?.let { biometricPrompt?.authenticate(it) } - } else { if (deviceHasPasswordPinLock(activity)) { authCallback = activity as? BiometricAuthCallback @@ -171,7 +158,15 @@ object BiometricAuthenticator { } } + fun isAuthEnabled(ctx: Context):Boolean { + return ctx.let { + PreferenceManager.getDefaultSharedPreferences(ctx) + .getBoolean(getString(ctx, R.string.biometric_key), false) + } + } + interface BiometricAuthCallback { fun onAuthenticationSuccess() + fun onAuthenticationError() } } 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 637f65b9..61cdd26a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -53,6 +53,7 @@ import com.lagradost.cloudstream3.extractors.FileMoonIn import com.lagradost.cloudstream3.extractors.FileMoonSx import com.lagradost.cloudstream3.extractors.Filesim import com.lagradost.cloudstream3.extractors.Fplayer +import com.lagradost.cloudstream3.extractors.Geodailymotion import com.lagradost.cloudstream3.extractors.GMPlayer import com.lagradost.cloudstream3.extractors.Gdriveplayer import com.lagradost.cloudstream3.extractors.Gdriveplayerapi @@ -83,6 +84,7 @@ import com.lagradost.cloudstream3.extractors.Maxstream import com.lagradost.cloudstream3.extractors.Mcloud import com.lagradost.cloudstream3.extractors.Megacloud import com.lagradost.cloudstream3.extractors.Meownime +import com.lagradost.cloudstream3.extractors.MetaGnathTuggers import com.lagradost.cloudstream3.extractors.Minoplres import com.lagradost.cloudstream3.extractors.MixDrop import com.lagradost.cloudstream3.extractors.MixDropBz @@ -139,6 +141,7 @@ import com.lagradost.cloudstream3.extractors.Sbspeed import com.lagradost.cloudstream3.extractors.Sbthe import com.lagradost.cloudstream3.extractors.Sendvid import com.lagradost.cloudstream3.extractors.ShaveTape +import com.lagradost.cloudstream3.extractors.Simpulumlamerop import com.lagradost.cloudstream3.extractors.Solidfiles import com.lagradost.cloudstream3.extractors.Ssbstream import com.lagradost.cloudstream3.extractors.StreamM4u @@ -175,6 +178,7 @@ import com.lagradost.cloudstream3.extractors.UpstreamExtractor import com.lagradost.cloudstream3.extractors.Uqload import com.lagradost.cloudstream3.extractors.Uqload1 import com.lagradost.cloudstream3.extractors.Uqload2 +import com.lagradost.cloudstream3.extractors.Urochsunloath import com.lagradost.cloudstream3.extractors.Userload import com.lagradost.cloudstream3.extractors.Userscloud import com.lagradost.cloudstream3.extractors.Uservideo @@ -182,10 +186,12 @@ import com.lagradost.cloudstream3.extractors.Vanfem import com.lagradost.cloudstream3.extractors.Vicloud import com.lagradost.cloudstream3.extractors.VidSrcExtractor import com.lagradost.cloudstream3.extractors.VidSrcExtractor2 +import com.lagradost.cloudstream3.extractors.VidSrcTo import com.lagradost.cloudstream3.extractors.VideoVard import com.lagradost.cloudstream3.extractors.VideovardSX import com.lagradost.cloudstream3.extractors.Vidgomunime import com.lagradost.cloudstream3.extractors.Vidgomunimesb +import com.lagradost.cloudstream3.extractors.Vidguardto import com.lagradost.cloudstream3.extractors.VidhideExtractor import com.lagradost.cloudstream3.extractors.Vidmoly import com.lagradost.cloudstream3.extractors.Vidmolyme @@ -207,6 +213,7 @@ import com.lagradost.cloudstream3.extractors.Watchx import com.lagradost.cloudstream3.extractors.WcoStream import com.lagradost.cloudstream3.extractors.Wibufile import com.lagradost.cloudstream3.extractors.XStreamCdn +import com.lagradost.cloudstream3.extractors.Yipsu import com.lagradost.cloudstream3.extractors.YourUpload import com.lagradost.cloudstream3.extractors.YoutubeExtractor import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor @@ -217,6 +224,8 @@ import com.lagradost.cloudstream3.extractors.Zorofile import com.lagradost.cloudstream3.extractors.Zplayer import com.lagradost.cloudstream3.extractors.ZplayerV2 import com.lagradost.cloudstream3.extractors.Ztreamhub +import com.lagradost.cloudstream3.extractors.EPlayExtractor +import com.lagradost.cloudstream3.extractors.Vtbe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay @@ -299,7 +308,18 @@ enum class ExtractorLinkType { /** No support at the moment */ TORRENT, /** No support at the moment */ - MAGNET, + MAGNET; + + // See https://www.iana.org/assignments/media-types/media-types.xhtml + fun getMimeType(): String { + return when (this) { + VIDEO -> "video/mp4" + M3U8 -> "application/x-mpegURL" + DASH -> "application/dash+xml" + TORRENT -> "application/x-bittorrent" + MAGNET -> "application/x-bittorrent" + } + } } private fun inferTypeFromUrl(url: String): ExtractorLinkType { @@ -402,9 +422,29 @@ open class ExtractorLink constructor( open val extractorData: String? = null, open val type: ExtractorLinkType, ) : VideoDownloadManager.IDownloadableMinimum { - val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8 - val isDash : Boolean get() = type == ExtractorLinkType.DASH - + val isM3u8: Boolean get() = type == ExtractorLinkType.M3U8 + val isDash: Boolean get() = type == ExtractorLinkType.DASH + + // Cached video size + private var videoSize: Long? = null + + /** + * Get video size in bytes with one head request. Only available for ExtractorLinkType.Video + * @param timeoutSeconds timeout of the head request. + */ + suspend fun getVideoSize(timeoutSeconds: Long = 3L): Long? { + // Content-Length is not applicable to other types of formats + if (this.type != ExtractorLinkType.VIDEO) return null + + videoSize = videoSize ?: runCatching { + val response = + app.head(this.url, headers = headers, referer = referer, timeout = timeoutSeconds) + response.headers["Content-Length"]?.toLong() + }.getOrNull() + + return videoSize + } + @JsonIgnore fun getAllHeaders() : Map { if (referer.isBlank()) { @@ -849,6 +889,7 @@ val extractorApis: MutableList = arrayListOf( Streamlare(), VidSrcExtractor(), VidSrcExtractor2(), + VidSrcTo(), PlayLtXyz(), AStreamHub(), Vidplay(), @@ -864,7 +905,16 @@ val extractorApis: MutableList = arrayListOf( Megacloud(), VidhideExtractor(), StreamWishExtractor(), - EmturbovidExtractor() + EmturbovidExtractor(), + Vtbe(), + EPlayExtractor(), + Vidguardto(), + Simpulumlamerop(), + Urochsunloath(), + Yipsu(), + MetaGnathTuggers(), + Geodailymotion(), + ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt index 153dbd3e..d9f0b382 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt @@ -50,7 +50,7 @@ class JsUnpacker(packedJS: String?) { throw Exception("Unknown p.a.c.k.e.r. encoding") } val unbase = Unbase(radix) - p = Pattern.compile("\\b\\w+\\b") + p = Pattern.compile("""\b[a-zA-Z0-9_]+\b""") m = p.matcher(payload) val decoded = StringBuilder(payload) var replaceOffset = 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt new file mode 100644 index 00000000..27609730 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt @@ -0,0 +1,86 @@ +package com.lagradost.cloudstream3.utils + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.PowerManager +import android.provider.Settings +import android.util.Log +import androidx.appcompat.app.AlertDialog +import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout + +const val packageName = BuildConfig.APPLICATION_ID +const val TAG = "PowerManagerAPI" + +object BatteryOptimizationChecker { + + fun isAppRestricted(context: Context?): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context != null) { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + return !powerManager.isIgnoringBatteryOptimizations(context.packageName) + } + + return false // below Marshmallow, it's always unrestricted when app is in background + } + + fun openBatteryOptimizationSettings(context: Context) { + if (shouldShowBatteryOptimizationDialog(context)) { + showBatteryOptimizationDialog(context) + } + } + + fun showBatteryOptimizationDialog(context: Context) { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) + + try { + context.let { + AlertDialog.Builder(it) + .setTitle(R.string.battery_dialog_title) + .setIcon(R.drawable.ic_battery) + .setMessage(R.string.battery_dialog_message) + .setPositiveButton(R.string.ok) { _, _ -> + intentOpenAppInfo(it) + } + .setNegativeButton(R.string.cancel) { _, _ -> + settingsManager.edit() + .putBoolean(context.getString(R.string.battery_optimisation_key), false) + .apply() + } + .show() + } + } catch (t: Throwable) { + Log.e(TAG, "Error showing battery optimization dialog", t) + } + } + + private fun shouldShowBatteryOptimizationDialog(context: Context): Boolean { + val isRestricted = isAppRestricted(context) + val isOptimizedNotShown = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.battery_optimisation_key), true) + return isRestricted && isOptimizedNotShown && isLayout(PHONE) + } + + private fun intentOpenAppInfo(context: Context) { + val intent = Intent() + try { + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", packageName, null)) + context.startActivity(intent, Bundle()) + } catch (t: Throwable) { + Log.e(TAG, "Unable to invoke any intent", t) + if (t is ActivityNotFoundException) { + showToast("Exception: Activity Not Found") + } else { + showToast(R.string.app_info_intent_error) + } + } + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index eedb626a..cb527020 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -45,6 +45,7 @@ import androidx.core.view.marginBottom import androidx.core.view.marginLeft import androidx.core.view.marginRight import androidx.core.view.marginTop +import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.NavHostFragment @@ -58,6 +59,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions.bitmapTransform import com.bumptech.glide.request.target.Target +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup @@ -208,6 +210,14 @@ object UIHelper { } } + fun View?.setAppBarNoScrollFlagsOnTV() { + if (isLayout(Globals.TV or EMULATOR)) { + this?.updateLayoutParams { + scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL + } + } + } + fun Activity.hideKeyboard() { window?.decorView?.clearFocus() this.findViewById(android.R.id.content)?.rootView?.let { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 50a8df02..7d4d5d98 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -187,7 +187,7 @@ object VideoDownloadManager { private val DOWNLOAD_BAD_CONFIG = DownloadStatus(retrySame = false, tryNext = false, success = false) - private const val KEY_RESUME_PACKAGES = "download_resume" + const val KEY_RESUME_PACKAGES = "download_resume" const val KEY_DOWNLOAD_INFO = "download_info" private const val KEY_RESUME_QUEUE_PACKAGES = "download_q_resume" diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt new file mode 100644 index 00000000..9ff5cc08 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt @@ -0,0 +1,135 @@ +package com.lagradost.cloudstream3.utils.fcast + +import android.content.Context +import android.net.nsd.NsdManager +import android.net.nsd.NsdManager.ResolveListener +import android.net.nsd.NsdServiceInfo +import android.os.Build +import android.util.Log +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe + +class FcastManager { + private var nsdManager: NsdManager? = null + + // Used for receiver + private val registrationListenerTcp = DefaultRegistrationListener() + private fun getDeviceName(): String { + return "${Build.MANUFACTURER}-${Build.MODEL}" + } + + /** + * Start the fcast service + * @param registerReceiver If true will register the app as a compatible fcast receiver for discovery in other app + */ + fun init(context: Context, registerReceiver: Boolean) = ioSafe { + nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager + val serviceType = "_fcast._tcp" + + if (registerReceiver) { + val serviceName = "$APP_PREFIX-${getDeviceName()}" + + val serviceInfo = NsdServiceInfo().apply { + this.serviceName = serviceName + this.serviceType = serviceType + this.port = TCP_PORT + } + + nsdManager?.registerService( + serviceInfo, + NsdManager.PROTOCOL_DNS_SD, + registrationListenerTcp + ) + } + + nsdManager?.discoverServices( + serviceType, + NsdManager.PROTOCOL_DNS_SD, + DefaultDiscoveryListener() + ) + } + + fun stop() { + nsdManager?.unregisterService(registrationListenerTcp) + } + + inner class DefaultDiscoveryListener : NsdManager.DiscoveryListener { + val tag = "DiscoveryListener" + override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) { + Log.d(tag, "Discovery failed: $serviceType, error code: $errorCode") + } + + override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) { + Log.d(tag, "Stop discovery failed: $serviceType, error code: $errorCode") + } + + override fun onDiscoveryStarted(serviceType: String?) { + Log.d(tag, "Discovery started: $serviceType") + } + + override fun onDiscoveryStopped(serviceType: String?) { + Log.d(tag, "Discovery stopped: $serviceType") + } + + override fun onServiceFound(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + nsdManager?.resolveService(serviceInfo, object : ResolveListener { + override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) { + } + + override fun onServiceResolved(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + + currentDevices.add(PublicDeviceInfo(serviceInfo)) + + Log.d( + tag, + "Service found: ${serviceInfo.serviceName}, Net: ${serviceInfo.host.hostAddress}" + ) + } + }) + } + + override fun onServiceLost(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + + // May remove duplicates, but net and port is null here, preventing device specific identification + currentDevices.removeAll { + it.rawName == serviceInfo.serviceName + } + + Log.d(tag, "Service lost: ${serviceInfo.serviceName}") + } + } + + companion object { + const val APP_PREFIX = "CloudStream" + val currentDevices: MutableList = mutableListOf() + + class DefaultRegistrationListener : NsdManager.RegistrationListener { + val tag = "DiscoveryService" + override fun onServiceRegistered(serviceInfo: NsdServiceInfo) { + Log.d(tag, "Service registered: ${serviceInfo.serviceName}") + } + + override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.e(tag, "Service registration failed: errorCode=$errorCode") + } + + override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) { + Log.d(tag, "Service unregistered: ${serviceInfo.serviceName}") + } + + override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.e(tag, "Service unregistration failed: errorCode=$errorCode") + } + } + + const val TCP_PORT = 46899 + } +} + +class PublicDeviceInfo(serviceInfo: NsdServiceInfo) { + val rawName: String = serviceInfo.serviceName + val host: String? = serviceInfo.host.hostAddress + val name = rawName.replace("-", " ") + host?.let { " $it" } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt new file mode 100644 index 00000000..1f33bca4 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt @@ -0,0 +1,60 @@ +package com.lagradost.cloudstream3.utils.fcast + +import android.util.Log +import androidx.annotation.WorkerThread +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.safefile.closeQuietly +import java.io.DataOutputStream +import java.net.Socket +import kotlin.jvm.Throws + +class FcastSession(private val hostAddress: String): AutoCloseable { + val tag = "FcastSession" + + private var socket: Socket? = null + @Throws + @WorkerThread + fun open(): Socket { + val socket = Socket(hostAddress, FcastManager.TCP_PORT) + this.socket = socket + return socket + } + + override fun close() { + socket?.closeQuietly() + socket = null + } + + @Throws + private fun acquireSocket(): Socket { + return socket ?: open() + } + + fun ping() { + sendMessage(Opcode.Ping, null) + } + + fun sendMessage(opcode: Opcode, message: T) { + ioSafe { + val socket = acquireSocket() + val outputStream = DataOutputStream(socket.getOutputStream()) + + val json = message?.toJson() + val content = json?.toByteArray() ?: ByteArray(0) + + // Little endian starting from 1 + // https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 + val size = content.size + 1 + + val sizeArray = ByteArray(4) { num -> + (size shr 8 * num and 0xff).toByte() + } + + Log.d(tag, "Sending message with size: $size, opcode: $opcode") + outputStream.write(sizeArray) + outputStream.write(ByteArray(1) { opcode.value }) + outputStream.write(content) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt new file mode 100644 index 00000000..61c00d6e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt @@ -0,0 +1,62 @@ +package com.lagradost.cloudstream3.utils.fcast + +// See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 +enum class Opcode(val value: Byte) { + None(0), + Play(1), + Pause(2), + Resume(3), + Stop(4), + Seek(5), + PlaybackUpdate(6), + VolumeUpdate(7), + SetVolume(8), + PlaybackError(9), + SetSpeed(10), + Version(11), + Ping(12), + Pong(13); +} + + +data class PlayMessage( + val container: String, + val url: String? = null, + val content: String? = null, + val time: Double? = null, + val speed: Double? = null, + val headers: Map? = null +) + +data class SeekMessage( + val time: Double +) + +data class PlaybackUpdateMessage( + val generationTime: Long, + val time: Double, + val duration: Double, + val state: Int, + val speed: Double +) + +data class VolumeUpdateMessage( + val generationTime: Long, + val volume: Double +) + +data class PlaybackErrorMessage( + val message: String +) + +data class SetSpeedMessage( + val speed: Double +) + +data class SetVolumeMessage( + val volume: Double +) + +data class VersionMessage( + val version: Long +) diff --git a/app/src/main/res/drawable/hourglass_24.xml b/app/src/main/res/drawable/hourglass_24.xml new file mode 100644 index 00000000..7bd1ebbd --- /dev/null +++ b/app/src/main/res/drawable/hourglass_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_battery.xml b/app/src/main/res/drawable/ic_battery.xml new file mode 100644 index 00000000..24d0a77f --- /dev/null +++ b/app/src/main/res/drawable/ic_battery.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_outline.xml b/app/src/main/res/drawable/rounded_outline.xml new file mode 100644 index 00000000..b85ace8e --- /dev/null +++ b/app/src/main/res/drawable/rounded_outline.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml index fd845ee8..4974a027 100644 --- a/app/src/main/res/layout/download_child_episode.xml +++ b/app/src/main/res/layout/download_child_episode.xml @@ -9,6 +9,7 @@ android:layout_height="50dp" android:layout_marginBottom="5dp" android:foreground="@drawable/outline_drawable" + android:focusable="true" android:nextFocusLeft="@id/nav_rail_view" android:nextFocusRight="@id/download_button" app:cardBackgroundColor="@color/transparent" @@ -84,7 +85,9 @@ android:layout_height="@dimen/download_size" android:layout_gravity="center_vertical|end" android:layout_marginStart="-50dp" - android:background="?selectableItemBackgroundBorderless" + android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusLeft="@id/download_child_episode_holder" android:padding="10dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index 226c1632..21f79ca6 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -9,6 +9,8 @@ android:layout_marginTop="10dp" android:layout_marginEnd="10dp" android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusRight="@id/download_button" app:cardBackgroundColor="?attr/boxItemBackground" app:cardCornerRadius="@dimen/rounded_image_radius"> @@ -71,7 +73,9 @@ android:layout_height="@dimen/download_size" android:layout_gravity="center_vertical|end" android:layout_marginStart="-50dp" - android:background="?selectableItemBackgroundBorderless" + android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusLeft="@id/episode_holder" android:padding="10dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_plugin_details.xml b/app/src/main/res/layout/fragment_plugin_details.xml index 7a8f85e4..79013d9f 100644 --- a/app/src/main/res/layout/fragment_plugin_details.xml +++ b/app/src/main/res/layout/fragment_plugin_details.xml @@ -52,6 +52,7 @@ android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="@string/title_settings" android:visibility="gone" + android:focusable="true" app:srcCompat="@drawable/ic_baseline_tune_24" tools:visibility="visible" /> @@ -61,6 +62,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical|end" android:layout_marginStart="16dp" + android:focusable="true" android:background="?attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_github_logo" /> @@ -81,6 +83,7 @@ style="@style/SmallBlackButton" android:layout_gravity="center" android:layout_marginStart="10dp" + android:focusable="false" android:text="@string/extension_description" /> + + - - + android:orientation="horizontal"> + android:layout_marginEnd="5dp" + tools:text="Season 2 Episode 1022 will be released in" /> + android:scaleType="centerCrop" + android:foreground="@drawable/rounded_outline" + tools:src="@drawable/profile_bg_orange" + android:contentDescription="@string/account"/> + + tools:text="Quick Brown Fox" /> @@ -132,10 +133,24 @@ android:id="@+id/commit_hash" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center" android:padding="10dp" android:text="@string/commit_hash" android:textColor="?attr/textColor" /> + + + + diff --git a/app/src/main/res/layout/player_select_tracks.xml b/app/src/main/res/layout/player_select_tracks.xml index d32e1b4e..94e09d60 100644 --- a/app/src/main/res/layout/player_select_tracks.xml +++ b/app/src/main/res/layout/player_select_tracks.xml @@ -38,21 +38,20 @@ android:requiresFadingEdge="vertical" android:id="@+id/video_tracks_list" android:layout_width="match_parent" - android:layout_height="match_parent" android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" - android:nextFocusLeft="@id/sort_subtitles" - android:nextFocusRight="@id/apply_btt" + android:nextFocusRight="@id/audio_tracks_holder" + tools:listitem="@layout/sort_bottom_single_choice" /> + android:id="@+id/audio_tracks_holder" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="50" + android:orientation="vertical"> @@ -107,17 +106,16 @@ + android:requiresFadingEdge="vertical" + android:id="@+id/auto_tracks_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_rowWeight="1" + android:background="?attr/primaryBlackBackground" + android:nextFocusRight="@id/apply_btt" + android:nextFocusLeft="@id/video_tracks_list" + tools:listfooter="@layout/sort_bottom_footer_add_choice" + tools:listitem="@layout/sort_bottom_single_choice" /> @@ -132,11 +130,12 @@ + style="@style/WhiteButton" + android:layout_gravity="center_vertical|end" + android:text="@string/sort_apply" + android:id="@+id/apply_btt" + android:nextFocusLeft="@id/auto_tracks_list" + android:layout_width="wrap_content" /> - - + android:layout_height="wrap_content" + tools:visibility="visible"> @@ -130,7 +133,7 @@ android:clickable="true" android:contentDescription="@string/download" android:focusable="true" - android:nextFocusLeft="@id/repository_item_root" + android:nextFocusLeft="@id/action_settings" android:padding="12dp" tools:src="@drawable/ic_baseline_add_24" /> diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index 76e8c434..e5a6881a 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -43,14 +43,26 @@ android:foreground="?android:attr/selectableItemBackgroundBorderless" android:nextFocusRight="@id/download_button" android:scaleType="centerCrop" - tools:src="@drawable/example_poster" /> + tools:src="@drawable/example_poster" + tools:visibility="invisible"/> + android:src="@drawable/play_button" + tools:visibility="invisible"/> + + + + + + Laai tans… Sub Verwyder - Stroom + Netwerk stroom Op rus CloudStream Speel @@ -105,4 +105,5 @@ Soek met behulp van tipes Voer lettertipes in deur dit in %s te plaas Rolverdeling: %s + Nuwe episode notifikasie diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index 550d83a9..734d5644 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -68,8 +68,8 @@ هيدا المصدر مش عاطي \"ميتا داتا\". إذا مش موجودة بالمصدر، ما رح يمشي الڤيديو. ما في أجزاء مشّي الحلقة - أكونتات - سرعة \"إيگن گرايڤي\" + الحسابات والأمان + سرعة الڤيديو كَمِّل سِجِل ما نلاقى الوصف @@ -80,7 +80,6 @@ محي بلش في تِلِفونات ما فيا تعوز الطريقة الجديدة لتجديد الآپات. جربو \"الطريقة القديمة\" إذا ما عم تنزل التجديدات. - بتزيد أوپسيونات لتحدد سرعة الڤيديو بعد ما تسكر \"كلود ستريم\"، بكفي الڤيديو بشِباك زغير فوق غير آپ هيدا المصدر ما بيدعم \"كروم كاست\" تنبيش منظّم @@ -124,7 +123,7 @@ %1$d–%2$d عطي المطورين موزززة عوز النسخة الإحتياطية - نقى أكونت + نقي أكونت سحابو ل فوق وتحت من اليمين أو الشمال ل تتحكمو بقوة الصوت أو قوة الضو ننسخ الرابط الوصف @@ -144,7 +143,7 @@ لودينگ… شيل بَعِد مَعلومات - التجديد والنسخات الاحتياطية + التجديدات والنسخات الاحتياطية خبي هيدي الجودات من نتائج التنبيش موقف موقتًا كلود ستريم @@ -199,7 +198,7 @@ شوف إذا في تجديد في مشكلة بجهاز العرض (Renderer error) العِنوان - پروكسي raw.githubusercontent.com + پروكسي \"گِت هَب\" جودة مشغل الڤيديو ملصق الترجمة أوڤا @@ -252,7 +251,7 @@ الحد الأعلى للحروف بعنوان الڤيديو المظاهر تجديدات الآپ - لغات المصادر + لغات الإضافات عام ممر التنزيل إخلاء مسؤولية @@ -265,7 +264,7 @@ فرجي أنمي المدبلج-المترجم النسخ الإحتياطي الإجراءات - بتتجاوز منع \"گِت هَب\" بستعمال JSDelivr. معقول تقدي لتأخير بتجديدات الآپ بكم يوم. + تجاوز منع روابط \"گِت هَب\" الـ\"raw\" بستعمال JSDelivr. معقول تقدي لتأخير تجديدات الآپ بكم يوم. ميزات مشغل الڤيديوات شيل موقع مظهر @@ -289,7 +288,7 @@ الشكل %1$d ساعة %2$d ديقة فضّى - إسم أكونتي الكول + إسم الأكونت فَلتِر الإشارات المرجعية بَلَش التجديد نسخ @@ -305,9 +304,9 @@ مشّي الحلقة سرعة الڤيديو تحكمو بالأكونتات - رمز اللغة (apc/ar/en) + رمز اللغة (ar) عمول أكونت - فرجي المحتوى الـ18+ بالمصادر يلي بتحتوي + تمكين محتوى 18+ في الامتدادات الداعمة المحتوى المفضل غَيِر الأكونت حط پوز على التنزيل @@ -334,8 +333,8 @@ إي مايل (ع شكل: email@example.com) %d ديقة فحص المصادر - example.com - إسم الوبسيت الكول تبعي + https://example.com + إسم الوب سيت الجديدة فتاح ومَشّي الملف نوع الألون رجاع @@ -348,7 +347,7 @@ اللون الاساسي فوت ع الأكونت مترجم - بِث + بِث من الإنترنت مشي التخزين الجُواني زيد أكونت @@ -382,11 +381,9 @@ حطو الأرقام السرية لـ\"%s\" الطريقة القديمة معلى - \"كلود ستريم\" ما بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپوز. + \"كلود ستريم\" ما بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپويات. \n -\nفي معلومات على الـ\"ديسكورد\" تبعنا، أو فيكون تنبشو ع معلومات على الإنترنت. -\n -\nما فينا تحط الروابط تبع ريپوز المصادر هون من ورا \"سكاي يو كي المحدودة\" 🤮، يلي عازت تفكيرا المحدود لتجرب توقف هيدا الآپ بإستعمال \"قانون الألفية للملكية الرقميَّة\". +\nفي معلومات على الـ\"ديسكورد\" تبعنا، أو فيكن تنبشو ع معلومات على الإنترنت. زبد تتبع 3G/4G… نفَتح %s @@ -410,7 +407,7 @@ HDR مش منزل: %d ل بعدو - رابط الڤيديو + https://example.com/example.mp4 نقي الرفّ وقفتو الإشتراك لـ\"%s\" WP @@ -451,7 +448,7 @@ التلخيص إِيه الرئيسي - طبّق وقتما سكّر الآپ + سكر الآپ حتى تطبق التغيرات ساعدوني عوز هيدا إذا عم بتبين الترجمة %d ميلي ثانية بعدما لازم ما نلاقا الآپ @@ -569,7 +566,7 @@ دي ڤي دي الجودة عين الافتراضي - المرجع + المرجع (إختياري) المشغل يلي بـ\"كلود ستريم\" نزل لايحة المواقع يلي بدك تعوزن حطو الأرقام السرية @@ -595,5 +592,34 @@ برومو غير إتجاه الشاشة أوتوماتيكيًا حسب شكل الڤيديو رجع نعمل لاود لاللينك - نعمل كَپي للعنوان! + نبش بغير مصادر + نوتيفيكايشن عن حلقات جديدة + فرجي الاقترحات + بتزيد خيار السرعة بالمشغل + فحاص كل المصادر + هيدا الفحص معمول للمطورين وما بأكد لحالو إزا المصدر عم يشتغل. + المفضلة + فتح قفل كلودستريم + قفل بواسطة المقاييس الحيوية + رمز/كلمة مرور للمصادقة + فتاح التطبيق باستعمال البصمة، آي دي الوج، پِن، النمط، إو الپاسورد. + تسَكرت هيدي الواجهة من ورا محاولات فاشلة عديدة. پليز، سكر الآپ ورجاع فتحه. + %s +\nباقي + المصادقة البيومترية مش مدعومة ع هالجهاز + شيله من المفضل + اسم وعنوان الريپوزيتوري + نتسخ! + في ارور بالوصول ل الكليپبورد. پليز جرب مرة أخرى. + في ارور بالنسخ. پليز نسوخ الـLogcat 🐈 وبعته ل المسؤولين عن دعم الآپ. + هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. +\nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج. + أوكي + وقف اپتميزايشن بطارية جهازك + بطارية الآپ اصلًا محطوطة ع «غير مقيد» \"Unrestricted\" + ما قدرنا نفتح معلومات الآپ تبع \"كلود ستريم\". + موسيقى + أوديو بوك + الميديا + لتضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يلي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي بـ الباكگروند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". ملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآن بـ الباكگروند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ بـ«الإعدادات العامة» \"General settings\"، إزا غيرت رأيك. diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a8e79d22..8681398d 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -54,7 +54,7 @@ فشل التنزيل تم إلغاء التنزيل تم التنزيل - بث + دفق الشبكة خطأ في تحميل الرابط التخزين الداخلي مدبلج @@ -114,8 +114,7 @@ إعدادات ترجمة المُشغل ترجمة كروم كاست إعدادات ترجمة كروم كاست - وضع إيغنغرافي - يضيف خيار السرعة في المُشغل + سرعة التشغيل السحب لتقديم اسحب من جانب إلى آخر للتحكم في موضعك في مقطع فيديو السحب لتغيير الإعدادات @@ -139,7 +138,7 @@ إذن الوصول الى ذاكرة التخزين مفقود, من فضلك حاول مجددا. فشل إنشاء نسخة احتياطية %s بحث - الحسابات + الحسابات والأمن التحديثات والنسخ الاحتياطية معلومات البحث المتقدم @@ -284,10 +283,10 @@ عام زر العشوائي إظهار زر عشوائي على الصفحة الرئيسية والمكتبة - لغات المزود + لغات الامتداد واجهة التطبيق المحتوى المفضل - تفعيل محتوى خاص للبالغين داخل المزودين المدعومين + تفعيل محتوى خاص للبالغين داخل الإمتداد المدعوم فك تشفير الترجمة المصادر الواجهة @@ -308,8 +307,8 @@ إسم المستخدم البريد الإلكتروني 127.0.0.1 - إسم الموقع - رابط الموقع + إسم الموقع الجديد + رابط الموقع مثلا : https://example.com اللغة (الإنجليزية) Poster Pôster - Episode Poster - Main Poster - Next Random - Go back - Change Provider - Preview Background + Pôster do episódio + Pôster Principal + Próximo Aleatório + Voltar + Alterar Provedor + Visualizar plano de fundo Velocidade (%.2fx) - Nota: %.1f + Avaliado: %.1f Nova atualização encontrada! \n%1$s -> %2$s - Filler + Preenchimento %d min CloudStream Início - Procurar + Pesquisar Downloads Configurações Procurar… - Procurar no %s… + Pesquisar %s… Sem dados - Mais Opções + Mais opções Próximo episódio Gêneros Compartilhar - Abrir no Navegador - Pular Carregamento + Abrir no navegador + Pular carregamento Carregando… Assistindo Em espera - Completado - Deixado + Concluído + Desistido Planejando assistir Reassistindo - Assistir Filme + Reproduzir filme Transmitir Torrent Fontes Legendas - Tentar reconectar… - Voltar - Assistir Episódio + Tentando conectar novamente… + Volte + Reproduzir episódio - Baixar - Baixado + Download + Download concluído Baixando - Download Pausado - Download Iniciado - Download Falhado - Download Cancelado - Download Finalizado + Download pausado + Download iniciado + Download falhou + Download cancelado + Download concluído Transmitir - Erro Carregando Links - Armazenamento Interno + Erro ao carregar links + Armazenamento interno Dub Sub - Deletar Arquivo - Assistir Arquivo - Retomar Download - Pausar Download - Desativar relatório automático de erros - Mais info + Deletar arquivo + Reproduzir arquivo + Retomar download + Pausar download + Desative o relatório automático de erros + Mais informações Esconder - Assistir - Info - Filtrar Marcadores + Reproduzir + Informações + Filtrar marcadores Marcadores Remover - Selecionar marcador + Definir como assistido/não assistido Aplicar Copiar Fechar Limpar Salvar - Velocidade do Reprodutor - Configurar Legendas - Cor do Texto - Cor do Contorno - Cor do Fundo - Cor da Janela - Tipo de Borda - Elevação da Legenda + Velocidade de reprodução + Configurações de legendas + Cor do texto + Cor do contorno + Cor de fundo + Cor da janela + Tipo de borda + Elevação da legenda Fonte - Tamanho da Fonte + Tamanho da fonte Pesquisar usando fornecedor - Pesquisar usando genêros + Pesquisar usando tipos %d Benenes doados aos desenvolvedores Nenhuma Benenes doada - Autosseleção de Lingua - Baixar Linguas - Lingua da legenda - Segure para retornar a configuração padrão - Importe fontes colocando elas em %s - Continue Assistindo + Seleção automática de idioma + Baixar idiomas + Idioma da Legenda + Segure para redefinir para o padrão + Importe fontes colocando-as em %s + Continuar assistindo Remover Mais Info @string/home_play @@ -122,8 +122,7 @@ Configurações de legendas do Player Legendas do Chromecast Configurações de legendas do Chromecast - Modo Eigengravy - Adiciona um botão de velocidade no player + Velocidade de playback Deslize para avançar o vídeo Deslize de lado à lado para controlar a posição no vídeo Deslize para mudar as configurações @@ -145,8 +144,8 @@ Permissões de armazenamento faltando. Por favor tente novamente. Erro no backup de %s Procurar - Contas - Atualizações e backup + Contas e Segurança + Atualizações e Backup Info Procura Avançada Mostrar resultados separados por fornecedor @@ -167,7 +166,7 @@ Junte-se ao Discord Dar um benene para os desenvolvedores Benene dada - Linguagem do App + Idioma do aplicativo Esse fornecedor não possui suporte para Chromecast Nenhum link encontrado Link copiado para área de transferência @@ -280,8 +279,8 @@ Any legal issues regarding the content on this application should be taken up with the actual file hosts and providers themselves as we are not affiliated with them. In case of copyright infringement, please directly contact the responsible parties or the streaming websites. The app is purely for educational and personal use. CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down. CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface. It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream 3 at your own risk. Geral Botão Aleatório - Mostra o botão Aleatório na página inicial - Linguagem dos fornecedores + Mostrar botão aleatório na página inicial e na biblioteca + Linguagem das extensões Layout do App Mídia preferida Codificação das legendas @@ -296,11 +295,11 @@ Coloca o título debaixo do poster senha123 - MeuNomeLegal + Nome de usuário oi@mundo.com 127.0.0.1 - MeuSiteLegal - examplo.com + NovoNomedoSite + https://example.com Codigo da Língua (bp) - Já fiz vinho com toque de kiwi para belga sexy. + A rápida raposa marrom salta sobre o cachorro preguiçoso Recomendada %s carregada Carregar de arquivo @@ -419,8 +418,6 @@ Não transferido: %d CloudStream não tem fontes instaladas por padrão. Você precisa instalar um site de repositórios. \n -\nPor causa das limitações do DMCA (Digital Millennium Copyright Act ) feito em nome de Sky UK Limited 🤮nós não podemos adicionar site de repositórios no app. -\n \nEntre no nosso Discord ou pesquise online. Ver repositórios da comunidade Lista pública @@ -429,23 +426,23 @@ %s (Desativado) Reproduzir automaticamente próximo episódio Começa o próximo episódio quando o atual termina - Ativar NSFW em fornecedores compatíveis + Ativar NSFW em extensões compatíveis Fornecedores Reverter Ações votou com sucesso Baixando atualização do aplicativo… - Referencias + Referenciador (opcional) Atualizações do App - Tocar com CloudStream + Assistir com o CloudStream Automaticamente instale todos os plugins não instalados dos repositórios adicionados. - Reproduzir Trailer + Reproduzir trailer Navegador Copia de Segurança A Barra de Progresso pode ser usada quando o player estiver oculto Inscrito Essa lista está vazia. Tente mudar para outra. - Reproduzir Livestream + Reproduzir transmissão ao vivo Log do Teste Baixar plugins automaticamente Selecione o modo para filtrar os plugins baixados @@ -476,7 +473,7 @@ Conteúdo +18 Ajuda Processo de configuração de Redo - Não pudemos instalar a nova versão do App + Não foi possível instalar a nova versão do aplicativo instalador de pacotes Organizar por Votação (Alta para Baixa) @@ -493,7 +490,7 @@ Arquivo de modo de segurança encontrado! \nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido. Inscrito em %s - Episódio %d lançado + Episódio %d lançado! Selecionar padrão Inscrição cancelada de %s Alguns aparelhos não possuem suporte para este pacote de instalação. Tente a opção legada se a atualização não instalar. @@ -544,7 +541,7 @@ MPV Abrindo mistura VLC - Aplicar quando reiniciar + Reinicie o aplicativo para ver as alterações. Visualização info de crash Faixas de áudio Adicionado em (novo para antigo) @@ -558,7 +555,7 @@ Aparência Desativar Usar - Link da stream + https://example.com/example.mp4 Gestos Plugin baixado Não foi possível se conectar ao GitHub. Ativando proxy JsDelivr… @@ -572,7 +569,74 @@ Provedor de teste Layout Padrões - Proxy: raw.githubusercontent.com - Contorna o bloqueio do GitHub usando jsDelivr. Pode atrasar as atualizações por alguns dias. + Proxy do GitHub + Contorne o bloqueio de URLs \"raw\" do GitHub usando jsDelivr. Pode atrasar as atualizações por alguns dias. Rotas alternativas + Favoritos + %s adicionado aos favoritos + Duplicata em potencial encontrada + Adicionar + Substituir + Possíveis itens duplicados foram encontrados em sua biblioteca: +\n +\n %s +\n +\nGostaria de adicionar este item mesmo assim, substituir os existentes ou cancelar a ação? + Insira o PIN + Insira o PIN para %s + Insira o PIN atual + PIN incorreto. Por favor, tente novamente. + O PIN deve ter 4 caracteres + Selecione uma conta + Gerenciar contas + Ignorar a seleção da conta na inicialização + Exibir um botão para alternar a orientação da tela + %s removido dos favoritos + Adicionar aos favoritos + Remover dos favoritos + Girar + Bloquear perfil + PIN + Links recarregados + Frequência de backup + Substitua tudo + Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' +\n +\nGostaria de adicionar este item mesmo assim, substituir o existente ou cancelar a ação? + Inscrever-se + Cancelar inscrição + Usar conta padrão + Editar conta + Conectado como %s + Habilite a troca automática de orientação da tela com base na orientação do vídeo + Rotação automática + Notificação de novo episódio + Pesquisar em outras extensões + Mostrar recomendações + Adiciona uma opção de velocidade no reprodutor + Testar todas as extensões + Esse teste é feito somente para desenvolvedores e não verifica ou nega o funcionamento de qualquer extensão. + Desbloquear CloudStream + O backup dos seus dados do CloudStream foi feito agora. Embora a possibilidade disso seja muito baixa, todos os dispositivos podem se comportar de maneira diferente. No caso raro de você ficar impedido de acessar o aplicativo, limpe os dados do aplicativo completamente e restaure a partir de um backup. Lamentamos muito qualquer inconveniente decorrente disso. + Bloquear com Biometria + Autenticação de Senha/PIN + A autenticação biométrica não é compatível com este dispositivo + Desbloquear o aplicativo com impressão digital, ID facial, PIN, padrão e senha. + Esta tela foi fechada devido a diversas tentativas malsucedidas. Por favor reinicie o aplicativo. + %s +\nrestante(s) + Favorito + Não favorito + copiado! + Erro ao acessar a área de transferência. Tente novamente. + Nome e URL do repositório + Erro ao copiar. Copie o logcat e entre em contato com o suporte do aplicativo. + Para garantir downloads e notificações ininterruptos para programas de TV assinados, o CloudStream precisa de permissão para ser executado em segundo plano. Ao pressionar OK, você será direcionado para as informações do aplicativo. Lá, vá até Uso da bateria do aplicativo e defina o uso da bateria como Irrestrito. Observe que esta permissão não significa que o CS3 irá descarregar sua bateria. Ele só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se você optar por cancelar, poderá ajustar essa configuração posteriormente nas Configurações Gerais. + Ok + Desativar otimização de bateria + O uso da bateria do app já está definido como irrestrito + Não foi possível abrir as informações do aplicativo CloudStream. + Música + Áudio-livro + Mídia diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 00c968f2..0a8cf997 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -117,8 +117,7 @@ Nastavení titulků přehrávače Titulky Chromecastu Natavení titulků Chromecastu - Rychlostní režim - Přidá do přehrávače možnost rychlosti + Rychlost přehrávání Přejet pro posun Přejeďte prstem ze strany na stranu pro ovládání své pozice ve videu Přejet pro změnu nastavení @@ -140,7 +139,7 @@ Chybí oprávnění k úložišti. Zkuste to prosím znovu. Chyba při zálohování %s Search - Účty + Účty a zabezpečení Aktualizace a záloha Informace Pokročilé hledání @@ -266,7 +265,7 @@ Obecné Náhodné tlačítko Zobrazit na domovské stránce a v knihovně náhodné tlačítko - Jazyk poskytovatelů + Jazyky rozšíření Rozložení aplikace Preferovaná média Kódování titulků @@ -281,7 +280,7 @@ Umístit název pod plakát heslo123 - MojeSuperJmeno + Uživatelské jméno ahoj@svete.cz 127.0.0.1 lozinka123 - MojeCoolIme + Korisničko ime bok@svijete.com 127.0.0.1 - MojaCoolStranica - primjer.com + NovoImeStranice + https://primjer.com Šifra jezika (en) %1$s %2$s račun @@ -402,8 +401,8 @@ Filtriraj po željenom jeziku medija Extras Trailer - Veza na stream - Upućivač + https://primjer.com/primjer.mp4 + Referent (nije obavezno) Sljedeće Gledaj videozapise na ovim jezicima Prethodno @@ -434,8 +433,6 @@ Nepreuzeto: %d CloudStream nema instalirane web stranice prema zadanim postavkama. Morate instalirati stranice iz repozitorija. \n -\nZbog bezumnog uklanjanja DMCA od strane Sky UK Limited 🤮 ne možemo povezati web mjesto repozitorija u aplikaciji. -\n \nPridružite se našem Discordu ili tražite online. Pregledajte repozitorije zajednice Javni popis @@ -549,9 +546,9 @@ Otkazana pretplata sa %s Vraćanje ISP zaobilaznice - raw.githubusercontent.com Proxy + GitHub Proxy Neuspješno dohvaćanje GitHuba. Uključuje se jsdelivr proxy … - Zaobilazi blokiranje GitHuba koristeći jsdelivr. Može odgoditi ažuriranja za nekoliko dana. + Zaobilazi blokiranje neobrađenih GitHub URL-ova koristeći jsDelivr. Može uzrokovati kašnjenje ažuriranja nekoliko dana. Preferirana kvaliteta gledanja (podatkovna mobilna mreža) Profil %d Wi-Fi @@ -614,7 +611,22 @@ Prikaži gumb za prebacivanje orijentacije zaslona Omogućuje automatsko mijenjanje orijentacije zaslona na temelju orijentacije videa Automatsko rotiranje - Naslov je kopiran! rotiraj_video_tipka automatski_rotiraj_video_tipka + Obavijest za novu epizodu + Pretraži u ostalim proširenjima + Dodaje opciju brzine u playeru + Testiraj sva proširenja + Ovaj je test namijenjen samo programerima i ne provjerava niti negira rad bilo kojeg proširenja. + Prikaži preporuke + Ime repozitorija i URL + kopirano! + Zaključaj s biometrijskim podatcima + %s +\npreostalo + Greška u pristupanju međuspremnika. Pokušaj ponovo. + Otključaj CloudStream + Lozinka/PIN autentifikacija + Ovaj uređaj ne podržava biometrijsku autentifikaciju + Ovaj je ekran zatvoren zbog višestrukih neuspjelih pokušaja. Pokrenite aplikaciju ponovo. diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 5b1dbcf0..5533cdc0 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -37,7 +37,7 @@ Állapot Forrás címe Előzmények - Eigengravy mód + Visszajátszás sebessége Felirat magassága Letöltés elkezdve Nem található cselekmény @@ -71,7 +71,7 @@ Feliratok Vissza Letöltés kész - Stream + Hálózati stream Hiba a linkek betöltésekor Belső tárhely Dub @@ -124,7 +124,6 @@ Lejátszó feliratok beállításai Chromecast Feliratok Chromecast feliratok beállításai - Sebességbeállítást ad hozzá a lejátszóhoz Epizód lejátszása Letöltve Letöltés szüneteltetve @@ -171,11 +170,11 @@ OVA Egyebek Sorozat - @string/anime + Anime Forráshiba NSFW Rajzfilm - @string/ova + OVA Élőadás NSFW Videó @@ -184,7 +183,7 @@ Ázsiai dráma Linkek újratöltése Link másolás - Link letöltés + Letöltés mirror Automatikus letöltés Adatok eltárolva Hiba a biztonsági mentés során %s @@ -223,9 +222,9 @@ Távoli hiba Render hiba Váratlan lejátszó hiba - Letöltés hiba, ellenőrízze a tárolási engedélyeket + Letöltés hiba, ellenőrizze a tárolási engedélyeket Chromecast epizód - Chromecast link + Chromecast mirror Lejátszás az alkalmazásban Lejátszás %s Lejátszás böngészőben @@ -233,7 +232,7 @@ Újracsatlakozás… Húzd balra vagy jobbra a videólejátszóban az idő vezérléséhez Csúsztassa ujját a beállítások módosításához - Csúsztassa felfelé vagy lefelé a bal vagy jobb oldalon a fényerő vagy a hangerő megváltoztatásához + Csúsztassa felf/le az ujját a bal/jobb oldalon a fényerő vagy a hangerő megváltoztatásához Biztonsági mentés 0 Banán a fejlesztőknek Húzd el, hogy beless @@ -243,7 +242,7 @@ Dupla koppintás a szüneteltetéshez Lejátszó keresési értéke (Másodpercben) Koppintson kétszer a jobb vagy bal oldalra az előre vagy hátra ugráshoz - Koppintson kétszer középen a szüneteltetéshez + Koppintson kétszer középre a szüneteltetéshez Rendszer fényerejének használata Rendszer fényerejének használata az appban a sötét átfedés helyett Előrehaladás frissítése @@ -286,7 +285,7 @@ Funkciók Előnyben részesített videóminőség (mobilinternet) Videolejátszó cím max karakterek - Nem sikerült elérni a GitHubot, a jsdelivr proxy engedélyezése. + Nem sikerült elérni a GitHubot, a jsdelivr proxy bekapcsolva… Bővítmények Általános Felirat kódolása @@ -295,10 +294,10 @@ Szolgáltató teszt Sikertelen Problémákat okoz, ha túl magasra van állítva az alacsony tárhellyel rendelkező eszközökön, például az Android TV-n. - Korhatáros tartalmak engedélyezése a támogatott szolgáltatóknál + Korhatáros tartalmak engedélyezése a támogatott kiegészítőknél Elrendezés - raw.githubusercontent.com Proxy - A lejátszó funkciói + GitHub Proxy + Lejátszó funkciók Előnyben részesített videóminőség (WiFi-n) Hasznos az internetszolgáltató blokkjainak megkerüléséhez Elrendezés @@ -306,10 +305,10 @@ NGINX szerver URL-címe Szinkronizált/feliratozott animék megjelenítése Alapértelmezettek - Megjelenít egy gombot a Kezdőlapon, amely egy véletlenszerű filmet vagy TV sorozatot választ a Kezdőlapról + Véletlenszerű gomb megjelenítése a Könyvtárban és Főoldalon Letöltési útvonal Gyorsítótár - Szolgáltatók nyelvei + Kiegészítők nyelvei Napló Könyvtár internetszolgáltató-kikerülések @@ -322,7 +321,7 @@ Előnyben részesített média Hivatkozások Videó és kép gyorsítótár törlése - A jsdelivr használatával a GitHub blokkolása megkerülhető. Néhány nappal késleltetheti a frissítéseket. + A jsDelivr használatával a tiszta GitHub blokkolása megkerülhető. Néhány nappal késleltetheti a frissítéseket. Összeomlást okoz, ha túl magasra van állítva a kevés memóriával rendelkező eszközökön, például az Android TV-n. Betöltés az internetről Videósávok @@ -333,13 +332,13 @@ Alkalmazásfrissítés letöltése… Frissítve (újabbtól a régebbihez) Úgy tűnik, a könyvtárad üres :( -\nJelentkezz be egy könyvtár fiókba, vagy adj hozzá műsorokat a helyi könyvtárodhoz - Úgy tűnik, ez a lista üres, próbálj meg egy másikra váltani +\nJelentkezz be egy könyvtár fiókba, vagy adj hozzá műsorokat a helyi könyvtárodhoz. + Úgy tűnik, ez a lista üres, próbálj meg egy másikra váltani. Max 4K SDR Fiók létrehozása - pelda.com + https://példa.hu Feliratok szinkronizálása Alkalmazásfrissítés telepítése… Túl sok szöveg. Nem lehet a vágólapra menteni. @@ -349,7 +348,7 @@ Frissítés /\?\? Árnyék - Filmelőzetes + Előzetes Mit szeretnél látni Minden %s már letöltött Először telepítse a bővítményt @@ -357,13 +356,13 @@ Kinézet Alkalmazás elrendezés Szinkronizálás - Nem sikerült bejelentkezni a következőként: %s + Nem sikerült bejelentkezni a %s-nál Min 1000 ms Ajánlott Érvénytelen adatok - Link a streamhez + https://példa.hu/példa.mp4 Nem sikerült betölteni: %s Elkezdődött a(z) %1$d %2$s letöltése… Töltse le az összes bővítményt ebből a tárolóból\? @@ -372,16 +371,16 @@ MPV Alkalmazás nem található PackageInstaller - Rendezés e szerint: + Rendezés e szerint Feliratkozott a következőre: %s - MenőWeboldalam + ÚjOldalNév DVD %d plugin frissítve Értékelés: %s Előzmények törlése Nem Feliratkozva - Használd ezt, ha a feliratok %d ms-sel korábban jelennek meg. + Használd ezt, ha a feliratok %d ms-sel korábban jelennek meg Lejátszó Felbontás és cím Előnyben részesített videolejátszó @@ -420,7 +419,7 @@ Minden felirat nagybetűs Intro Leiratkozott a következőről: %s - Bloat eltávolítása a feliratokról + Szükségtelen elemek eltávolítása a feliratokról Szűrés előnyben részesített médianyelv szerint Biztos vagy benne, hogy ki akarsz lépni\? Rendezés @@ -431,9 +430,7 @@ Ez az összes tároló bővítményt is törli A CloudStream alapértelmezés szerint nem telepített webhelyeket. A webhelyeket a tárolókból kell telepítenie. \n -\nA Sky UK Limited agyatlan DMCA letiltása miatt 🤮 nem tudjuk az alkalmazásban linkelni az adattár oldalát. -\n -\nCsatlakozz a Discordunkhoz vagy keress online. +\nCsatlakozz a Discord-unkhoz vagy keress online. Verzió Megjelölés megtekintettként Eltávolítás a megnézettek közül @@ -466,7 +463,7 @@ Betűrendben (A-tól a Z-ig) Frissítve (régebbitől az újabbig) jelszó123 - AzÉnMenőFelhasználónevem + Felhasználónév 127.0.0.1 Fiókváltás Fiók hozzáadása @@ -481,14 +478,14 @@ Bővítmények Tároló hozzáadása Tároló neve - Tárhely URL címe + Repó URL Bővítmény betöltve Bővítmény letöltve Közreműködők Betűrendben (Z-től az A-ig) Könyvtár kiválasztása - Biztonságos módú fájl található! -\nNem tölt be semmilyen kiterjesztést indításkor, amíg a fájl el nem lesz távolítva. + Biztonságos módú fájlba ütköztünk! +\nNem töltődik be semmilyen kiegészítő indításkor, amíg a fájl nem kerül törlésre. Normál %s betöltve Beállítás kihagyása @@ -503,7 +500,7 @@ Az átugrás mértéke, amikor a lejátszó el van rejtve Jogi nyilatkozat Lejátszó megjelenítve - Ugrási Érték - Lejátszó elrejtve - Ugrási Érték + Lejátszó Elrejtve - Ugrási Érték Klónozott oldal Egy meglévő webhely klónjának hozzáadása, más URL-címmel TV elrendezés @@ -513,4 +510,86 @@ Mentési gyakoriság Értékelt Kikapcsolás + Kamera Rip + Nyitás + WP + Új epizód értesítés + Keresés más kiegészítőkben + Ajánlatok mutatása + Sebesség opció megjelenítése a lejátszóban + Minden kiegészítő tesztelése + Ez a teszt szigorúan fejlesztők számára készült, nem alkalmas egyes kiegészítők működésének visszaigazolására. + %1$s %2$s + TS + Feliratkozás + Leiratkozás + Linkek Újratöltve + Zárás + Hivatkozó (opcionális) + Nem találhatóak pluginek a repóban + Repó nem található, ellenőrizze a címet vagy próbálja VPN-el + Web Videó Cast + %s kihagyása + A kihagyási felugró ablakok mutatása nyitás/zárás esetén + Alapbeállítás + Használ + %d profil + Használja ezt ha a felirat %d ms-ot késik + Kamera HD + Poszter Kép + TC + Mobil adat + Wi-Fi + Szerkeszt + Kamera + Nyomott + Kevert zárás + Kevert nyitás + Segítség + Profilok + Eltávolítás kedvencekből + Adja meg a jelenlegi PIN-t + Itt beállíthatja hogyan rendezze el a forrásokat. Ha egy videónak nagyobb a prioritása, előbb fog megjelenni a listában. A forrás prioritás és a minőség prioritás együttes értéke adja ki a videó prioritást. +\n +\nForrás A: 3 +\nMinőség B: 7 +\nEzek összértéke egy 10-es videó prioritást eredményez. +\n +\nFIGYELEM: Ha az összeg több mint 10, a lejátszó nem tölt be mást ha már a link betöltésre került! + Potenciálisan dupla elemek a könyvtárjában: +\n +\n%s +\n +\nSzeretné hozzáadni ezt az elemet mindenképpen, ezzel felülírva a jelenlegit, vagy visszavonja a műveletet? + Fiók választás kihagyása belépéskor + Használjon alapértelmezett fiókot + Elforgatás + Profil háttér + Kedvencek + %s hozzáadva a kedvencekhez + %s eltávolítva a kedvencekből + Hozzáadás a kedvencekhez + Úgy tűnik egy potenciálisan dupla elem már létezik a könyvtárjában: \'%s.\' +\n +\nMindenképpen hozzá akarja adni ezt az elemet, ezzel felülírva a régit, vagy visszavonja a műveletet? + Adja meg a PIN-t + Profil Zárolása + Válasszon egy fiókot + Fiókok kezelése + Fiók módosítása + Belépve mint %s + Jelenítsen meg egy kapcsolót a képorientáció váltáshoz + Potenciális Dupla Találat + Adja meg a PIN-t a %s-hoz + PIN + Hibás PIN. Próbálja újra. + A UI hibásan jelenítődött meg, ez egy JELENTŐS BUG ezért kérjük jelentse be %s + Már korábban szavazott + Hozzáadás + Kicserélés + Mind Kicserélése + Minőségek + A PIN 4 karakter hosszú kell legyen + Auto elforgatás + Az automatikus videó orientáció alapján való képernyő elforgatás bekapcsolása diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 0bb2a24a..d537a1d5 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -115,8 +115,7 @@ Pengaturan subtitle pemutar Subtitle Chromecast Pengaturan subtitle Chromecast - Mode Eigengravy - Menambahkan opsi kecepatan di pemutar + Kecepatan pemutaran Geser untuk mengubah waktu Geser dari sisi ke sisi untuk mengontrol posisi dalam video Geser untuk mengubah pengaturan @@ -138,8 +137,8 @@ Izin penyimpanan tidak ditemukan, mohon coba lagi. Error saat mencadang %s Cari - Kredit dan akun - Update dan cadangan + Akun dan Keamanan + Update dan Cadangan Info Pencarian Lanjutan Memberikan hasil pencarian yang dipisahkan berdasarkan provider @@ -263,8 +262,8 @@ Any legal issues regarding the content on this application should be taken up with the actual file hosts and providers themselves as we are not affiliated with them. In case of copyright infringement, please directly contact the responsible parties or the streaming websites. The app is purely for educational and personal use. CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down. CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface. It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream 3 at your own risk. Umum Tombol Acak - Tampilkan tombol acak di Beranda - Bahasa provider + Tampilkan tombol acak di Beranda dan Pustaka + Bahasa ekstensi Tata Letak Aplikasi Media yang lebih diinginkan Antarmuka pengguna @@ -373,10 +372,10 @@ Bawaan Tampilan Fitur - Tampilkan konten NSFW + Mengaktifkan NSFW pada Ekstensi yang didukung Putar Cuplikan Putar Siaran - Siaran + Aliran jaringan Bahasa Subtitel Putar otomatis episode selanjutnya Putar episode selanjutnya, setelah ini berakhir @@ -398,22 +397,20 @@ %1$s %2$d%3$s Siaran langsung Hapus Website - UsernameKeren + Username contoh@email.com 127.0.0.1 - Websiteku - contoh.com + NamaSitus Baru + https://contoh.com Ekstra Apa yang ingin anda lihat Plugin terhapus %d plugin diperbarui Lihat Repositori dari Group List Umum - CloudStream tidak memiliki sumber video secara bawaan. Kamu harus menginstall dari repositori. + CloudStream tidak memiliki situs yang terinstal secara default. Anda perlu menginstal situs-situs dari repositori. \n -\nKarena banyak laporan dari banyak pihak berwajib, kami tidak dapat memberikannya secara langsung. -\n -\nGabung dengan group Discord atau cari di internet. +\nBergabunglah dengan Discord kami atau cari secara online. Alamat Repositori Buat Akun Error @@ -435,7 +432,7 @@ Semua Umur %s (Tidak aktif) Trek - Terapkan saat dimuat ulang + Terapkan saat dimuat ulang untuk melihat perubahan. Keterangan Versi Status @@ -457,7 +454,7 @@ Pilih ini untuk menghapus semua repositori plugin Lewati pengaturan Alamat salah - Alamat streaming + https://contoh.com/contoh.mp4 Selanjutnya Sebelumnya Ubah tampilan aplikasi @@ -482,7 +479,7 @@ Gerakan Beberapa perangkat tidak mendukung penginstal paket mode baru. Coba mode lama jika pembaruan tidak dapat diinstal. Aksi - Referer + Memberi referensi (opsional) Ya Install ekstensi terlebih dahulu Semua Bahasa @@ -545,9 +542,9 @@ Berlangganan ke %s Berhenti berlangganan di %s Episode %d telah rilis! - Proxy raw.githubusercontent.com + Proxy GitHub Tidak dapat menjangkau GitHub. Mengaktifkan proxy jsDelivr… - Melewati pemblokiran GitHub menggunakan jsDelivr. Dapat menyebabkan pembaruan tertunda beberapa hari. + Lewati pemblokiran raw URL github menggunakan jsDelivr. Dapat menyebabkan pembaruan tertunda selama beberapa hari. Bypass ISP Pulihkan Kualitas nonton yang diinginkan (Data Seluler) @@ -607,4 +604,38 @@ Lewati pemilihan akun saat startup Kelola Akun Edit akun + Putar + Menampilkan tombol sakelar untuk orientasi layar + Tautan Dimuat Ulang + Mengaktifkan peralihan otomatis orientasi layar berdasarkan orientasi video + Putar otomatis + Cari di ekstensi lainnya + Menambahkan opsi percepat di pemutar + Tes ini hanya ditujukan untuk pengembang dan tidak memverifikasi atau menolak kerja ekstensi apa pun. + Notifikasi episode baru + Tampilkan rekomendasi + Menguji semua Ekstensi + Data CloudStream Anda telah dicadangkan. Meskipun peluang terjadinya kasus ini sangat kecil dan jarang terjadi, tetapi semua perangkat berperilaku berbeda. Jika Anda ada dalam situasi terburuk, misalnya gagal untuk mengakses aplikasi, segera hapus data aplikasi sepenuhnya dan pulihkan data cadangan. Kami mohon maaf atas segala ketidaknyamanan yang mungkin ditimbulkan. + Otentikasi Kata Sandi/PIN + Otentikasi biometrik tidak didukung di perangkat ini + Buka kunci aplikasi dengan Sidik Jari, ID Wajah, PIN, Pola, dan Kata Sandi. + Layar ini ditutup setelah mengalami beberapa kali percobaan yang gagal. Anda harus memulai ulang aplikasi ini. + Batalkan favorit + Buka kunci CloudStream + %s +\ntersisa + Favorit + Kunci dengan Biometrik + Nama dan URL repositori + Gagal mengakses Papan Klip, mohon coba lagi. + disalin! + Gagal menyalin, mohon salin logcat dan hubungi pengembang aplikasi. + Oke + Matikan pengoptimalan Baterai + Pemakaian baterai untuk aplikasi ini sudah diatur menjadi tidak dibatasi + Gagal membuka info aplikasi CloudStream. + Musik + Buku Audio + Media + Untuk memastikan unduhan dan pemberitahuan tanpa gangguan untuk acara TV berlangganan, CloudStream memerlukan izin untuk berjalan di latar belakang. Dengan menekan OK, Anda akan diarahkan ke Info aplikasi. Di sana, gulir ke Penggunaan baterai aplikasi dan atur penggunaan baterai ke Tidak Terbatas. Harap dicatat, izin ini tidak berarti CS3 akan menguras baterai Anda. Ini hanya akan beroperasi di latar belakang ketika diperlukan, seperti ketika menerima pemberitahuan atau mengunduh video dari ekstensi resmi. Jika Anda memilih untuk membatalkannya, Anda dapat menyesuaikan pengaturan ini nanti di Pengaturan Umum. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 74839a47..040b0f31 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -61,7 +61,7 @@ Download fallito Download cancellato Download completato - Stream + Flusso di rete Errore durante il caricamento dei link Archiviazione interna Doppiato @@ -122,8 +122,7 @@ Impostazioni sottotitoli lettore Sottotitoli Chromecast Impostazioni sottotitoli Chromecast - Modalità Eigengravy - Aggiungi opzione velocità nel player + Velocità di riproduzione Scorri per mandare avanti/indietro Scorri da un lato all\'altro per controllare la tua posizione in un video Scorri per cambiare le impostazioni @@ -147,7 +146,7 @@ Permessi di archiviazione mancanti. Per favore riprova. Errore nel backup %s Cerca - Accounts + Account e sicurezza Aggiornamenti e Backup Info Ricerca avanzata @@ -287,11 +286,11 @@ Avvertenza Generale Random - Mostra pulsante Random nella homepage - Lingua provider + Mostra pulsante casuale nella home page e nella libreria + Lingue estensione Layout app Media preferito - Abilita NSFW sui provider supportati + Abilita NSFW sulle estensioni supportate Encoding Sottotitoli Provider Interfaccia utente @@ -305,11 +304,11 @@ Titolo sotto il poster password123 - IlMioUsername + Nome utente hello@world.com 127.0.0.1 - IlMioSito - example.com + NuovoNomeSito + https://example.com Codice lingua (it) %1$s %2$s account @@ -391,8 +390,8 @@ Filtra in base alla lingua preferita Extra Trailer - Link allo stream - Referer + https://example.com/example.mp4 + Referente (facoltativo) Prossimo Guarda video in queste lingue Precedente @@ -424,9 +423,7 @@ Aggiornati %d plugin CloudStream non ha siti installati per impostazione predefinita. È necessario installare i siti dai repository. \n -\nA causa di una rimozione DMCA senza cervello da Sky UK Limited 🤮 non possiamo collegare il sito repository nell\'app. -\n -\nUnisciti al nostro Discord o cerca online. +\nJoin our Discord or search online. Vedi le repository della community Lista pubblica Tutti i sottotitoli in maiuscolo @@ -435,7 +432,7 @@ Tracce Traccia audio Traccia video - Applica al riavvio + Riavvia app per visualizzare le modifiche. Safe mode attiva Tutte le estensioni sono state disabilitate a causa di un arresto anomalo per aiutarti a trovare l\'estensione che causa il problema. Vedi informazioni del crash @@ -539,12 +536,12 @@ Ferma Superato Fallito - Proxy raw.githubusercontent.com + Proxy GitHub Disiscritto da %s Iscritto Iscritto a %s Impossibile raggiungere GitHub. Attivazione proxy jsDelivr… - Aggira il blocco di GitHub usando jsDelivr. Potrebbe causare un ritardo degli aggiornamenti di alcuni giorni. + Evita il blocco degli URL github non elaborati utilizzando jsDelivr. Potrebbe causare un ritardo degli aggiornamenti di alcuni giorni. Baypass ISP Ripristina Aggiornando shows a cui sei iscritto @@ -602,10 +599,42 @@ Entrato come %s Inserisci il PIN per %s Blocca profilo - Usa Account Default + Usa account predefinito Salta la selezione dell\'account all\'avvio Gestisci Accounts Modifica account Collegamenti ricaricati Ruota + Visualizza un pulsante di commutazione per l\'orientamento dello schermo + Abilita la commutazione automatica dell\'orientamento dello schermo in base all\'orientamento del video + Rotazione automatica + Cerca in altre estensioni + Mostra consigli + Aggiunge un\'opzione di velocità nel lettore + Prova tutte le estensioni + Questo test è pensato solo per gli sviluppatori e non verifica o nega il funzionamento di alcuna estensione. + Notifica nuovo episodio + Sblocca CloudStream + Blocca con biometria + Autenticazione con password/PIN + L\'autenticazione biometrica non è supportata su questo dispositivo + Sblocca app con impronta digitale, Face ID, PIN, sequenza e password. + Questa schermata è stata chiusa a causa di più tentativi falliti. Riavvia l\'app. + È stato eseguito il backup dei tuoi dati CloudStream. Sebbene questa possibilità sia molto bassa, tutti i dispositivi possono comportarsi in modo diverso. Nel raro caso in cui ti venga bloccato l\'accesso all\'app, cancella completamente i dati dell\'app e ripristina da un backup. Siamo molto spiacenti per qualsiasi inconveniente derivanti da questo. + Non preferito + %s +\nresiduo + Preferito + Nome e URL del repository + copiato! + Errore durante l\'accesso agli Appunti. Riprova. + Errore durante la copia. Copia logcat e contatta il supporto dell\'app. + OK + Disabilita ottimizzazione della batteria + Impossibile aprire le informazioni sull\'app CloudStream. + Media + Per garantire download e notifiche ininterrotti per i programmi TV sottoscritti, CloudStream necessita dell\'autorizzazione per l\'esecuzione in background. Premendo OK, verrai indirizzato alle informazioni sull\'app. Successivamente, scorri fino a \"Utilizzo della batteria\" e imposta l\'utilizzo della batteria su \"Senza restrizioni\". Tieni presente che questa autorizzazione non significa che CS3 scaricherà la batteria. Funzionerà in background solo quando necessario, ad esempio quando si ricevono notifiche o si scaricano video da estensioni ufficiali. Se scegli di annullare, puoi modificare questa impostazione più tardi in \"Impostazioni generali\". + L\'utilizzo della batteria dell\'app è già impostato su \"Senza restrizioni\" + Musica + Audiolibro diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index d5c2ad5e..da2952a0 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -271,7 +271,6 @@ ‪דפדפן צבע חלון הצג לוג - הוסף אפשרות מהירות בנגן לחץ פעמיים כדי להציץ לחץ פעמיים כדי לעצור התשתמש בבהירות המערכת בנגן האפליקציה במקום שכבת-על כהה diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 04e27c85..acb2cfc3 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -237,9 +237,9 @@ プレーヤーの字幕設定 Chromecastの字幕 Chromecastの字幕設定 - プレーヤーに速度オプションを追加します スワイプして探す 次のエピソードを自動再生する 現在のエピソードが終了したら次のエピソードを開始する 長押しするとデフォルトにリセットされます + ダウンロードを再開 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 0bf3bd9b..1a63050a 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -111,7 +111,6 @@ Chromecast 자막 Chromecast 자막 설정 배속 모드 - 플레이어에 속도 옵션을 추가합니다 스와이프하여 탐색 좌우로 스와이프하여 동영상 위치 제어하기 스와이프하여 설정 변경 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 54f9be82..f61bcfc0 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -67,7 +67,6 @@ Ištrinti Atšaukti Pradėti - Prideda greičio pasirinkti grotuve Filmukas Atsiuntimas atšauktas Išplėstinė paieška diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index ab8db2a9..49b333e3 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -124,7 +124,6 @@ Chromecast subtitri Chromecast subtitru iestāfijumi Eigengravy Mode - Pievieno atskaņošanas ātrumu playerim Novelc lai paradītu Novelc no māla lidz malai lai pozicionētu video Novēlu lai mainītu iestādījums diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 956f18e5..fe82a90b 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -90,7 +90,6 @@ Преводи Поставки на плеерот за преводи Режим на Eigengravy - Додава можност за брзина на снимка во плеерот Повлечете за да барате Повлечете од страна на страна за да ја контролирате вашата позиција во видеото Повлечете за да ги промените поставките @@ -336,7 +335,7 @@ Карактеристики Азиска драма Додатоци - Се прикажува копче на почетната страница што може да избере случаен филм или ТВ серија од почетната страница + Прикажи случајно копче на почетната страница и библиотеката Поддржано Сметки Вовед @@ -531,4 +530,65 @@ Смени провајдер Оди назад Актери: %s + %s додадени на фаворити + %s избришено од фаворити + Одбери опција за филтрирање на превземени плагини + Складиштето не е пронајдено, проверете го URL-то и пробајте VPN + Фаворити + Додадено во фаворити + Избриши од фаворити + Замени ги сите + Се чини дека потенцијално дупликат ставка веќе постои во вашата библиотека: „%s“. +\n +\nДали сепак сакате да ја додадете оваа ставка, да ја замените постојната или да го откажете дејството? + Во вашата библиотека се пронајдени потенцијални дупликати ставки: +\n +\n%s +\n +\nДали сепак сакате да ја додадете оваа ставка, да ги замените постоечките или да го откажете дејството? + Внеси ПИН за %s + ПИН-от мора да биде 4 карактери + Менаџирај кориснички сметки + Измени корисничка сметка + Логиран како %s + Прескокнете го изборот на корисничка сметка при стартување + Користете ја стандардната сметка + Ротирај + Прикажете копче за префрлување за ориентација на екранот + Веќе гласаше + Овде можете да го промените начинот на кој се подредуваат изворите. Ако видеото има повисок приоритет, ќе се појави повисоко во изборот на изворот. Збирот на приоритетот на изворот и приоритетот на квалитетот е приоритет на видеото. +\n +\nИзвор А: 3 +\nКвалитет Б: 7 +\nЌе има комбиниран приоритет на видеото од 10. +\n +\nЗАБЕЛЕШКА: Ако сумата е 10 или повеќе, играчот автоматски ќе го прескокне вчитувањето кога ќе се вчита таа врска! + Одбери корисничка сметка + Повторно вчитани линкови + Оневозможи + Внеси ПИН + Внеси моментален ПИН + ПИН + Заклучи профил + Неточен ПИН. Пробај повторно. + Исклучи претплата + Профил %d + Претплати се + Позадина на профил + Мобилен интернет + Поставете стандардно + Квалитети + Пронајдени потенцијални дупликати + Додади + Замени + Wi-Fi + Не се најдени плагини во складиштето + Користи + Измени + Профили + Помош + UI-то не можеше да се креира правилно, ова е ГОЛЕМ БАГ и треба веднаш да се пријави %s + Зачестеност на зачувување на бекап + Овозможете автоматско префрлување на ориентацијата на екранот врз основа на видео ориентација + Автоматска ротација diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 49c5b3ec..279f5511 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -3,14 +3,14 @@ വേഗം (%.2fx) റേറ്റിംഗ്: %.1f - പുതിയ അപ്ഡേറ്റ് + പുതിയ അപ്ഡേറ്റ്! \n%1$s -> %2$s - CloudStream + ക്ലൗഡ് സ്ട്രീം ഹോം തിരയുക ഡൗൺലോഡ്സ് സെറ്റിങ്‌സ് - തിരയുക + തിരയുക… ടാറ്റ ലഭ്യമല്ല കൂടുതൽ ഓപ്ഷൻസ് അടുത്ത എപ്പിസോഡ് @@ -84,8 +84,6 @@ കറുത്ത അതിർത്തി നീക്കംചെയ്യുക പ്ലേയർ സബ്‌ടൈറ്റിലുകളുടെ സെറ്റിങ്‌സ് - - വേഗം നിയന്ത്രിക്കാൻ ഓപ്ഷൻ ചേർക്കുക വീഡിയോപ്ലേയറിൽ സമയം നിയന്ത്രിക്കാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ സ്വൈപ്പുചെയ്യുക @@ -169,11 +167,11 @@ ഔചിത്യ വീഡിയോ ക്വാളിറ്റി ചരിത്രം കണ്ടതാണെന്ന് അടയാളപ്പെടുത്തുക - %1$d%2$d - yg5t4r%dujyhtg - %d മണിക്കൂർ %d മിനിറ്റ് - %1$sghj%2$d - rtf:% + %d ദിവസങ്ങൾ %d മണിക്കൂർ %d മിനിറ്റ് + അധ്യായം%dൽ റിലീസ് ചെയ്യും + %1$d മണിക്കൂർ %2$d മിനിറ്റ് + %1$sഅധ്യാ%2$d + കാസ്റ്റ്:%s അക്കൗണ്ട് ഉണ്ടാക്കുക പുറത്ത്പോകുന്നതോടുകൂടി ആപ് അപ്ഡേറ്റ് ആവുന്നതാണ് ലൈബ്രറി തിരഞ്ഞെടുക്കുക @@ -181,8 +179,8 @@ ട്രെയിലർ പ്ലേ ചെയ്യുക ലൈവ് സ്ട്രീം പ്ലേ ചെയ്യുക ഫില്ലർ - %d min - ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് കളിക്കുക + %d മിനിറ്റ് + ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് പ്രവർത്തിപ്പിക്കുക അടുത്ത ക്രമരഹിതമായ എപ്പിസോഡ് പോസ്റ്റർ അപ്ഡേറ്റ് ആരംഭിച്ചു @@ -190,7 +188,7 @@ പോസ്റ്റർ ലോഡിംഗ് ഒഴിവാക്കുക തിരയുക %s… - %dm + %dമിനിറ്റ് മടങ്ങിപ്പോവുക പശ്ചാത്തല പ്രിവ്യൂ പോസ്റ്റർ @@ -204,8 +202,6 @@ പൊതു പട്ടിക CloudStream-ന് സ്ഥിരസ്ഥിതിയായി സൈറ്റുകളൊന്നും ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. നിങ്ങൾ റിപ്പോസിറ്ററികളിൽ നിന്ന് സൈറ്റുകൾ ഇൻസ്റ്റാൾ ചെയ്യേണ്ടതുണ്ട്. \n -\nസ്‌കൈ യുകെ ലിമിറ്റഡിലെ ഡോഗ്‌ഷിറ്റ് ആളുകളിൽ നിന്ന് DMCA നീക്കം ചെയ്‌തതിനാൽ 🤮 ഞങ്ങൾക്ക് ആപ്പിൽ റിപ്പോസിറ്ററി സൈറ്റ് ലിങ്ക് ചെയ്യാൻ കഴിയില്ല. -\n \nഞങ്ങളുടെ ഡിസ്കോർഡിൽ ചേരുക അല്ലെങ്കിൽ ഓൺലൈനിൽ തിരയുക. പകർത്തുക എല്ലാ സബ്‌ടൈറ്റിലുകളും വലിയക്ഷരമാക്കുക @@ -216,7 +212,7 @@ അക്കൗണ്ടുകൾ കൈകാര്യം ചെയ്യുക ഉടൻ വരുന്നു… പുനരാരംഭിക്കുമ്പോൾ പ്രയോഗിക്കുക - അക്കൗണ്ട് എഡിറ്റ് ചെയ്യുക + അക്കൗണ്ട് തിരുത്തുക തെറ്റായ പിൻ. ദയവായി വീണ്ടും ശ്രമിക്കുക. നിർത്തുക ട്രാക്കുകൾ @@ -247,4 +243,41 @@ ഉറവിട പിശക് നിലവിലെ പിൻ നൽകുക ഓഡിയോ ട്രാക്കുകൾ + ചിത്രം-ഇൻ-ചിത്രം + പുതുക്കിയത് (പഴയത് മുതൽ പുതിയത് വരെ) + റേറ്റിംഗ് (ഉയർന്നത് മുതൽ താഴ്ന്നത്) + പാരമ്പര്യം + വിൻഡോ നിറം + ക്ലിയർ + ലോഗ് + ശുപാർശകൾ കാണിക്കുക + %s ആയി ലോഗിൻ ചെയ്തു + ഇങ്ങനെ അടുക്കുക + അടുക്കുക + തിരുത്തുക + പുതുക്കിയത് (പുതിയത് മുതൽ പഴയത് വരെ) + NSFW + ആപ്പ് അപ്ഡേറ്റ് ഇൻസ്റ്റാൾ ചെയ്യുന്നു… + അപ്ഡേറ്റുകളും ഒപ്പം ബാക്കപ്പും + %s(അപ്രാപ്തമാക്കി) + റേറ്റിംഗ് (താഴ്ന്നത് മുതൽ ഉയർന്നത് വരെ) + വാചക നിറം + ആപ്പിൻ്റെ പുതിയ പതിപ്പ് ഇൻസ്റ്റാൾ ചെയ്യാനായില്ല + പാക്കേജ് ഇൻസ്റ്റാളർ + അക്ഷരമാലാക്രമം (A മുതൽ Z വരെ) + അക്ഷരമാലാക്രമം (Z മുതൽ A വരെ) + ഈ ലിസ്റ്റ് ശൂന്യമാണ്. മറ്റൊന്നിലേക്ക് മാറാൻ ശ്രമിക്കുക. + ചരിത്രം മായ്ക്കുക + ലോഗ്കാറ്റ് കാണിക്കുക 🐈 + നിങ്ങളുടെ ലൈബ്രറി ശൂന്യമാണ് :( +\nഒരു ലൈബ്രറി അക്കൗണ്ടിൽ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ നിങ്ങളുടെ പ്രാദേശിക ലൈബ്രറിയിലേക്ക് ഷോകൾ ചേർക്കുക. + വീഡിയോ + റിപ്പോസിറ്ററി നാമവും URL ഉം + പകർത്തി! + പുതിയ എപ്പിസോഡ് അറിയിപ്പ് + മറ്റ് വിപുലീകരണങ്ങളിൽ തിരയുക + ഉപശീർഷകം ക്രമീകരണങ്ങൾ + എഡ്ജ് തരം + ഔട്ട്ലൈൻ നിറം + പശ്ചാത്തല നിറം diff --git a/app/src/main/res/values-mt/strings.xml b/app/src/main/res/values-mt/strings.xml new file mode 100644 index 00000000..b2c0356a --- /dev/null +++ b/app/src/main/res/values-mt/strings.xml @@ -0,0 +1,126 @@ + + + Preferenzi tas-sottotitli + Kulur tal-kitba + Kulur tat-Tieqa + Fittex bl-użu ta \'tipi + Importa fonts billi tpoġġihom ġo %s + Dan il-fornitur huwa torrent, VPN huwa rakkomandat + Atturi: %s + L-episodju %d ha johrog fil + %1$dh %2$dm + %dm + Kartellun + Kartellun + Kartellun tal-episodju + Kartellun Principali + Li jmiss bl\'addoċċ + Ibdel Il-fornitur + veloċità (%.2fx) + Klassifikazzjoni: %.1f + Aġġornament ġdid misjub! +\n%1$s -> %2$s + %d min + CloudStream + Ara bil-CloudStream + Dar + Fittex + Imnizzel + Preferenzi + Fittex… + Fittex%s… + Bla dejta + Iktar Preferenzi + L-episodju li\'jmiss + Ġeneri + Aqsam + Iftah fil-brawser + Brawser + Aqbez it-tagħbija + Tagħbija… + Jaraw + Stenna ftit + Lest + Imwaqqa + Pjana biex tara + Terġa\' tara + Ibda t-trejler + Ibda l-livestream + Stream Torrent + Sorsi + Erġa\' pprova l-konnessjoni… + Mur lura + Ibda l-episodju + Tniżżila ppawzata + Qed jinżlu + Imniżżel + Tniżżil ikkanċellat + Lest it-tniżżil + Beda l-aġġornament + Network stream + Tagħbija tal-Links falliet + Links regaw gew mogħbija + Ħażna Interna + Dub + Ibda + Info + Issettja l-istatus ta-rajtux + Applika + Ikkopja + Għalaq + Neħħi + Issevja + Isem tar-repożitorju u URL + Ikkupjat! + Notifika ta\' episodju ġdid + Fittex f\'estensjonijiet oħra + Uri r-rakkomandazzjonijiet + Veloċità tal-Plejer + Kulur tal-Kontorn + Kulur tal-Isfond + Tip tat-tarf + Elevazzjoni tas-Sottotitolu + Font + Daqs tal-font + Fittex bl-użu ta\' fornituri + %d Benenes mogħtija lil devs + Ebda Benenes mogħtija + Agħżel il-Lingwa Awtomatikament + Niżżel Lingwi + Lingwa tas-sottotitolu + Żomm biex tirrisettja għal default + Kompli Ara + Neħħi + Iktar informazzjoni + @string/home_play + Jista\' jkun hemm bżonn ta\' VPN biex dan il-fornitur jaħdem b\'mod korrett + Il-metadata mhix ipprovduta mis-sit, it-tagħbija tal-vidjo se tfalli jekk ma teżistix fuq is-sit. + Deskrizzjoni + Lebda Plot misjub + Lebda Deskrizzjoni misjuba + Uri Logcat 🐈 + ġurnal + Stampa f-istampa + Ikompli d-daqq fi player minjatura fuq apps oħra + %1$s Ep %2$d + %1$dd %2$dh %3$dm + Mur Lura + Ara l\'isfond + Mili + Ibda l-film + Sottotitli + Sut + Ibda l-fajl + Niżżel + Hassar il-fajl + Kompli Nizzel + Ieqaf Nizzel + Iddiżattiva r-rappurtar awtomatiku tal-bugs + Iktar Informazzjoni + Aħbi + Iffiltra l-Bookmarks + Beda t-tniżżil + Bookmarks + Neħħi + Falla t-tniżżil + diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index f60362ae..ef796f9f 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -263,7 +263,6 @@ အက်ပ်ထဲဝင်လိုက်သည့်နှင့်အက်ပ်အပ်ဒိတ်ကိုစစ်ဆေးပါ။ Chromecast စာတန်းထိုးများ Chromecast စာတန်းထိုး ပြုပြင်ရန် - ကြည့်ရှုမှုပုံစံထဲမှာအရိှန်ရွေးစရာတစ်ခုထည့်ရန် အသံအတိုးအကျယ်နှင့်အလင်းအမှောင်များကိုချိန်ညိှရန် ဘယ် သို့ ညာ ဘက်တွင် အပေါ်အောက်ဆွဲပါ ယခုကြည့်နေသောအပိုင်းပြီးပါကနောက်အပိုင်းကိုဖွင့်ပါ သင့်၏အပိုင်းကြည်ရှုမှုရောက်ရှိနေရာကိုအလိုအလျောက်သိမ်းဆည်းပါ diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 5b594334..fc537837 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -124,7 +124,6 @@ Chromecast Ondertitels Chromecast ondertitels instellingen Eigengravy Modus - Voegt een snelheidsoptie toe in de speler Swipe to seek Veeg naar links of rechts om de tijd in de videospeler te regelen Veeg om instellingen te wijzigen @@ -606,4 +605,7 @@ PIN invoeren PIN Huidige PIN invoeren + Link opnieuw geladen + Autoroteer + Roteer diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 49b559ed..724f4a63 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -98,7 +98,6 @@ Undertekster Innstillinger for spillerens teksting Eigengravy Modus - Legger til hastighetsalternativ i spilleren Sveip for å søke Sveip til venstre eller høyre for å kontrollere tiden i videospilleren Sveip for å endre innstillinger diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index cb7cf73d..c61f0104 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -52,7 +52,7 @@ Błąd przy pobieraniu Anulowano pobieranie Zakończono pobieranie - Odtwórz + Strumień sieciowy Błąd przy ładowaniu linków Pamięć wewnętrzna Dub @@ -112,18 +112,17 @@ Ustawienia napisów Napisy Chromecast Ustawienia napisów Chromecast - Tryb Eigengravy - Ustawienia prędkości - Przesuń aby przewinąć + Prędkość odtwarzania + Przesuń, aby przewinąć Przesuwaj w lewo lub prawo, aby kontrolować czas filmu - Przesuń aby zmienić ustawienia - Przesuwaj góra-dół z lewej lub prawej strony ekranu aby zmienić jasność czy głośność + Przesuń, aby zmienić ustawienia + Przesuwaj góra-dół z lewej lub prawej strony ekranu, aby zmienić jasność czy głośność Autoodtwarzanie następnego odcinka Rozpocznij następny odcinek po skończeniu bieżącego Czas przewinięcia przy podwójnym kliknięciu (w sekundach) - Podwójne kliknięcie aby przewinąć - Kliknij 2 razy z prawej lub lewej strony aby przewinąć - Kliknij dwukrotnie aby wstrzymać + Podwójne kliknięcie, aby przewinąć + Kliknij dwa razy z prawej lub lewej strony, aby przewinąć + Kliknij dwukrotnie, aby wstrzymać Kliknij dwukrotnie na środku, aby zatrzymać wideo Użyj jasności systemowej Użyj jasności systemowej w odtwarzaczu aplikacji zamiast ciemnej nakładki @@ -137,8 +136,8 @@ Brak uprawnień do pamięci, spróbuj ponownie. Błąd tworzenia kopii zapasowej %s Szukaj - Konta - Aktualizacje i kopia zapasowa + Konta i zabezpieczenia + Aktualizacje i kopie zapasowe Informacje Zaawansowane wyszukiwanie Szukaj z podziałem na źródła @@ -210,7 +209,7 @@ Torrenty Filmy dokumentalne OVA - Dramy azjatyckie + Dramaty azjatyckie Transmisje na żywo NSFW Inne @@ -220,7 +219,7 @@ Kreskówka Torrent Film dokumentalny - Drama azjatycka + Dramat azjatycki Transmisja na żywo NSFW Inne @@ -229,8 +228,8 @@ Błąd renderowania Nieoczekiwany błąd odtwarzacza Błąd pobierania, sprawdź uprawnienia aplikacji - odcinek Chromecast - mirror dla Chromecast + Odcinek Chromecast + Mirror dla Chromecast Odtwórz w aplikacji Odtwórz w %s Odtwórz w przeglądarce @@ -254,7 +253,7 @@ Pomiń tę aktualizację Aktualizacja Domyślna jakość (WiFi) - Maksymalna ilość znaków w tytule odtwarzacza + Maksymalna liczba znaków w tytule odtwarzacza Rozdzielczość odtwarzacza wideo Rozmiar bufora wideo Długość bufora wideo @@ -276,11 +275,11 @@ Zastrzeżenie Ogólne Przycisk do losowania - Pokaż przycisk do losowania na stronie głównej - Języki źródeł + Pokaż przycisk do losowania na stronie głównej i w bibliotece + Języki rozszerzeń Układ aplikacji - Preferowane media - Włącz NSFW w obsługiwanych źródłach + Preferowane multimedia + Włącz NSFW w obsługiwanych rozszerzeniach Kodowanie napisów Źródła Układ interfejsu @@ -365,14 +364,14 @@ Filtrowanie wg preferowanego języka Dodatki Zwiastun - Odsyłacz + Odsyłacz (opcjonalny) Następny Wyświetlaj filmy w tych językach Poprzedni - Pomiń setup + Pomiń konfigurację Dostosuj wygląd aplikacji do urządzenia Zgłaszanie błędów - Co chciałbyś obejrzeć + Co chcesz obejrzeć Gotowe Rozszerzenia Dodaj repozytorium @@ -397,8 +396,6 @@ Zaaktualizowano %d rozszerzeń CloudStream nie ma domyślnie zainstalowanych żadnych witryn. Musisz zainstalować witryny z repozytoriów. \n -\nZ powodu bezmyślnego usunięcia DMCA przez Sky UK Limited 🤮 nie możemy zamieścić linku do witryny z repozytoriami. -\n \nDołącz do naszego Discorda lub poszukaj online. Zobacz repozytoria społeczności Publiczna lista @@ -408,7 +405,7 @@ Ścieżki Ścieżki audio Ścieżki wideo - Zastosuj po ponownym uruchomieniu + Uruchom ponownie aplikację, aby zobaczyć zmiany. Tryb bezpieczny włączony Z powodu wystąpienia błędu wszystkie rozszerzenia zostały wyłączone, aby ułatwić wykrycie tego wadliwego. Wyświetl informacje o błędzie @@ -433,7 +430,7 @@ Wyczyść historię Historia Za dużo tekstu. Nie można skopiować do schowka. - Link do odtwarzania + https://example.com/example.mp4 Odtwórz w CloudStream Pomiń %s %1$dh %2$dm @@ -453,17 +450,17 @@ Podsumowanie Instalator APK Niektóre telefony nie obsługują nowego instalatora pakietów. Wypróbuj tryb legacy, jeśli aktualizacje nie zostaną zainstalowane. - password123 + hasło123 @string/ova - MojaFajnaWitryna - MyCoolUsername + NowaNazwaWitryny + Nazwa użytkownika 127.0.0.1 Tryb kompatybilności - przyklad.pl + https://example.com /\?\? Instalator pakietów @string/home_play - hello@world.com + witaj@poczta.pl @string/anime Opening Ending @@ -518,7 +515,7 @@ Rozpocznij Nie powiodło się Ukończone powodzeniem - Serwer pośredniczący raw.githubusercontent.com + Serwer proxy GitHuba Obejścia ISP Test dostawcy Zatrzymaj @@ -528,8 +525,8 @@ Zasubskrybowano %s Anulowano subskrypcję %s Został wydany odcinek %d! - Obchodzi blokadę GitHuba za pomocą jsDelivr. może spowodować opóźnienie aktualizacji o kilka dni. - Nie udało się połączyć z GitHub, włączono serwer pośredniczący jsDelivr… + Obchodzi blokadę surowych adresów URL GitHuba za pomocą jsDelivr. Może powodować opóźnienie aktualizacji o kilka dni. + Nie udało się połączyć z GitHubem. Włączono serwer pośredniczący jsDelivr… Domyślna jakość (dane mobilne) W tym miejscu można zmienić kolejność źródeł. Jeśli wideo ma wyższy priorytet, pojawi się wyżej w wyborze źródła. Priorytet wideo jest sumą priorytetu źródła i priorytetu jakości. \n @@ -562,7 +559,7 @@ \n%s \n \nCzy chcesz dodać ten element, zastąpić istniejące, czy anulować operację? - Wprowadź pin dla %s + Wprowadź PIN dla %s Częstotliwość tworzenia kopii zapasowych Znaleziono potencjalny duplikat Zablokuj profil @@ -592,4 +589,33 @@ Automatyczny obrót Obrót Włącz automatyczne przełączanie orientacji ekranu na podstawie orientacji filmu + Dodaje opcję prędkości w odtwarzaczu + Powiadomienie o nowym odcinku + Szukaj w innych rozszerzeniach + Pokaż rekomendacje + Przetestuj wszystkie rozszerzenia + Ten test jest przeznaczony wyłącznie dla programistów i nie weryfikuje ani nie zaprzecza działaniu żadnego rozszerzenia. + Zablokuj za pomocą biometrii + Uwierzytelnianie hasłem/kodem PIN + Ten ekran został zamknięty z powodu wielu nieudanych prób. Uruchom ponownie aplikację. + Odblokuj CloudStream + To urządzenie nie obsługuje uwierzytelniania biometrycznego + Odblokuj aplikację za pomocą odcisku palca, identyfikatora twarzy, kodu PIN, wzoru i hasła. + Kopia zapasowa Twoich danych CloudStream została teraz utworzona. Chociaż prawdopodobieństwo tego jest bardzo niskie, wszystkie urządzenia mogą zachowywać się inaczej. W rzadkich przypadkach, gdy dostęp do aplikacji zostanie zablokowany, należy całkowicie wyczyścić dane aplikacji i przywrócić je z kopii zapasowej. Bardzo nam przykro z powodu wszelkich niedogodności z tym związanych. + Usuń z ulubionych + %s +\npozostało + Dodaj do ulubionych + Nazwa repozytorium i adres URL + Błąd dostępu do schowka. Spróbuj ponownie. + skopiowano! + Błąd podczas kopiowania. Skopiuj logcat i skontaktuj się z pomocą techniczną aplikacji. + Wyłącz optymalizację akumulatora + Nie można otworzyć informacji o aplikacji CloudStream. + Muzyka + Audiobook + OK + Multimedia + Użycie akumulatora przez aplikację jest już ustawione na nieograniczone + Aby zapewnić nieprzerwane pobieranie i powiadomienia o subskrybowanych programach telewizyjnych, CloudStream potrzebuje pozwolenia na działanie w tle. Naciskając OK, zostaniesz przekierowany do informacji o aplikacji. Tam przewiń do użycia akumulatora przez aplikację i ustaw je na nieograniczone. Pamiętaj, że to pozwolenie nie oznacza, że CS3 będzie zużywać akumulator. Będzie działać w tle tylko wtedy, gdy będzie to konieczne, na przykład podczas odbierania powiadomień lub pobierania filmów z oficjalnych rozszerzeń. Jeśli zdecydujesz się anulować, możesz dostosować to ustawienie później w ustawieniach głównych. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 398a1aa3..06e2352c 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -59,7 +59,7 @@ Falha no Download Download cancelado Download concluído - Stream + Transmitir Erro a Carregar Links Armazenamento Interno Dob @@ -118,8 +118,7 @@ Configurações de legendas do player Legendas do Chromecast Configurações de legendas do Chromecast - Modo Eigengravy - Acrescenta uma opção de velocidade no player + Velocidade de reprodução Deslize para andar Deslize para os lados para controlar a posição em um vídeo Deslize para mudar as configurações @@ -143,8 +142,8 @@ Permissão de armazenamento não encontrada, por favor tente novamente. Erro no backup de %s Procurar - Contas - Atualizações e backup + Contas e segurança + Atualizações e cópias de segurança Info Procura Avançada Mostra resultados separados por fornecedor @@ -273,10 +272,10 @@ Geral Botão Aleatório Mostrar botão aleatório na página inicial - Idioma dos fornecedores + Línguas de extensão Layout da App Mídia preferida - Ativar NSFW em fornecedores compatíveis + Ativar NSFW nas Extensões suportadas Codificação das legendas Fornecedores Layout @@ -289,11 +288,11 @@ Local do título do poster Coloca o título debaixo do poster senha123 - MeuNomeFixe + Nome de utilizador ola@mundo.com 127.0.0.1 - MeuSiteFixe - examplo.com + NovoNomedoSite + https://example.com Codigo da Língua (pt) Conta Sair @@ -374,9 +373,7 @@ Transferido: %d Desativado: %d Não transferido: %d - O CloudStream não possui sites instalados por padrão. Você precisa instalar os sites a partir de repositórios. -\n -\nDevido a uma restrição sem sentido de direitos autorais (DMCA) pela Sky UK Limited 🤮 não podemos vincular o site do repositório no aplicativo. + O CloudStream não tem sites instalados por padrão. É necessário instalar os sites a partir de repositórios. \n \nJunte-se ao nosso Discord ou pesquise online. Ver repositórios da comunidade @@ -443,7 +440,7 @@ Cam Abertura Selecionar Biblioteca - Ignora o bloqueio do GitHub usando jsDelivr. Pode fazer com que as actualizações sejam atrasadas por alguns dias. + Contorna o bloqueio de URLs raw do GitHub usando jsDelivr. Pode atrasar as atualizações por uns dias. VLC Todas as linguagens Atualizado (Novo para Antigo) @@ -461,10 +458,10 @@ Inscrição cancelada em %s Final misto Avaliações (Decrescente) - Aplicar ao reiniciar - Referente + Reinicie a aplicação para ver as alterações. + Referenciador (opcional) Player oculto - Quantidade de Busca - raw.githubusercontent.com Proxy + Proxy do GitHub Blu-ray Aparência 1000 ms @@ -476,7 +473,7 @@ Ver informações sobre falha Aplicativo não encontrado Reverter - Link para transmitir + https://example.com/example.mp4 Plugins baixados %d plugins atualizados Pular %s @@ -551,4 +548,71 @@ Não foram encontrados plugins no repositório Repositório não encontrado, verifique o URL e tente a VPN Você já votou + Cancelar Inscrição + Subscrever + Favoritos + A recarregar links + Frequência de Backup + %s removido dos favoritos + Adicionar aos favoritos + Possível duplicata encontrada + %s adicionado aos favoritos + Remover dos favoritos + Substituir + Substituir Tudo + Insira o PIN atual + PIN + PIN incorreto. Por favor, tente novamente. + O PIN deve ter 4 caracteres + Editar conta + Conectado como %s + Ignorar a seleção da conta na inicialização + Girar + Digite o PIN para %s + Bloquear Perfil + Selecione uma conta + Gerenciar contas + Usar conta padrão + Potenciais itens duplicados foram encontrados na sua biblioteca: +\n +\n%s +\n +\nDeseja adicionar esse item mesmo assim, subtituir os existentes, ou cancelar a ação? + Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' +\n +\nDeseja adicionar esse item mesmo assim, subtituir o existente, ou cancelar a ação? + Mostrar recomendações + Adiciona uma opção de velocidade no leitor + Testar todas as extensões + Este teste destina-se apenas a programadores e não verifica ou nega o funcionamento de qualquer extensão. + Adicionar + Insira o PIN + Notificação de novo episódio + Procurar noutras extensões + Apresentar um botão de alternância para a orientação do ecrã + Ativar a mudança automática da orientação do ecrã com base na orientação do vídeo + Rotação automática + Nome do repositório e URL + Favorito + Desfavorito + Bloqueio com biometria + copiado! + %s +\nrestante + Erro ao aceder à área de transferência, tente novamente. + Erro ao copiar, copie o logcat e contacte o suporte da aplicação. + Desbloquear o CloudStream + Autenticação por palavra-passe/PIN + A autenticação biométrica não é suportada neste dispositivo + Desbloqueie a aplicação com impressão digital, ID facial, PIN, padrão e palavra-passe. + Este ecrã foi encerrado devido a várias tentativas falhadas. Reinicie a aplicação. + Os dados do seu CloudStream já foram copiados. Embora a possibilidade de isto acontecer ser muito baixa, todos os dispositivos podem comportar-se de forma diferente. No caso raro de ficar impedido de aceder à aplicação, limpe completamente os dados da aplicação e restaure a partir de uma cópia de segurança. Lamentamos qualquer incómodo causado por esta situação. + OK + A utilização da bateria da aplicação já está definida como sem restrições + Não é possível abrir a informação da aplicação CloudStream. + Música + Livro Aúdio + Multimédia + Desativar a otimização da bateria + Para garantir descarregamentos ininterruptos e notificações de programas de TV subscritos, o CloudStream precisa de permissão para ser executado em segundo plano. Ao premir OK, será direcionado para informações da aplicação. Aí, desloque-se para utilização da bateria da aplicação e defina a utilização da bateria para sem restrições. Tenha em atenção que esta permissão não significa que o CS3 irá esgotar a sua bateria. Este só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se optar por cancelar, pode ajustar esta definição mais tarde em definições gerais. diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index 72f16012..5de97c7d 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -82,7 +82,6 @@ ahhhaauugghh oha ooh ouuhhh oooohhahhh ouuhhh haaahhh ahoouuh - haaoooohhaaahhuoha ouuhhh ah oouuh ohoohaaahhu ohahaaaauugghh ahooo aaahhu aaaghh aaaghhohahooooo ouuhhh oouuh ooo-ahahahooo-ahah ohaaaaaghh aaaaaahhaaahhuoouuhaaaaa aahooo diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 1a084e02..d7da44b4 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -120,8 +120,7 @@ Setări de subtitrare Subtitrări Chromecast Setări pentru subtitrare Chromecast - Modul Eigengravy - Adăugați opțiunea de viteză în player + viteza de redare Derulați spre înainte/înapoi Derulați dintr-o parte în alta pentru a controla timpul de difuzare a videoclipului Derulați pentru a modifica setările @@ -273,7 +272,7 @@ Orice probleme legale privind conținutul acestei aplicații ar trebui să fie rezolvate cu furnizorii și gazdele actuale de fișiere, întrucât noi nu suntem afiliați cu aceștia. În caz de încălcare a drepturilor de autor, vă rugăm să contactați direct părțile responsabile sau site-urile de streaming. Aplicația este destinată exclusiv utilizării educaționale și personale. CloudStream 3 nu găzduiește niciun fel de conținut în aplicație și nu are niciun control asupra conținutului media care este pus sau retras. CloudStream 3 funcționează ca orice alt motor de căutare, cum ar fi Google. CloudStream 3 nu găzduiește, nu încarcă și nu gestionează niciun videoclip, film sau conținut. Pur și simplu navighează, adună și afișează linkuri într-o interfață convenabilă și ușor de utilizat. Pur și simplu, acesta extrage paginile web ale unor terțe părți care sunt accesibile publicului prin intermediul oricărui browser web obișnuit. Este responsabilitatea utilizatorului de a evita orice acțiune care ar putea încălca legile care guvernează locația sa. Utilizați CloudStream 3 pe propria răspundere. General Aleatoriu - Afișați butonul Aleatoriu pe pagina de start + Afișați butonul aleatoriu pe pagina de start și în bibliotecă Limba furnizorului Aplicație de prezentare Media preferată @@ -289,11 +288,11 @@ Locația titlului de pe poster parola123 - David Popovici + Numele utilizatorului/utilizatoarei davidpopovici@gmail.com 127.0.0.1 - David Popovici - davidpopovici.com + NouNumeSite + https://exemplu.com Cod limbă (RO) %1$s %2$s Cont @@ -309,7 +308,7 @@ %d / 10 /\?\? /%d - %s autentificat + %s autentificat/ă Nu s-a putut autentifica la %s Nu există @@ -396,11 +395,11 @@ NSFW %1$d-%2$d Player Afișat - Căutați Suma - Player Ascuns - Căutați Suma + Player Ascuns/ă - Căutați Suma Livestream-uri NSFW Eșuat - Cantitatea de căutare utilizată atunci când playerul este vizibil + Suma căutată și utilizată atunci când player-ul este vizibil/ă Livestream Cantitatea de căutare utilizată atunci când playerul este ascuns Calitatea preferată (Date Mobile) @@ -484,11 +483,9 @@ Toate extensiile au fost dezactivate din cauza unei defecțiuni pentru a vă ajuta să o găsiți pe cea care cauzează probleme. Se descarcă actualizarea aplicației… Browser web - CloudStream nu are niciun site instalat în mod implicit. Trebuie să instalați site-urile din depozite. + CloudStream nu are niciun site instalat din start. Trebuie să instalați site-urile din depozite. \n -\nDin cauza unui DMCA takedown fără creier de către Sky UK Limited 🤮 nu putem lega site-ul de depozit în aplicație. -\n -\nAlăturați-vă Discordului nostru sau căutați online. +\nAlăturați-vă Discord-ului nostru sau căutați online. A început să descarce %1$d %2$s… Mod sigur pornit Fișier Mod Sigur găsit! @@ -513,8 +510,8 @@ Repornește Activează NSFW la furnizori suportate Nu s-a putut ajunge la GitHub. Se activează proxy-ul jsDelivr… - Proxy raw.githubusercontent.com - Depășește blocarea GitHub folosind jsdelivr, poate cauza întârzieri de câteva zile la actualizări. + Proxy GitHub + Depășește blocarea GitHub folosind jsDelivr. Poate cauza întârzieri de câteva zile la actualizări. Următorul Toate %s deja descărcate S-a descărcat: %d @@ -528,7 +525,7 @@ Moştenit Test de furnizor Furnizori - Link către stream + https://example.com/example.mp4 Acest lucru va șterge, de asemenea, toate plugin-urile din depozit Se instalează actualizarea aplicației… S-a descărcat %1$d %2$s @@ -569,4 +566,31 @@ UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s Selectați modul de filtrare a descărcării plugin-urilor Ați votat deja + Elemente potențial duplicate au fost găsite în biblioteca ta: +\n +\n%s +\n +\nÎn ciuda acestui fapt, ai dori să adaugi acest alement, să le înlocuiești pe cele existente, sau să anulezi acțiunea? + %s a fost adăugat la favoriți/te + %s a fost eliminat din favoriți/te + Adaugă la favoriți/te + Elimină din favoriți/te + Se pare că un element potențial duplicat deja există în biblioteca ta: \'%s.\' +\n +\nÎn ciuda aceasta, ai dori să adaugi acest element, să îl înlocuiești pe cel existent, sau să anulezi acțiunea? + Introduce PIN-ul pentru %s + Introduce PIN-ul actual + Introduce PIN-ul + Blochează Profilul + Dezabonează-te + Abonează-te + Adaugă + Înlocuiește + Înlocuiește tot + Posibil Duplicat Găsit + Notificare episod nou + Arată sugestii + Adaugă o opțiune de viteză la player + Favoriți/te + Frecvența de backup diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 82bd3581..cf456f56 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -15,7 +15,7 @@ Отмена Все Пауза - Актёрский состав: %s + В ролях: %s Название источника Войти Нет @@ -39,7 +39,7 @@ Заполнитель CloudStream Убирать - %1$s Серия %2$d + %1$s Ep %2$d Смотреть с CloudStream Главная Поиск @@ -71,8 +71,8 @@ Скачано Скачивание Скачать остановлена - Скачать начатый - Скачать отменённый + Загрузка началась + Загрузка отменена Скачать выполнено Инфо Обновление началось @@ -99,17 +99,17 @@ Цвет фона Цвет окна Тип края - Субтитр подъём - Поиск с использованием поставщиков + Подъем субтитров + Поиск с использованием провайдеров Поиск с использованием типов - %d Бенены данность на разрабы - Бенены не дают + %d бенен(а/ов) выдано разрабам + Бенены не выданы Автовыбор языка Скачать языки Язык субтитров Удерживайте, чтобы сбросить по умолчанию Ошибка загрузки ссылок - Поток + Сетевой Поток Шрифт Размер шрифта Удалить файл @@ -132,13 +132,12 @@ Картинка в картинке Продолжение воспроизведения в миниатюрном проигрывателе поверх других приложений Кнопка изменения размера проигрывателя - Удалите черные границы + Убрать черные границы Субтитры Настройки субтитров проигрывателя Субтитры Chromecast Настройки субтитров Chromecast - Режим Eigengravy - Добавляет опцию скорости в проигрывателе + Скорость воспроизведения Проведите пальцем для поиска Проведите пальцем для изменения настроек Проведите вверх или вниз по левой или правой стороне, чтобы изменить яркость или громкость @@ -149,14 +148,14 @@ Загружена резервная копия Не удалось восстановить данные из %s Отсутствует разрешение на хранение. Пожалуйста попробуйте снова. - Аккаунты - Обновления и резервное + Аккаунты и Безопасность + Обновления и Резервное копирование Информация Расширенный поиск Показывать трейлеры - Скрыть выбранное качество видео в результатах поиска - Автоматическое обновление плагинов - Автоматическая загрузка плагинов + Скрыть выбранные форматы видео в результатах поиска + Автообновление плагинов + Автозагрузка плагинов Показать обновления приложения Автоматически проверять обновления при старте приложения. Обновится до пре-релиза @@ -197,10 +196,10 @@ Сезон Аниме приложение от тех же разработчиков Автоматически загружать еще не установленные плагины из добавленных репозиториев. - Присоединится в Discord - Бесплатно + Присоединиться к Discord-серверу + Свободно %dm - %1$d ч. %2$d мин. + %1$dч %2$dм Фильмы Мультфильм Сериалы @@ -218,7 +217,7 @@ Азиатская драма Общие Провайдеры - Макет + Расстановка Расширения Плеер Резервное копирование данных @@ -246,25 +245,25 @@ Оговорка Синхронизация субтитров Добавить клон существующего сайта с другим URL-адресом - Используется для обхода блокировок интернет провайдера + Используется для обхода блокировок интернет-провайдера Путь скачивания Давал бенен Обновить Основной цвет - Языки поставщиков + Языки провайдеров Название репозитория Очистить историю - Referer + Реферер (необязательно) Дайте бенен разрабам Ссылки - Макет - Макет приложения + Расстановка + Расстановка приложения Тема приложения Добавить репозиторий Убрать отметку Вы уверены, что хотите выйти\? Плагин скачан - Плагин удалён + Плагин удален Описание Версия Статус @@ -276,7 +275,7 @@ Пропустить %s Концовка Используйте яркость системы в проигрывателе приложения вместо темного наложения - Обновить состояние хода просмотра + Обновлять прогресс просмотра Данные сохранены Показывает результаты поиска, разделенные по провайдеру Поиск предварительных обновлений вместо полных выпусков @@ -302,7 +301,7 @@ Больше не показывать Пропустить это обновление URL сервера NGINX - Создать учётную запись + Создать аккаунт Добавить слежение Добавлено %s Синхронизировать @@ -325,7 +324,7 @@ Отчистить кеш видео и изображений Вызывает сбои, если установлено слишком высокое значение на устройствах с небольшим объемом памяти, таких как Android TV. Вызывает проблемы, если установлено слишком высокое значение на устройствах с небольшим объемом памяти, таких как Android TV. - Легкая новелла от тех же разработчиков + Легкое приложение для новелл от тех же разработчиков Язык Плейлист HLS Сначала установить расширение @@ -344,13 +343,13 @@ Эмулятор Под плакатом parol123 - МоёИмяПользователя - Сменить учётную запись - Добавить учётную запись - МойКрутойСайт - example.com + Юзернейм + Сменить аккаунт + Добавить аккаунт + ИмяСайта + https://example.com Код языка (ru) - учётная запись + аккаунт Автоматически 127.0.0.1 Обновления приложения @@ -423,9 +422,6 @@ %s (отключено) Далее В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев. -\n -\nИз-за безмозглой жалобы DMCA от Sky UK Limited 🤮 мы не можем привязать сайт репозитория в приложении. -\n \nПрисоединяйтесь к нашему Discord-серверу или найдите в интернете. Недопустимые данные Разрешение и название @@ -461,7 +457,7 @@ Загрузить из интернета Загрузка обновления приложения… Недопустимый URL - Применить при перезапуске + Перезапустите приложение, чтобы увидеть изменения. Отчеты ошибках Что вы хотите увидеть Смотрите видео на этих языках @@ -469,8 +465,8 @@ Изображение постера Пакетная загрузка Скачайте список сайтов, который вы хотите использовать - Отображать Аниме с Дубляжом/Субтитрами - Включить NSFW на поддерживаемых провайдерах + Отображать аниме с дубляжом/субтитрами + Включить NSFW у поддерживаемых расширений (провайдеров) Удалять скрытые субтитры из субтитров Дополнительно Изменить вид интерфейса, чтобы соответствовать устройству @@ -491,11 +487,11 @@ Показывать всплывающие окна для пропуска вступления/заключения Фильтровать по предпочитаемому языку медиа Неверный ID - Ссылка на стрим - Отображать рандомную кнопку на Главной странице + https://example.com/example.mp4 + Отображать рандомную кнопку в библиотеке и главной странице Рандомная кнопка Legacy (старый) - Веб видеокаст + Web Video Cast Не отправляет данные Перезагрузить ссылки Предпочтительные медиа @@ -520,12 +516,12 @@ Вернуться Подписался на %s Предпочтительное качество видео (Мобильный интернет) - raw.githubusercontent.com Прокси-сервер - Не удалось подключиться к GitHub. Включаем проксирование через jsdelivr… + GitHub прокси + Не удалось подключиться к GitHub. Включаем проксирование через jsDelivr… Эпизод %d выпущен! Обходы провайдера Обновление подписки на фильмы и сериалы - Обход ограничения доступа к GitHub с помощью jsDelivr может задержать обновления на несколько дней. + Обход ограничения доступа к raw github URLs с помощью jsDelivr. Обновления могут задержаться на несколько дней. Подписные Отказались от подписки на %s Мобильный интернет @@ -552,4 +548,72 @@ \n \nПРИМЕЧАНИЕ. Если сумма равна 10 или более, плеер автоматически пропустит загрузку при загрузке этой ссылки! Ссылки перезагружены + Выбрать аккаунт + %s убран из избранных + Введите пин-код + Защитить профиль + Пин-код + Найден возможный дубликат + Добавить + Неверный пин-код. Попробуйте снова. + Пин-код должен состоять из 4 цифр + Изменить аккаунт + Управление аккаунтами + Вы вошли как %s + Пропускать выбор аккаунта при запуске + Повернуть + Показывать переключатель ориентации экрана + Заменить все + Потенциальные дубликаты элементов были найдены в вашей библиотеке: +\n +\n\'%s.\' +\n +\nВы хотите добавить этот элемент, заменить существующие или отменить это действие? + Убрать из избранных + Добавить в избранное + Включить автоматическую смену ориентации экрана на основе ориентации видео + Автоповорот + rotate_video_key + Использовать аккаунт по умолчанию + Отписаться + Заменить + Введите текущий пин-код + auto_rotate_video_key + Избранное + %s добавлен в избранное + Введите пин-код от %s + Подписаться + Частота резервного копирования + Похоже, что потенциальный дубликат элемента уже существует в вашей библиотеке: \'%s.\' +\n +\nВы хотите добавить этот элемент, заменить существующий или отменить это действие? + Оповещение о выходе нового эпизода + Искать в других расширениях + Показать рекомендации + Этот тест предназначен только для разработчиков и не подтверждает или не опровергает работоспособность провайдеров. + Добавление настроек скорости в плеер + Протестировать всех провайдеров + скопировано! + ОК + Имя репозитория и URL адрес + Ошибка доступа к буферу обмена, пожалуйста, попробуйте ещё раз + Ошибка при копировании, пожалуйста, скопируйте лог и свяжитесь с технической поддержкой. + Нелюбимое + Разблокировать CloudStream + Любимое + Использование батареи приложением уже настроено на неограниченное + Не удается открыть информацию о приложении CloudStream. + Заблокировать биометрией + Музыка + Аудиокнига + Медиа + Разблокируйте приложение с помощью отпечатка пальца, Face ID, PIN-кода, шаблона и пароля. + %s +\nосталось + Отключить оптимизацию батареи + Аутентификация по паролю/PIN-коду + Биометрическая аутентификация на этом устройстве не поддерживается + Этот экран был закрыт из-за нескольких неудачных попыток. Пожалуйста, перезапустите приложение. + Ваши данные в CloudStream были скопированы. Хотя вероятность этого очень мала, все устройства могут вести себя по-разному. В редких случаях, когда доступ к приложению заблокирован, полностью удалите данные приложения и восстановите их из резервной копии. Мы приносим свои извинения за любые неудобства, связанные с этим. + Чтобы обеспечить бесперебойную загрузку и получение уведомлений о телепередачах, на которые вы подписаны, CloudStream необходимо разрешение на запуск в фоновом режиме. Нажав OK, вы перейдете к информации о приложении. Там перейдите к разделу 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и установите значение \"Использование батареи\" 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Пожалуйста, обратите внимание, что это разрешение не означает, что CS3 разрядит вашу батарею. Он будет работать в фоновом режиме только при необходимости, например, при получении уведомлений или загрузке видео с официальных расширений. Если вы решите отменить, вы можете изменить эту настройку позже в 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 490f74ac..ebaaa2ae 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -149,7 +149,6 @@ Použiť systémový jas Obnoviť dáta zo zálohy Dvojitým ťuknutím pretočiť - Pridá možnosť rýchlosti do prehrávača Automaticky sťahovať doplnky Pripojte sa na Discord Neodosiela žiadne dáta diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index a63d44fe..7b0d2870 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -250,7 +250,6 @@ Qoraal-hoosaadka Habaynta Qrl-hoosaadka Koromakaastiga Habka Xawaare-kordhinta - Waxaad kordhin karta xawaaraha aad ku daawanayso Midig iyo bidix u jiid si aad marba dhinac ugu dhaafiso muqaalka Laba jeer taabo midig ama bidix si aad u dhaafiso ama ku ceshato Laba jeer taabo si aad u dhaafiso diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 0f6f37cd..76508c43 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -83,8 +83,7 @@ Ta bort de svarta kanterna Undertexter Inställningar för undertexter - Eigengrau Läge - Lägger till hastighetsalternativet i spelaren + Uppspelningshastighet Svep för att strya tiden Svep åt vänster eller höger för att styra tiden i videospelaren Svep för att ändra inställningar @@ -138,7 +137,7 @@ Inga undertexter Standard Tillgängligt - Använtt + Använt App Filmer Tv Serier @@ -180,9 +179,9 @@ Använd system ljusstyrka Använder systemets ljusstyrka instället för en svart överlaga Visa filler avsnitt för anime - Visa Dubbad/Subbad anime - Anpassad till skärmstorlek - Leverantörspråk + Visa dubbad/undertextad anime + Anpassa till skärmstorlek + Tilläggsspråk Utsträckt Inzoomad Oförväntat uppspelingsfel @@ -205,7 +204,7 @@ Logga ut konto Nerladdningsplats - Kollar om + Tittar på nytt Automatisk DNS över HTTPS " " @@ -217,7 +216,7 @@ Chromecast-undertexter Dubbeltryck i mitten för att pausa Återställ data från backup - Konton + Konton och säkerhet Uppdateringar och backup Automatiska pluginuppdateringar %1$dd %2$dh %3$dm @@ -294,7 +293,7 @@ UHD Upplösning och titel Fel - Referer + Referent (valfritt) Nästa 1000 ms Lägga till en klon av en befintlig webbplats med en annan webbadress @@ -327,7 +326,7 @@ Trailer Applayout Funktioner - exempel.se + https://example.com Språkkod (en) Videocache på disken Rensa video- och bildcache @@ -348,7 +347,7 @@ Cache Säkerhetskopiering Undertexter - Aktivera NSFW på sidor som stöds + Aktivera NSFW på tillägg som stöds Undertextkodning lösenord123 Fördröjning av undertexter @@ -367,7 +366,7 @@ Titta på videor på dessa språk Föregående Spår - Uppdaterar + Uppdatering påbörjad Logg Videospelarens hoppsträcka (Sekunder) Ändra status @@ -379,12 +378,12 @@ Slumpknapp Visa sub Kunde inte öppna appen - raw.githubusercontent.com Proxy + GitHub Proxy Webbläsarens videospelare Installerar uppdatering till appen… Kunde inte nå GitHub, sätter på jsDelivr proxy… Leverantörer - MinTrevligaWebbsida + Nytt webbplatsnamn Ta bort reklam från undertexter VLC Alla språk @@ -432,7 +431,7 @@ Profiler Hjälp Kvalitet - Ström + Nätverks Stream Databasens namn All %s har redan laddats ner Ladda ner alla tillägg från den här databasen? @@ -480,7 +479,7 @@ Loggat in som %s Hoppa över val av konto vid start auto_rotera_video_nyckel - Förbi passera blockering av GitHub genom att använda jsDelivr. Kan göra att uppdateringar försenas med några dagar. + Gå förbi blockering av rå GitHub-URL:er med jsDelivr. Kan göra att uppdateringar försenas med några dagar. Funktion Önskad media Lägg titeln under affischen @@ -499,9 +498,7 @@ Visa community databaser Blandad inledning Skippa %s - CloudStream har inga webbplatser installerade som standard. Du måste installera webbplatserna från databaser. -\n -\nPå grund av en hjärnlös DMCA-borttagning av Sky UK Limited kan vi inte länka databaser på denna applikation. + CloudStream har inga webbplatser installerade som standard. Du måste installera webbplatser från arkiv. \n \nGå med i vår Discord eller sök online. Välj bibliotek @@ -551,7 +548,7 @@ Gester %s autentiserad Statister - Länka till strömmen + https://example.com/example.mp4 Radera databasen Profil bakgrund WP @@ -591,5 +588,20 @@ \nKommer att ha en kombinerad videoprioritet på 10. \n \nOBS: Om summan är 10 eller mer kommer spelaren automatiskt att hoppa över laddningen när den länken laddas! - Titel kopierad! + Meddelande om nytt avsnitt + Sök i andra tillägg + Visa rekommendationer + Lägger till ett hastighetsalternativ i spelaren + Testa alla tillägg + Detta test är endast avsett för utvecklare och verifierar eller förnekar inte att någon tillägg fungerar. + Lås med biometrik + Lösenord/PIN autentisering + Lås upp appen med Fingerprint, Face ID, PIN, mönster eller lösenord. + Lås upp CloudStream + Biometrisk autentisering stöds inte på den här enheten + Detta fönster stängs efter några misslyckade försök. Du måste starta om appen. + Favorit + Ta bort från favoriter + %s +\nkvarstår diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 234207b9..e981d05a 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -99,7 +99,6 @@ Logcat 🐈 காட்டு பிற பயன்பாடுகளுக்கு மேல் மினியேச்சர் பிளேயரில் பிளேபேக் தொடர்கிறது வசன வரிகள் - பிளேயரில் வேக விருப்பத்தை சேர்க்க வீடியோ பிளேயரில் நேரத்தைக் கட்டுப்படுத்த இடது அல்லது வலதுபுறம் ஸ்வைப் செய்யவும் பிரகாசம் அல்லது ஒலியளவை மாற்ற இடது அல்லது வலது பக்கத்தில் ஸ்வைப் செய்யவும் இடைநிறுத்துவதற்கு இருமுறை தட்டவும் diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index fc3946bb..b4308eb7 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -101,7 +101,6 @@ Subtitles Player subtitles settings Eigengravy Mode - Magdagdag ng \'speed option\' sa \'player\' Swipe to seek Swipe pakanan o pakaliwa upang makontrol ang oras ng pinapanood Swipe to change settings diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e8b7881a..c3e5959a 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -76,7 +76,7 @@ İndirme iptal edildi İndirme bitti %s - %s - Yayınla + Ağ akışı Bağlantılar yüklenirken hata oluştu Dahili depolama Dublajlı @@ -137,8 +137,7 @@ Oynatıcı alt yazı ayarları Chromecast alt yazıları Chromecast alt yazı ayarları - Eigengrau modu - Oynatıcıya hız seçeneği ekler + Oynatma hızı Atlamak için kaydır Zamanı ayarlamak için yanlardan kaydır Ayarları değiştirmek için kaydır @@ -162,8 +161,8 @@ Depolama izinleri eksik. Lütfen tekrar deneyin. %s yedeklenirken hata Ara - Hesaplar - Güncellemeler ve yedekleme + Hesaplar ve Güvenlik + Güncellemeler ve Yedekleme Bilgi Gelişmiş arama Arama sonuçlarını sağlayıcıya göre ayırır @@ -309,10 +308,10 @@ Genel Rastgele İçerik Anasayfada ve Kütüphanede rastgele düğmesini göster - Sağlayıcı dilleri + Uzantı dilleri Uygulama düzeni Tercih edilen medya - Desteklenen sağlayıcılarda +18 içeriği etkinleştir + Desteklenen Uzantılarda NSFW\'yi etkinleştirin Alt yazı kodlaması Sağlayıcılar Düzen @@ -330,11 +329,11 @@ opensubtitles_key nginx_key şifre123 - HavalıKullanıcıAdı + Kullanıcı Adı hello@world.com 127.0.0.1 - MyCoolSite - ornek.com + YeniSiteAdı + https://ornek.com Dil kodu (tr) ?attr/colorPrimary - @android:dimen/dialog_min_width_major - @android:dimen/dialog_min_width_minor + @dimen/abc_dialog_min_width_major + @dimen/abc_dialog_min_width_minor @drawable/dialog__window_background diff --git a/app/src/main/res/xml/settings_general.xml b/app/src/main/res/xml/settings_general.xml index c4900bca..cdda6d85 100644 --- a/app/src/main/res/xml/settings_general.xml +++ b/app/src/main/res/xml/settings_general.xml @@ -6,10 +6,7 @@ android:title="@string/app_language" android:icon="@drawable/ic_baseline_language_24" /> - + + android:title="@string/title_downloads"> + + + + + + + + ("clean") { - delete(rootProject.layout.buildDirectory) -} +//tasks.register("clean") { +// delete(rootProject.layout.buildDirectory) +//} diff --git a/fastlane/metadata/android/af/short_description.txt b/fastlane/metadata/android/af/short_description.txt index 59cfb7e5..aaba87f3 100644 --- a/fastlane/metadata/android/af/short_description.txt +++ b/fastlane/metadata/android/af/short_description.txt @@ -1 +1 @@ -Laai af of stroom flieks, TV-reekse en anime. +Laai af en stroom flieks, TV-reekse en anime. diff --git a/fastlane/metadata/android/hi-IN/full_description.txt b/fastlane/metadata/android/hi-IN/full_description.txt index 89927810..465db20e 100644 --- a/fastlane/metadata/android/hi-IN/full_description.txt +++ b/fastlane/metadata/android/hi-IN/full_description.txt @@ -1,10 +1,10 @@ -क्लाउडस्ट्रीम-3 आपको मूवी, टीवी-सीरीज़ और एनीमे स्ट्रीम और डाउनलोड करने की सुविधा देता है। +क्लाउडस्ट्रीम-3 आपको फ़िल्में, टीवी शृंखलाएँ और एनिमे स्ट्रीम एवं डाउनलोड करने की सुविधा देता है। -ऐप बिना किसी विज्ञापन और एनालिटिक्स के आता है और -कई ट्रेलर और मूवी साइटों और अन्य का समर्थन करता है जैसे कि- +ऐप विज्ञापन और विश्लेषिकी से मुक्त है एवं +अनेकों ट्रेलर और मूवी साइटों के समर्थन जैसी सुविधाएँ देता है, जैसे कि – -बुकमार्क +पृष्ठचिह्न उपशीर्षक डाउनलोड -क्रोमकास्ट का समर्थन +क्रोमकास्ट समर्थन diff --git a/fastlane/metadata/android/hi-IN/short_description.txt b/fastlane/metadata/android/hi-IN/short_description.txt index a8a1eb4d..5a0f2d12 100644 --- a/fastlane/metadata/android/hi-IN/short_description.txt +++ b/fastlane/metadata/android/hi-IN/short_description.txt @@ -1 +1 @@ -फिल्में, टीवी-सीरीज़ और एनीमे स्ट्रीम करें और डाउनलोड करें। +फ़िल्में, टीवी धारावाहिक और एनिमे स्ट्रीम और डाउनलोड करें। diff --git a/fastlane/metadata/android/hu-HU/changelogs/2.txt b/fastlane/metadata/android/hu-HU/changelogs/2.txt new file mode 100644 index 00000000..012882d2 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/2.txt @@ -0,0 +1 @@ +- Változáslista hozzáadva! diff --git a/fastlane/metadata/android/hu-HU/full_description.txt b/fastlane/metadata/android/hu-HU/full_description.txt new file mode 100644 index 00000000..24deb97a --- /dev/null +++ b/fastlane/metadata/android/hu-HU/full_description.txt @@ -0,0 +1,10 @@ +A CloudStream-3 segítségével streamelhet vagy letölthet filmeket, TV sorozatokat vagy animéket.. + +Az app nem tartalmaz semmilyen reklámot vagy követést, +és támogat többféle film és előzetes oldalt, és sok minden mást, pl. + +Könyvjelzőket + +Felirat Letöltést + +Chromecast támogatást diff --git a/fastlane/metadata/android/hu-HU/short_description.txt b/fastlane/metadata/android/hu-HU/short_description.txt new file mode 100644 index 00000000..2e7e6127 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/short_description.txt @@ -0,0 +1 @@ +Streameljen vagy töltsön le filmeket, TV sorozatokat vagy animét. diff --git a/fastlane/metadata/android/hu-HU/title.txt b/fastlane/metadata/android/hu-HU/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/id/changelogs/2.txt b/fastlane/metadata/android/id/changelogs/2.txt new file mode 100644 index 00000000..677af86c --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/2.txt @@ -0,0 +1 @@ +- Log perubahan ditambahkan! diff --git a/fastlane/metadata/android/id/full_description.txt b/fastlane/metadata/android/id/full_description.txt new file mode 100644 index 00000000..fde69775 --- /dev/null +++ b/fastlane/metadata/android/id/full_description.txt @@ -0,0 +1,12 @@ +CloudStream-3 memudahkan Anda streaming dan mengunduh Film, Seri TV, dan Anime. + + +Aplikasi ini hadir tanpa iklan dan pelacak analitik dan +mendukung beberapa situs trailer & film, dan +masih banyak lagi, misalnya + +Bookmark + +Mengunduh subtitle + +Dukungan Chromecast diff --git a/fastlane/metadata/android/id/short_description.txt b/fastlane/metadata/android/id/short_description.txt new file mode 100644 index 00000000..d7a9ebe4 --- /dev/null +++ b/fastlane/metadata/android/id/short_description.txt @@ -0,0 +1 @@ +Stream dan unduh film, seri TV, dan anime. diff --git a/fastlane/metadata/android/id/title.txt b/fastlane/metadata/android/id/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/id/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/ko-KR/changelogs/2.txt b/fastlane/metadata/android/ko-KR/changelogs/2.txt new file mode 100644 index 00000000..f4c05b14 --- /dev/null +++ b/fastlane/metadata/android/ko-KR/changelogs/2.txt @@ -0,0 +1 @@ +- 변경기록이 추가됨! diff --git a/fastlane/metadata/android/ko-KR/full_description.txt b/fastlane/metadata/android/ko-KR/full_description.txt new file mode 100644 index 00000000..542c1ff7 --- /dev/null +++ b/fastlane/metadata/android/ko-KR/full_description.txt @@ -0,0 +1,10 @@ +클라우트스트림-3는 영화, TV-연속극 및 애니메이션 스트리밍을 할 수 있고 내려받을 수 있습니다. + +이 앱은 광고나 분석 없이 제공되고 +여러 예고편 & 영화 사이트 등을 지원합니다. + +북마크 + +자막 내려받기 + +크롬캐스트 지원 diff --git a/fastlane/metadata/android/ko-KR/short_description.txt b/fastlane/metadata/android/ko-KR/short_description.txt new file mode 100644 index 00000000..e85980b1 --- /dev/null +++ b/fastlane/metadata/android/ko-KR/short_description.txt @@ -0,0 +1 @@ +영화, TV 시리즈 및 애니메이션 스트림과 내려받기. diff --git a/fastlane/metadata/android/ko-KR/title.txt b/fastlane/metadata/android/ko-KR/title.txt new file mode 100644 index 00000000..a199a665 --- /dev/null +++ b/fastlane/metadata/android/ko-KR/title.txt @@ -0,0 +1 @@ +클라우드스티림 diff --git a/fastlane/metadata/android/mk-MK/changelogs/2.txt b/fastlane/metadata/android/mk-MK/changelogs/2.txt index 949f6579..ee4f49cc 100644 --- a/fastlane/metadata/android/mk-MK/changelogs/2.txt +++ b/fastlane/metadata/android/mk-MK/changelogs/2.txt @@ -1 +1 @@ -- Дневник на промени е додаден! +- Дневникот на промени е додаден! diff --git a/fastlane/metadata/android/mk-MK/full_description.txt b/fastlane/metadata/android/mk-MK/full_description.txt index cb06980e..b49c1683 100644 --- a/fastlane/metadata/android/mk-MK/full_description.txt +++ b/fastlane/metadata/android/mk-MK/full_description.txt @@ -1,8 +1,8 @@ CloudStream-3 ви дозволува да гледате и превземате филмови, телевизиски серии и аниме. Апликацијата нема реклами и аналитика. Таа поддржува повеќе страници за трејлери, филмови и многу повеќе. Апликацијата вклучува: - + Обележувачи (Bookmarks) - + Превземање на преводи - + Поддршка за Chromecast diff --git a/fastlane/metadata/android/ml-IN/changelogs/2.txt b/fastlane/metadata/android/ml-IN/changelogs/2.txt new file mode 100644 index 00000000..f7523831 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/changelogs/2.txt @@ -0,0 +1 @@ +-ചേഞ്ച്ലോഗ് ചേർത്തു! diff --git a/fastlane/metadata/android/ml-IN/full_description.txt b/fastlane/metadata/android/ml-IN/full_description.txt new file mode 100644 index 00000000..218f9f98 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/full_description.txt @@ -0,0 +1,10 @@ +ക്ലൗഡ് സ്ട്രീം-3 സിനിമകൾ, ടിവി സീരീസ്, ആനിമേഷൻ എന്നിവ സ്ട്രീം ചെയ്യാനും ഡൗൺലോഡ് ചെയ്യാനും നിങ്ങളെ അനുവദിക്കുന്നു. + +പരസ്യങ്ങളും അനലിറ്റിക്‌സും കൂടാതെ ആപ്പ് വരുന്നു ഒപ്പം +ഒന്നിലധികം ട്രെയിലർ, മൂവി സൈറ്റുകൾ എന്നിവയും മറ്റും പിന്തുണയ്ക്കുന്നു, ഉദാഹരണം + +ബുക്ക്മാർക്കുകൾ + +ഉപശീർഷകം ഡൗൺലോഡുകൾ + +ക്രോംകാസ്റ്റ് പിന്തുണ diff --git a/fastlane/metadata/android/ml-IN/short_description.txt b/fastlane/metadata/android/ml-IN/short_description.txt new file mode 100644 index 00000000..f12fe5b5 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/short_description.txt @@ -0,0 +1 @@ +സ്ട്രീം ഒപ്പം ഡൗൺലോഡ് സിനിമകളും, ടിവി സീരീസുകളും, ആനിമേഷനും . diff --git a/fastlane/metadata/android/ml-IN/title.txt b/fastlane/metadata/android/ml-IN/title.txt new file mode 100644 index 00000000..8e89348a --- /dev/null +++ b/fastlane/metadata/android/ml-IN/title.txt @@ -0,0 +1 @@ +ക്ലൗഡ് സ്ട്രീം diff --git a/fastlane/metadata/android/mt/changelogs/2.txt b/fastlane/metadata/android/mt/changelogs/2.txt new file mode 100644 index 00000000..66bbca8f --- /dev/null +++ b/fastlane/metadata/android/mt/changelogs/2.txt @@ -0,0 +1 @@ +- Changelog miżjud! diff --git a/fastlane/metadata/android/mt/full_description.txt b/fastlane/metadata/android/mt/full_description.txt new file mode 100644 index 00000000..da507aae --- /dev/null +++ b/fastlane/metadata/android/mt/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 iħallik tistrimja u tniżżel Films, Serje TV u Anime. + +L-app tiġi mingħajr reklami u analytics u +jappoġġja siti multipli ta' trejlers u films, u aktar, eż. + +Bookmarks + +Downloads tas-sottotitli + +Appoġġ tal-Chromecast diff --git a/fastlane/metadata/android/mt/short_description.txt b/fastlane/metadata/android/mt/short_description.txt new file mode 100644 index 00000000..542b8614 --- /dev/null +++ b/fastlane/metadata/android/mt/short_description.txt @@ -0,0 +1 @@ +Tistrimja u tniżżel films, serje tat-TV u Anime. diff --git a/fastlane/metadata/android/mt/title.txt b/fastlane/metadata/android/mt/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/mt/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/pl-PL/changelogs/2.txt b/fastlane/metadata/android/pl-PL/changelogs/2.txt new file mode 100644 index 00000000..e558535d --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/2.txt @@ -0,0 +1 @@ +- Dodano dziennik zmian! diff --git a/fastlane/metadata/android/pl-PL/full_description.txt b/fastlane/metadata/android/pl-PL/full_description.txt new file mode 100644 index 00000000..11f71ff7 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 umożliwia strumieniowe przesyłanie i pobieranie filmów, seriali telewizyjnych i anime. + +Aplikacja jest dostarczana bez reklam i analityki, obsługuje +wiele witryn ze zwiastunami, filmami i nie tylko, np. + +Zakładki + +Pobieranie napisów + +Obsługa Chromecasta diff --git a/fastlane/metadata/android/pt-BR/changelogs/2.txt b/fastlane/metadata/android/pt-BR/changelogs/2.txt new file mode 100644 index 00000000..c094fe97 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/2.txt @@ -0,0 +1 @@ +- Histórico de mudanças adicionado! diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt new file mode 100644 index 00000000..1406838e --- /dev/null +++ b/fastlane/metadata/android/pt-BR/full_description.txt @@ -0,0 +1,10 @@ +O CloudStream-3 permite que você faça transmissões, download de filmes, séries, e anime. + +O aplicativo não contém anúncios ou ferramentas de análise, +e suporta múltiplos sites de filmes e trailers, e muito mais, como: + +Favoritos + +Download de legendas + +Suporte à Chromecast diff --git a/fastlane/metadata/android/pt-BR/short_description.txt b/fastlane/metadata/android/pt-BR/short_description.txt new file mode 100644 index 00000000..46635de9 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/short_description.txt @@ -0,0 +1 @@ +Faça transmissões, download de filmes, séries, e anime. diff --git a/fastlane/metadata/android/pt-BR/title.txt b/fastlane/metadata/android/pt-BR/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/ru-RU/changelogs/2.txt b/fastlane/metadata/android/ru-RU/changelogs/2.txt new file mode 100644 index 00000000..4b9464b6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/2.txt @@ -0,0 +1 @@ +- Добавлен список изменений! diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt new file mode 100644 index 00000000..1790888e --- /dev/null +++ b/fastlane/metadata/android/ru-RU/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 позволяет транслировать и скачивать фильмы, сериалы и аниме. + +Приложение поставляется без рекламы и аналитики и +поддерживает множество сайтов с трейлерами и фильмами, а также многое другое, например + +Книжные закладки + +Загрузка субтитров + +Поддержка Chromecast diff --git a/fastlane/metadata/android/ru-RU/short_description.txt b/fastlane/metadata/android/ru-RU/short_description.txt new file mode 100644 index 00000000..a43bc8a1 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/short_description.txt @@ -0,0 +1 @@ +Транслируйте и скачивайте фильмы, сериалы и аниме. diff --git a/fastlane/metadata/android/ru-RU/title.txt b/fastlane/metadata/android/ru-RU/title.txt new file mode 100644 index 00000000..3c0406a6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/title.txt @@ -0,0 +1 @@ +Облачный поток diff --git a/fastlane/metadata/android/vi/title.txt b/fastlane/metadata/android/vi/title.txt index dde89d58..0afff90c 100644 --- a/fastlane/metadata/android/vi/title.txt +++ b/fastlane/metadata/android/vi/title.txt @@ -1 +1 @@ -CloudStream +double_tap_seek_time_key2 diff --git a/fastlane/metadata/android/zh-CN/changelogs/2.txt b/fastlane/metadata/android/zh-CN/changelogs/2.txt index 5512f16c..c8c4624d 100644 --- a/fastlane/metadata/android/zh-CN/changelogs/2.txt +++ b/fastlane/metadata/android/zh-CN/changelogs/2.txt @@ -1 +1 @@ -- 添加了更新日志! +- 新增更新日志! diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt index 56519df6..b2dcf1de 100644 --- a/fastlane/metadata/android/zh-CN/full_description.txt +++ b/fastlane/metadata/android/zh-CN/full_description.txt @@ -1,9 +1,10 @@ -CloudStream-3可以让你串流和下载电影、剧集和动漫。这款应用没有任何广告和分析。它支持多个预告片和电影网站等。特点包括: +CloudStream-3可以让你串流和下载电影、剧集和动漫。 + +这款应用没有任何广告和隐私分析并且 +它支持多个预告片和电影网站等。特点包括: 书签 -下载和串流电影、电视节目和动漫 - 下载字幕 支持投屏 diff --git a/library/build.gradle.kts b/library/build.gradle.kts new file mode 100644 index 00000000..42a8c943 --- /dev/null +++ b/library/build.gradle.kts @@ -0,0 +1,68 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec + +plugins { + kotlin("multiplatform") + id("maven-publish") + id("com.android.library") + id("com.codingfeline.buildkonfig") +} + +kotlin { + version = "1.0.0" + androidTarget() + jvm() + + sourceSets { + commonMain.dependencies { + implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser + ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API + Level 25 or Less. */ + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + } + } +} + +repositories { + mavenLocal() + maven("https://jitpack.io") +} + +tasks.withType { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +buildkonfig { + packageName = "com.lagradost.api" + exposeObjectWithName = "BuildConfig" + + defaultConfigs { + val isDebug = kotlin.runCatching { extra.get("isDebug") }.getOrNull() == true + buildConfigField(FieldSpec.Type.BOOLEAN, "DEBUG", isDebug.toString()) + } +} + +android { + compileSdk = 34 + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + + defaultConfig { + minSdk = 21 + targetSdk = 33 + } + + // If this is the same com.lagradost.cloudstream3.R stops working + namespace = "com.lagradost.api" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} +publishing { + publications { + withType { + groupId = "com.lagradost.api" + } + } +} \ No newline at end of file diff --git a/library/src/androidMain/AndroidManifest.xml b/library/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..568741e5 --- /dev/null +++ b/library/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/library/src/androidMain/kotlin/com/lagradost/api/Log.kt b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..12524411 --- /dev/null +++ b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,21 @@ +package com.lagradost.api + +import android.util.Log + +actual object Log { + actual fun d(tag: String, message: String) { + Log.d(tag, message) + } + + actual fun i(tag: String, message: String) { + Log.i(tag, message) + } + + actual fun w(tag: String, message: String) { + Log.w(tag, message) + } + + actual fun e(tag: String, message: String) { + Log.e(tag, message) + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/api/Log.kt b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..4b8e6329 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,8 @@ +package com.lagradost.api + +expect object Log { + fun d(tag: String, message: String) + fun i(tag: String, message: String) + fun w(tag: String, message: String) + fun e(tag: String, message: String) +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt new file mode 100644 index 00000000..87ee4815 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt @@ -0,0 +1,3 @@ +package com.lagradost.cloudstream3 + +class ErrorLoadingException(message: String? = null) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt similarity index 86% rename from app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index 817d7db3..d3b4999a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -1,10 +1,7 @@ package com.lagradost.cloudstream3.mvvm -import android.util.Log -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import com.bumptech.glide.load.HttpException -import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.api.BuildConfig +import com.lagradost.api.Log import com.lagradost.cloudstream3.ErrorLoadingException import kotlinx.coroutines.* import java.io.InterruptedIOException @@ -49,18 +46,6 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) { } } -/** NOTE: Only one observer at a time per value */ -fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { - liveData.removeObservers(this) - liveData.observe(this) { it?.let { t -> action(t) } } -} - -/** NOTE: Only one observer at a time per value */ -fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { - liveData.removeObservers(this) - liveData.observe(this) { action(it) } -} - sealed class Resource { data class Success(val value: T) : Resource() data class Failure( @@ -158,14 +143,14 @@ fun throwAbleToResource( "Connection Timeout\nPlease try again later." ) } - is HttpException -> { - Resource.Failure( - false, - throwable.statusCode, - null, - throwable.message ?: "HttpException" - ) - } +// is HttpException -> { +// Resource.Failure( +// false, +// throwable.statusCode, +// null, +// throwable.message ?: "HttpException" +// ) +// } is UnknownHostException -> { Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}") } diff --git a/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..e9a0e6b4 --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,19 @@ +package com.lagradost.api + +actual object Log { + actual fun d(tag: String, message: String) { + println("DEBUG $tag: $message") + } + + actual fun i(tag: String, message: String) { + println("INFO $tag: $message") + } + + actual fun w(tag: String, message: String) { + println("WARNING $tag: $message") + } + + actual fun e(tag: String, message: String) { + println("ERROR $tag: $message") + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 17070047..eabd9f0e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ rootProject.name = "CloudStream" -include(":app") \ No newline at end of file +include(":app") +include(":library") \ No newline at end of file