Merge remote-tracking branch 'origin/master' into agp

# Conflicts:
#	build.gradle.kts
This commit is contained in:
IndusAryan 2024-05-15 18:26:22 +05:30
commit 4ba60ad835
169 changed files with 4604 additions and 1659 deletions

View File

@ -4,17 +4,16 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="delegatedBuild" value="true" />
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/library" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

View File

@ -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<Jar>("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<Copy>("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<Jar>("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<KotlinCompile> {

View File

@ -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

View File

@ -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() {

View File

@ -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<Keys>()?.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<String>
)
}

View File

@ -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<MetaData>() ?: 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
}

View File

@ -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<ExtractorLink>? {
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
)
)
}
}

View File

@ -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<VidsrctoEpisodeSources>() ?: return
if (res.status != 200) return
res.result?.amap { source ->
val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe<VidsrctoEmbedSource>() ?: 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<VidsrctoResult>?
)
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)
}

View File

@ -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<SvgObject>(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
)
}

View File

@ -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,
)
}
}

View File

@ -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) {

View File

@ -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<String>()
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<WcoSources>(decoded)
videoLinkDTO.let { videoLinks.add(it.toString()) }
}
videoLinks.forEach { videoLink ->
M3u8Helper.generateM3u8(
name,
videoLink,
"$mainUrl/",
headers = mapOf("Origin" to "$mainUrl/")
).forEach(callback)
}
}
}
data class WcoSources(
@JsonProperty("VideoLinkDTO") val VideoLinkDTO: String,
)
}

View File

@ -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<ExtractorLink>? {
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
}
}

View File

@ -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
)

View File

@ -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<List<MediaDetails>>(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<SearchResponse>? {
val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query")
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
element.toSearchResponse()
}
return results
}
override suspend fun load(url: String): LoadResponse {
val data = parseJson<Data>(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<People>(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<List<MediaDetails>>(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<Episode>()
val seasons = parseJson<List<Seasons>>(resSeasons)
val seasonsNames = mutableListOf<SeasonData>()
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<String>? = null,
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
@JsonProperty("genres") val genres: List<String>? = 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<String>? = null,
@JsonProperty("poster") val poster: List<String>? = null,
@JsonProperty("logo") val logo: List<String>? = null,
@JsonProperty("clearart") val clearart: List<String>? = null,
@JsonProperty("banner") val banner: List<String>? = null,
@JsonProperty("thumb") val thumb: List<String>? = null,
@JsonProperty("screenshot") val screenshot: List<String>? = null,
@JsonProperty("headshot") val headshot: List<String>? = null,
)
data class People(
@JsonProperty("cast") val cast: List<Cast>? = null,
)
data class Cast(
@JsonProperty("character") val character: String? = null,
@JsonProperty("characters") val characters: List<String>? = 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<TraktEpisode>? = 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<String>? = 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,
)
}

View File

@ -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 <T> LifecycleOwner.observe(liveData: LiveData<T>, 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 <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.removeObservers(this)
liveData.observe(this) { action(it) }
}

View File

@ -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()

View File

@ -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<T>(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<Int, HashMap<Int, Any?>>()
}
abstract class NoStateAdapter<T : Any>(fragment: Fragment) : BaseAdapter<T, Any>(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<T> = BaseDiffCallback()
) : RecyclerView.Adapter<ViewHolderState<S>>() {
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<T> = 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<T>?) {
// 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<S>, item: T, position: Int) =
onBindContent(holder, item, position)
open fun onBindContent(holder: ViewHolderState<S>, item: T, position: Int) = Unit
open fun onBindFooter(holder: ViewHolderState<S>) = Unit
open fun onBindHeader(holder: ViewHolderState<S>) = Unit
open fun onCreateContent(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
open fun onCreateFooter(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
open fun onCreateHeader(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
override fun onViewAttachedToWindow(holder: ViewHolderState<S>) {
holder.onViewAttachedToWindow()
}
override fun onViewDetachedFromWindow(holder: ViewHolderState<S>) {
holder.onViewDetachedFromWindow()
}
fun save(recyclerView: RecyclerView) {
for (child in recyclerView.children) {
val holder =
recyclerView.findContainingViewHolder(child) as? ViewHolderState<S> ?: continue
setState(holder)
}
}
fun clear() {
stateViewModel.layoutManagerStates[id]?.clear()
}
private fun getState(holder: ViewHolderState<S>): S? =
stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S
private fun setState(holder: ViewHolderState<S>) {
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<S>) {
setState(holder)
holder.onViewRecycled()
super.onViewRecycled(holder)
}
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderState<S> {
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<S>,
position: Int,
payloads: MutableList<Any>
) {
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<S>, 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<T : Any>(
val itemSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() },
val contentSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() }
) : DiffUtil.ItemCallback<T>() {
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()
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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<RecyclerView.ViewHolder> =
DownloadChildAdapter(
ArrayList(),

View File

@ -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
}

View File

@ -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 {

View File

@ -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<SearchResponse>,
class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState<Boolean>(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<RecyclerView.ViewHolder>() {
BaseAdapter<SearchResponse, Boolean>(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<Boolean> {
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<Boolean>,
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<SearchResponse>) {
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<SearchResponse>,
private val newList: List<SearchResponse>
) :
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
}

View File

@ -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<SearchResponse>()
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

View File

@ -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<HomeViewModel.ExpandableHomepageList>,
//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<ViewHolder>() {
// 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<Int, Parcelable?>()
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<HomePageList>) {
updateList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
.toMutableList())
}
@JvmName("updateListExpandableHomepageList")
fun updateList(
newList: MutableList<HomeViewModel.ExpandableHomepageList>,
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<HomeViewModel.ExpandableHomepageList, Bundle>(
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<Bundle>(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<HomeViewModel.ExpandableHomepageList>?) {
super.submitList(list?.sortedBy { it.list.list.isEmpty() })
}
override fun onUpdateContent(
holder: ViewHolderState<Bundle>,
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<Bundle>,
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<HomeViewModel.ExpandableHomepageList>,
private val newList: List<HomeViewModel.ExpandableHomepageList>
) :
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<HomePageList>) {
submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
.toMutableList())
}
}

View File

@ -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<HomeViewModel.ExpandableHomepageList>,
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<Bundle> {
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<Bundle>) {
(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<Bundle>(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<Parcelable>("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<Parcelable>("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<Pair<Chip, WatchType>>(
@ -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<SearchResponse>) {
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<Boolean, List<SearchResponse>>) {
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" }
}
}
}

View File

@ -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<RecyclerView.ViewHolder>() {
private var items: MutableList<LoadResponse> = mutableListOf()
class HomeScrollAdapter(
fragment: Fragment
) : NoStateAdapter<LoadResponse>(fragment) {
var hasMoreItems: Boolean = false
fun getItem(position: Int): LoadResponse? {
return items.getOrNull(position)
}
fun setItems(newItems: List<LoadResponse>, 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<Any> {
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<RecyclerView.ViewHolder>() {
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<Any>,
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<LoadResponse>,
private val newList: List<LoadResponse>
) :
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
}
}

View File

@ -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<List<SearchResponse>>()
private val _preview = MutableLiveData<Resource<Pair<Boolean, List<LoadResponse>>>>()
private val previewResponses = mutableListOf<LoadResponse>()
private val previewResponses = CopyOnWriteArrayList<LoadResponse>()
private val previewResponsesAdded = mutableSetOf<String>()
val resumeWatching: LiveData<List<SearchResponse>> = _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 =

View File

@ -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<ImageView>(androidx.appcompat.R.id.search_close_btn)
val searchExitIcon =
binding?.mainSearch?.findViewById<ImageView>(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)
}

View File

@ -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 {

View File

@ -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<Bundle>(binding) {
override fun save(): Bundle =
Bundle().apply {
putParcelable(
"pageRecyclerview",
binding.pageRecyclerview.layoutManager?.onSaveInstanceState()
)
}
override fun restore(state: Bundle) {
state.getParcelable<Parcelable>("pageRecyclerview")?.let { recycle ->
binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle)
}
}
}
class ViewpagerAdapter(
var pages: List<SyncAPI.Page>,
fragment: Fragment,
val scrollCallback: (isScrollingDown: Boolean) -> Unit,
val clickCallback: (SearchClickCallback) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return PageViewHolder(
) : BaseAdapter<SyncAPI.Page, Bundle>(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<Bundle> {
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<Bundle>,
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<Int>()
override fun onBindContent(holder: ViewHolderState<Bundle>, 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<AppBarLayout>(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<AppBarLayout>(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
}
}

View File

@ -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(

View File

@ -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(

View File

@ -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
}

View File

@ -10,7 +10,8 @@ enum class LoadType {
InAppDownload,
ExternalApp,
Browser,
Chromecast
Chromecast,
Fcast
}
fun LoadType.toSet() : Set<ExtractorLinkType> {
@ -29,12 +30,17 @@ fun LoadType.toSet() : Set<ExtractorLinkType> {
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
)
}
}

View File

@ -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)) {

View File

@ -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))
}

View File

@ -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,
)
}

View File

@ -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(

View File

@ -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
}

View File

@ -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<LinkLoadingResult, Int>) -> 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<String, String>().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

View File

@ -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? {

View File

@ -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<TvType>().sorted().filter { it != TvType.NSFW }
.map { it.ordinal.toString() }.toSet()
@ -506,8 +508,8 @@ class SearchFragment : Fragment() {
}*/
//main_search.onActionViewExpanded()*/
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
ParentItemAdapter(mutableListOf(), { callback ->
val masterAdapter =
ParentItemAdapter(fragment = this, id = "masterAdapter".hashCode(), { callback ->
SearchHelper.handleSearchClickCallback(callback)
}, { item ->
bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = {

View File

@ -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<SwitchPreferenceCompat>(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 =

View File

@ -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<AccountManager>): 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
}
}

View File

@ -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(

View File

@ -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

View File

@ -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)
}

View File

@ -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 */

View File

@ -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()
}
}

View File

@ -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<String, String> {
if (referer.isBlank()) {
@ -849,6 +889,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Streamlare(),
VidSrcExtractor(),
VidSrcExtractor2(),
VidSrcTo(),
PlayLtXyz(),
AStreamHub(),
Vidplay(),
@ -864,7 +905,16 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Megacloud(),
VidhideExtractor(),
StreamWishExtractor(),
EmturbovidExtractor()
EmturbovidExtractor(),
Vtbe(),
EPlayExtractor(),
Vidguardto(),
Simpulumlamerop(),
Urochsunloath(),
Yipsu(),
MetaGnathTuggers(),
Geodailymotion(),
)

View File

@ -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

View File

@ -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)
}
}
}
}

View File

@ -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<AppBarLayout.LayoutParams> {
scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL
}
}
}
fun Activity.hideKeyboard() {
window?.decorView?.clearFocus()
this.findViewById<View>(android.R.id.content)?.rootView?.let {

View File

@ -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"

View File

@ -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<PublicDeviceInfo> = 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" }
}

View File

@ -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 <T> 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)
}
}
}

View File

@ -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<String, String>? = 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
)

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#9BA0A4"
android:pathData="M320,800h320v-120q0,-66 -47,-113t-113,-47q-66,0 -113,47t-47,113v120ZM480,440q66,0 113,-47t47,-113v-120L320,160v120q0,66 47,113t113,47ZM160,880v-80h80v-120q0,-61 28.5,-114.5T348,480q-51,-32 -79.5,-85.5T240,280v-120h-80v-80h640v80h-80v120q0,61 -28.5,114.5T612,480q51,32 79.5,85.5T720,680v120h80v80L160,880ZM480,800ZM480,160Z"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/white"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M15.67,4L14,4L14,3c0,-0.55 -0.45,-1 -1,-1h-2c-0.55,0 -1,0.45 -1,1v1L8.33,4C7.6,4 7,4.6 7,5.33v15.33C7,21.4 7.6,22 8.34,22h7.32c0.74,0 1.34,-0.6 1.34,-1.33L17,5.33C17,4.6 16.4,4 15.67,4zM13,18h-2v-2h2v2zM13,13c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1v-3c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v3z" />
</vector>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@android:color/white">
<item>
<shape android:shape="oval">
<stroke
android:width="2dp"
android:color="?attr/white" />
<corners android:radius="10dp" />
</shape>
</item>
</ripple>

View File

@ -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" />
</GridLayout>
</androidx.cardview.widget.CardView>

View File

@ -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" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -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" />
</LinearLayout>
@ -81,6 +83,7 @@
style="@style/SmallBlackButton"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:focusable="false"
android:text="@string/extension_description" />
<TextView
@ -111,6 +114,7 @@
style="@style/SmallBlackButton"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:focusable="false"
android:text="@string/extension_authors" />
<TextView
@ -140,6 +144,7 @@
style="@style/SmallBlackButton"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:focusable="false"
android:text="@string/extension_version" />
<TextView
@ -169,6 +174,7 @@
style="@style/SmallBlackButton"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:focusable="false"
android:text="@string/extension_status" />
<TextView
@ -198,6 +204,7 @@
style="@style/SmallBlackButton"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:focusable="false"
android:text="@string/extension_size" />
<TextView
@ -228,6 +235,7 @@
style="@style/SmallBlackButton"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:focusable="false"
android:text="@string/extension_types" />
<TextView
@ -258,6 +266,7 @@
style="@style/SmallBlackButton"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:focusable="false"
android:text="@string/extension_language" />
<TextView
@ -305,6 +314,7 @@
android:layout_marginEnd="32dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_thumb_up_24"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/plugin_votes"

View File

@ -178,42 +178,40 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:textStyle="bold"
tools:text="The Perfect Run The Perfect Run" />
<TextView
android:id="@+id/result_episodes_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
android:visibility="gone"
tools:visibility="visible"
tools:text="8 Episodes" />
<LinearLayout
android:id="@+id/result_next_airing_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:orientation="vertical">
<TextView
android:id="@+id/result_episodes_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="20dp"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
android:visibility="gone"
tools:text="8 Episodes" />
android:orientation="horizontal">
<TextView
android:id="@+id/result_next_airing"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center"
android:gravity="start"
android:textColor="?attr/grayTextColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="Episode 1022 will be released in" />
android:layout_marginEnd="5dp"
tools:text="Season 2 Episode 1022 will be released in" />
<TextView
android:id="@+id/result_next_airing_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="start"
android:textColor="?attr/textColor"
android:textSize="17sp"

View File

@ -24,7 +24,6 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="20dp"
android:visibility="gone"
tools:visibility="visible">
<androidx.cardview.widget.CardView
@ -36,7 +35,11 @@
android:id="@+id/settings_profile_pic"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="ContentDescription" />
android:scaleType="centerCrop"
android:foreground="@drawable/rounded_outline"
tools:src="@drawable/profile_bg_orange"
android:contentDescription="@string/account"/>
</androidx.cardview.widget.CardView>
<TextView
@ -50,7 +53,7 @@
android:textColor="?attr/textColor"
android:textSize="18sp"
android:textStyle="normal"
tools:text="Hello world" />
tools:text="Quick Brown Fox" />
</LinearLayout>
<TextView
@ -112,19 +115,17 @@
android:orientation="horizontal">
<TextView
android:id="@+id/version_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="10dp"
android:text="@string/app_version"
android:textColor="?attr/textColor" />
<TextView
android:id="@+id/textView3"
android:id="@+id/delimiter0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="10dp"
android:text="•"
android:textColor="?attr/textColor" />
@ -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" />
<TextView
android:id="@+id/delimiter1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="•"
android:textColor="?attr/textColor" />
<TextView
android:id="@+id/build_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:textColor="?attr/textColor"
tools:text="21/03/2024 09:02 pm"/>
</LinearLayout>
</LinearLayout>

View File

@ -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" />
</LinearLayout>
<LinearLayout
android:id="@+id/audio_tracks_holder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="50"
android:orientation="vertical">
android:id="@+id/audio_tracks_holder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="50"
android:orientation="vertical">
<!-- android:id="@+id/subs_settings" android:foreground="?android:attr/selectableItemBackgroundBorderless"
-->
@ -107,17 +106,16 @@
</LinearLayout>
<ListView
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:nextFocusLeft="@id/sort_providers"
android:nextFocusRight="@id/cancel_btt"
tools:listfooter="@layout/sort_bottom_footer_add_choice"
tools:listitem="@layout/sort_bottom_single_choice" />
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" />
</LinearLayout>
</LinearLayout>
@ -132,11 +130,12 @@
<com.google.android.material.button.MaterialButton
style="@style/WhiteButton"
android:layout_gravity="center_vertical|end"
android:text="@string/sort_apply"
android:id="@+id/apply_btt"
android:layout_width="wrap_content" />
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" />
<com.google.android.material.button.MaterialButton
style="@style/BlackButton"

View File

@ -23,11 +23,10 @@
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_arrow_back_24"
app:tint="@android:color/white"
android:focusable="true"
android:visibility="gone"
android:layout_width="25dp"
android:layout_height="wrap_content">
<requestFocus />
android:layout_height="wrap_content"
tools:visibility="visible">
</ImageView>
<FrameLayout

View File

@ -6,7 +6,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/outline_drawable"
android:nextFocusRight="@id/action_button"
android:nextFocusRight="@id/action_settings"
android:orientation="horizontal"
android:clickable="true"
android:focusable="true"
@ -117,6 +117,9 @@
android:background="@drawable/outline_drawable"
android:contentDescription="@string/title_settings"
android:visibility="gone"
android:focusable="true"
android:nextFocusLeft="@id/repository_item_root"
android:nextFocusRight="@id/action_button"
app:srcCompat="@drawable/ic_baseline_tune_24"
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" />

View File

@ -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"/>
<ImageView
android:id="@+id/episode_play_icon"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center"
android:contentDescription="@string/play_episode"
android:src="@drawable/play_button" />
android:src="@drawable/play_button"
tools:visibility="invisible"/>
<ImageView
android:id="@+id/episode_upcoming_icon"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center"
android:src="@drawable/hourglass_24"
android:visibility="gone"
tools:visibility="visible" />
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/episode_progress"
@ -100,6 +112,13 @@
android:layout_height="wrap_content"
android:textColor="?attr/grayTextColor"
tools:text="Rated: 8.8" />
<TextView
android:id="@+id/episode_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?attr/grayTextColor"
tools:text="15 Apr 2024" />
</LinearLayout>
<com.lagradost.cloudstream3.ui.download.button.PieFetchButton

View File

@ -113,6 +113,13 @@
<requestFocus />
</com.google.android.material.button.MaterialButton>
<com.google.android.material.button.MaterialButton
android:id="@+id/reset_btt"
style="@style/BlackButton"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical|end"
android:text="@string/reset_btn" />
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel_btt"
style="@style/BlackButton"

View File

@ -92,7 +92,7 @@
<string name="loading">Laai tans…</string>
<string name="app_subbed_text">Sub</string>
<string name="action_remove_watching">Verwyder</string>
<string name="stream">Stroom</string>
<string name="stream">Netwerk stroom</string>
<string name="type_on_hold">Op rus</string>
<string name="app_name">CloudStream</string>
<string name="home_play">Speel</string>
@ -105,4 +105,5 @@
<string name="search_provider_text_types">Soek met behulp van tipes</string>
<string name="subs_import_text" formatted="true">Voer lettertipes in deur dit in %s te plaas</string>
<string name="cast_format" formatted="true">Rolverdeling: %s</string>
<string name="subscribe_tooltip">Nuwe episode notifikasie</string>
</resources>

View File

@ -68,8 +68,8 @@
<string name="provider_info_meta">هيدا المصدر مش عاطي \"ميتا داتا\". إذا مش موجودة بالمصدر، ما رح يمشي الڤيديو.</string>
<string name="no_season">ما في أجزاء</string>
<string name="play_episode_toast">مشّي الحلقة</string>
<string name="category_account">أكونتات</string>
<string name="eigengraumode_settings">سرعة \"إيگن گرايڤي\"</string>
<string name="category_account">الحسابات والأمان</string>
<string name="eigengraumode_settings">سرعة الڤيديو</string>
<string name="resume">كَمِّل</string>
<string name="test_log">سِجِل</string>
<string name="torrent_no_plot">ما نلاقى الوصف</string>
@ -80,7 +80,6 @@
<string name="delete">محي</string>
<string name="start">بلش</string>
<string name="apk_installer_settings_des">في تِلِفونات ما فيا تعوز الطريقة الجديدة لتجديد الآپات. جربو \"الطريقة القديمة\" إذا ما عم تنزل التجديدات.</string>
<string name="eigengraumode_settings_des">بتزيد أوپسيونات لتحدد سرعة الڤيديو</string>
<string name="picture_in_picture_des">بعد ما تسكر \"كلود ستريم\"، بكفي الڤيديو بشِباك زغير فوق غير آپ</string>
<string name="no_chromecast_support_toast">هيدا المصدر ما بيدعم \"كروم كاست\"</string>
<string name="advanced_search">تنبيش منظّم</string>
@ -124,7 +123,7 @@
<string name="episodes_range">%1$d%2$d</string>
<string name="benene">عطي المطورين موزززة</string>
<string name="restore_settings">عوز النسخة الإحتياطية</string>
<string name="select_an_account">نقى أكونت</string>
<string name="select_an_account">نقي أكونت</string>
<string name="swipe_to_change_settings_des">سحابو ل فوق وتحت من اليمين أو الشمال ل تتحكمو بقوة الصوت أو قوة الضو</string>
<string name="copy_link_toast">ننسخ الرابط</string>
<string name="torrent_plot">الوصف</string>
@ -144,7 +143,7 @@
<string name="loading">لودينگ…</string>
<string name="action_remove_watching">شيل</string>
<string name="action_open_watching">بَعِد مَعلومات</string>
<string name="category_updates">التجديد والنسخات الاحتياطية</string>
<string name="category_updates">التجديدات والنسخات الاحتياطية</string>
<string name="pref_filter_search_quality">خبي هيدي الجودات من نتائج التنبيش</string>
<string name="type_on_hold">موقف موقتًا</string>
<string name="app_name">كلود ستريم</string>
@ -199,7 +198,7 @@
<string name="check_for_update">شوف إذا في تجديد</string>
<string name="render_error">في مشكلة بجهاز العرض (Renderer error)</string>
<string name="show_title">العِنوان</string>
<string name="jsdelivr_proxy">پروكسي raw.githubusercontent.com</string>
<string name="jsdelivr_proxy">پروكسي \"گِت هَب\"</string>
<string name="limit_title_rez">جودة مشغل الڤيديو</string>
<string name="show_sub">ملصق الترجمة</string>
<string name="ova_singular">أوڤا</string>
@ -252,7 +251,7 @@
<string name="limit_title">الحد الأعلى للحروف بعنوان الڤيديو</string>
<string name="pref_category_looks">المظاهر</string>
<string name="pref_category_app_updates">تجديدات الآپ</string>
<string name="provider_lang_settings">لغات المصادر</string>
<string name="provider_lang_settings">لغات الإضافات</string>
<string name="category_general">عام</string>
<string name="download_path_pref">ممر التنزيل</string>
<string name="legal_notice">إخلاء مسؤولية</string>
@ -265,7 +264,7 @@
<string name="display_subbed_dubbed_settings">فرجي أنمي المدبلج-المترجم</string>
<string name="pref_category_backup">النسخ الإحتياطي</string>
<string name="pref_category_actions">الإجراءات</string>
<string name="jsdelivr_proxy_summary">بتتجاوز منع \"گِت هَب\" بستعمال JSDelivr. معقول تقدي لتأخير بتجديدات الآپ بكم يوم.</string>
<string name="jsdelivr_proxy_summary">تجاوز منع روابط \"گِت هَب\" الـ\"raw\" بستعمال JSDelivr. معقول تقدي لتأخير تجديدات الآپ بكم يوم.</string>
<string name="pref_category_player_features">ميزات مشغل الڤيديوات</string>
<string name="remove_site_pref">شيل موقع</string>
<string name="pref_category_player_layout">مظهر</string>
@ -289,7 +288,7 @@
<string name="category_ui">الشكل</string>
<string name="next_episode_time_hour_format" formatted="true">%1$d ساعة %2$d ديقة</string>
<string name="sort_clear">فضّى</string>
<string name="example_username">إسم أكونتي الكول</string>
<string name="example_username">إسم الأكونت</string>
<string name="filter_bookmarks">فَلتِر الإشارات المرجعية</string>
<string name="update_started">بَلَش التجديد</string>
<string name="sort_copy">نسخ</string>
@ -305,9 +304,9 @@
<string name="play_episode">مشّي الحلقة</string>
<string name="player_speed">سرعة الڤيديو</string>
<string name="manage_accounts">تحكمو بالأكونتات</string>
<string name="example_lang_name">رمز اللغة (apc/ar/en)</string>
<string name="example_lang_name">رمز اللغة (ar)</string>
<string name="create_account">عمول أكونت</string>
<string name="enable_nsfw_on_providers">فرجي المحتوى الـ18+ بالمصادر يلي بتحتوي</string>
<string name="enable_nsfw_on_providers">تمكين محتوى 18+ في الامتدادات الداعمة</string>
<string name="preferred_media_settings">المحتوى المفضل</string>
<string name="switch_account">غَيِر الأكونت</string>
<string name="popup_pause_download">حط پوز على التنزيل</string>
@ -334,8 +333,8 @@
<string name="example_email">إي مايل (ع شكل: email@example.com)</string>
<string name="next_episode_time_min_format" formatted="true">%d ديقة</string>
<string name="category_provider_test">فحص المصادر</string>
<string name="example_site_url">example.com</string>
<string name="example_site_name">إسم الوبسيت الكول تبعي</string>
<string name="example_site_url">https://example.com</string>
<string name="example_site_name">إسم الوب سيت الجديدة</string>
<string name="popup_play_file">فتاح ومَشّي الملف</string>
<string name="app_theme_settings">نوع الألون</string>
<string name="go_back">رجاع</string>
@ -348,7 +347,7 @@
<string name="primary_color_settings">اللون الاساسي</string>
<string name="login">فوت ع الأكونت</string>
<string name="app_subbed_text">مترجم</string>
<string name="stream">بِث</string>
<string name="stream">بِث من الإنترنت</string>
<string name="home_play">مشي</string>
<string name="download_storage_text">التخزين الجُواني</string>
<string name="add_account">زيد أكونت</string>
@ -382,11 +381,9 @@
<string name="enter_pin_with_name" formatted="true">حطو الأرقام السرية لـ\"%s\"</string>
<string name="apk_installer_legacy">الطريقة القديمة</string>
<string name="subtitles_raised">معلى</string>
<string name="blank_repo_message">\"كلود ستريم\" ما بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپوز.
<string name="blank_repo_message">\"كلود ستريم\" ما بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپويات.
\n
\nفي معلومات على الـ\"ديسكورد\" تبعنا، أو فيكون تنبشو ع معلومات على الإنترنت.
\n
\nما فينا تحط الروابط تبع ريپوز المصادر هون من ورا \"سكاي يو كي المحدودة\" 🤮، يلي عازت تفكيرا المحدود لتجرب توقف هيدا الآپ بإستعمال \"قانون الألفية للملكية الرقميَّة\".</string>
\nفي معلومات على الـ\"ديسكورد\" تبعنا، أو فيكن تنبشو ع معلومات على الإنترنت.</string>
<string name="add_sync">زبد تتبع</string>
<string name="mobile_data">3G/4G…</string>
<string name="player_loaded_subtitles" formatted="true">نفَتح %s</string>
@ -410,7 +407,7 @@
<string name="quality_hdr">HDR</string>
<string name="plugins_not_downloaded" formatted="true">مش منزل: %d</string>
<string name="next">ل بعدو</string>
<string name="network_adress_example">رابط الڤيديو</string>
<string name="network_adress_example">https://example.com/example.mp4</string>
<string name="select_library">نقي الرفّ</string>
<string name="subscription_deleted">وقفتو الإشتراك لـ\"%s\"</string>
<string name="quality_workprint">WP</string>
@ -451,7 +448,7 @@
<string name="skip_type_recap">التلخيص</string>
<string name="yes">إِيه</string>
<string name="actor_main">الرئيسي</string>
<string name="apply_on_restart">طبّق وقتما سكّر الآپ</string>
<string name="apply_on_restart">سكر الآپ حتى تطبق التغيرات</string>
<string name="help">ساعدوني</string>
<string name="subtitle_offset_extra_hint_before_format">عوز هيدا إذا عم بتبين الترجمة %d ميلي ثانية بعدما لازم</string>
<string name="app_not_found_error">ما نلاقا الآپ</string>
@ -569,7 +566,7 @@
<string name="quality_dvd">دي ڤي دي</string>
<string name="resolution">الجودة</string>
<string name="set_default">عين الافتراضي</string>
<string name="referer">المرجع</string>
<string name="referer">المرجع (إختياري)</string>
<string name="player_settings_play_in_app">المشغل يلي بـ\"كلود ستريم\"</string>
<string name="setup_extensions_subtext">نزل لايحة المواقع يلي بدك تعوزن</string>
<string name="enter_pin">حطو الأرقام السرية</string>
@ -595,5 +592,34 @@
<string name="rotate_video">برومو</string>
<string name="auto_rotate_video_desc">غير إتجاه الشاشة أوتوماتيكيًا حسب شكل الڤيديو</string>
<string name="links_reloaded_toast">رجع نعمل لاود لاللينك</string>
<string name="copyTitle">نعمل كَپي للعنوان!</string>
<string name="result_search_tooltip">نبش بغير مصادر</string>
<string name="subscribe_tooltip">نوتيفيكايشن عن حلقات جديدة</string>
<string name="recommendations_tooltip">فرجي الاقترحات</string>
<string name="speed_setting_summary">بتزيد خيار السرعة بالمشغل</string>
<string name="test_extensions">فحاص كل المصادر</string>
<string name="test_extensions_summary">هيدا الفحص معمول للمطورين وما بأكد لحالو إزا المصدر عم يشتغل.</string>
<string name="favorite">المفضلة</string>
<string name="biometric_authentication_title">فتح قفل كلودستريم</string>
<string name="biometric_setting">قفل بواسطة المقاييس الحيوية</string>
<string name="password_pin_authentication_title">رمز/كلمة مرور للمصادقة</string>
<string name="biometric_setting_summary">فتاح التطبيق باستعمال البصمة، آي دي الوج، پِن، النمط، إو الپاسورد.</string>
<string name="biometric_prompt_description">تسَكرت هيدي الواجهة من ورا محاولات فاشلة عديدة. پليز، سكر الآپ ورجاع فتحه.</string>
<string name="resume_remaining" formatted="true">%s
\nباقي</string>
<string name="biometric_unsupported">المصادقة البيومترية مش مدعومة ع هالجهاز</string>
<string name="unfavorite">شيله من المفضل</string>
<string name="repo_copy_label">اسم وعنوان الريپوزيتوري</string>
<string name="toast_copied">نتسخ!</string>
<string name="clipboard_permission_error">في ارور بالوصول ل الكليپبورد. پليز جرب مرة أخرى.</string>
<string name="clipboard_unknown_error">في ارور بالنسخ. پليز نسوخ الـLogcat 🐈 وبعته ل المسؤولين عن دعم الآپ.</string>
<string name="biometric_warning">هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها.
\nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج.</string>
<string name="ok">أوكي</string>
<string name="battery_dialog_title">وقف اپتميزايشن بطارية جهازك</string>
<string name="app_unrestricted_toast">بطارية الآپ اصلًا محطوطة ع «غير مقيد» \"Unrestricted\"</string>
<string name="app_info_intent_error">ما قدرنا نفتح معلومات الآپ تبع \"كلود ستريم\".</string>
<string name="music_singlar">موسيقى</string>
<string name="audio_book_singular">أوديو بوك</string>
<string name="custom_media_singluar">الميديا</string>
<string name="battery_dialog_message">لتضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يلي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي بـ الباكگروند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". ملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآن بـ الباكگروند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ بـ«الإعدادات العامة» \"General settings\"، إزا غيرت رأيك.</string>
</resources>

View File

@ -54,7 +54,7 @@
<string name="download_failed">فشل التنزيل</string>
<string name="download_canceled">تم إلغاء التنزيل</string>
<string name="download_done">تم التنزيل</string>
<string name="stream">بث</string>
<string name="stream">دفق الشبكة</string>
<string name="error_loading_links_toast">خطأ في تحميل الرابط</string>
<string name="download_storage_text">التخزين الداخلي</string>
<string name="app_dubbed_text">مدبلج</string>
@ -114,8 +114,7 @@
<string name="player_subtitles_settings_des">إعدادات ترجمة المُشغل</string>
<string name="chromecast_subtitles_settings">ترجمة كروم كاست</string>
<string name="chromecast_subtitles_settings_des">إعدادات ترجمة كروم كاست</string>
<string name="eigengraumode_settings">وضع إيغنغرافي</string>
<string name="eigengraumode_settings_des">يضيف خيار السرعة في المُشغل</string>
<string name="eigengraumode_settings">سرعة التشغيل</string>
<string name="swipe_to_seek_settings">السحب لتقديم</string>
<string name="swipe_to_seek_settings_des">اسحب من جانب إلى آخر للتحكم في موضعك في مقطع فيديو</string>
<string name="swipe_to_change_settings">السحب لتغيير الإعدادات</string>
@ -139,7 +138,7 @@
<string name="backup_failed">إذن الوصول الى ذاكرة التخزين مفقود, من فضلك حاول مجددا.</string>
<string name="backup_failed_error_format">فشل إنشاء نسخة احتياطية %s</string>
<string name="search">بحث</string>
<string name="category_account">الحسابات</string>
<string name="category_account">الحسابات والأمن</string>
<string name="category_updates">التحديثات والنسخ الاحتياطية</string>
<string name="settings_info">معلومات</string>
<string name="advanced_search">البحث المتقدم</string>
@ -284,10 +283,10 @@
<string name="category_general">عام</string>
<string name="random_button_settings">زر العشوائي</string>
<string name="random_button_settings_desc">إظهار زر عشوائي على الصفحة الرئيسية والمكتبة</string>
<string name="provider_lang_settings">لغات المزود</string>
<string name="provider_lang_settings">لغات الامتداد</string>
<string name="app_layout">واجهة التطبيق</string>
<string name="preferred_media_settings">المحتوى المفضل</string>
<string name="enable_nsfw_on_providers">تفعيل محتوى خاص للبالغين داخل المزودين المدعومين</string>
<string name="enable_nsfw_on_providers">تفعيل محتوى خاص للبالغين داخل الإمتداد المدعوم</string>
<string name="subtitles_encoding">فك تشفير الترجمة</string>
<string name="category_providers">المصادر</string>
<string name="category_ui">الواجهة</string>
@ -308,8 +307,8 @@
<string name="example_username">إسم المستخدم</string>
<string name="example_email">البريد الإلكتروني</string>
<string name="example_ip">127.0.0.1</string>
<string name="example_site_name">إسم الموقع</string>
<string name="example_site_url">رابط الموقع</string>
<string name="example_site_name">إسم الموقع الجديد</string>
<string name="example_site_url">رابط الموقع مثلا : https://example.com</string>
<string name="example_lang_name">اللغة (الإنجليزية)</string>
<!--
<string name="mal_account_settings" translatable="false">MAL</string>
@ -400,8 +399,8 @@
<string name="subtitles_filter_lang">تصفية حسب لغة الوسائط المفضلة</string>
<string name="extras">اكسترا</string>
<string name="trailer">مقطع دعائي</string>
<string name="network_adress_example">رابط الفيديو</string>
<string name="referer">المرجع</string>
<string name="network_adress_example">https://example.com/example.mp4</string>
<string name="referer">المرجع (اختياري)</string>
<string name="next">التالي</string>
<string name="provider_languages_tip">شاهد الفيديوهات بهذه اللغات</string>
<string name="previous">السابق</string>
@ -430,11 +429,9 @@
<string name="plugins_downloaded" formatted="true">تم تحميل: %d</string>
<string name="plugins_disabled" formatted="true">مُعطل %d</string>
<string name="plugins_not_downloaded" formatted="true">غير مُحمل: %d</string>
<string name="blank_repo_message">لا يحتوي CloudStream على مواقع مثبتة بشكل افتراضي. تحتاج إلى تثبيت المواقع من المستودعات.
<string name="blank_repo_message">لا يحتوي CloudStream على مواقع مثبتة افتراضيًا. تحتاج إلى تثبيت المواقع من المستودعات.
\n
\nبسبب إزالة قانون الألفية الجديدة لحقوق طبع ونشر المواد الرقمية بدون تفكير مسبق بواسطة Sky UK Limited 🤮 ، لا يمكننا ربط موقع المستودع في التطبيق.
\n
\nانضم إلى Discord أو ابحث عبر الإنترنت.</string>
\nانضم إلى ديسكورد أو ابحث عبر الإنترنت.</string>
<string name="view_public_repositories_button">عرض مستودعات المجتمع</string>
<string name="view_public_repositories_button_short">قائمة عامة</string>
<string name="uppercase_all_subtitles">جميع الترجمات حروف كبيرة</string>
@ -443,7 +440,7 @@
<string name="tracks">المسارات</string>
<string name="audio_tracks">مسار الصوت</string>
<string name="video_tracks">مسار الفيديو</string>
<string name="apply_on_restart">تطبيق بعد إعادة التشغيل</string>
<string name="apply_on_restart">أعد تشغيل التطبيق لرؤية التغييرات.</string>
<string name="safe_mode_title">الوضع الآمن قيد التشغيل</string>
<string name="safe_mode_description">تم إيقاف تشغيل جميع الملحقات بسبب عطل لمساعدتك في العثور على الإضافة التي تسبب مشكلة.</string>
<string name="safe_mode_crash_info">عرض بيانات الاعطال</string>
@ -557,8 +554,8 @@
<string name="pref_category_bypass">تجاوز مزود خدمة الإنترنت</string>
<string name="revert">استرجاع</string>
<string name="jsdelivr_enabled">تعذر الوصول إلى جيثب. تشغيل وكيل jsDelivr …</string>
<string name="jsdelivr_proxy_summary">تجاوز حظر GitHub باستخدام jsdelivr ، قد يتسبب في تأخير التحديثات لبضعة أيام.</string>
<string name="jsdelivr_proxy">وكيل raw.githubusercontent.com</string>
<string name="jsdelivr_proxy_summary">تجاوز حظر عناوين URL الأولية لـ github باستخدام jsDelivr. قد يتسبب في تأخير التحديثات لبضعة أيام.</string>
<string name="jsdelivr_proxy">وكيل github</string>
<string name="watch_quality_pref_data">جودة المشاهدة المفضلة (بيانات الجوال)</string>
<string name="profile_number">الملف الشخصي %d</string>
<string name="wifi">واي فاي</string>
@ -623,5 +620,34 @@
<string name="auto_rotate_video">الدوران التلقائي</string>
<string name="rotate_video">تدوير</string>
<string name="auto_rotate_video_desc">تمكين التبديل التلقائي لاتجاه الشاشة بناءً على اتجاه الفيديو</string>
<string name="copyTitle">تم نسخ العنوان!</string>
<string name="recommendations_tooltip">إظهار التوصيات</string>
<string name="speed_setting_summary">يضيف خيار السرعة في المُشغل</string>
<string name="test_extensions_summary">هذا الاختبار مخصص للمطورين فقط ولا يتحقق أو ينفي عمل أي ملحق.</string>
<string name="subscribe_tooltip">إشعار الحلقة الجديدة</string>
<string name="result_search_tooltip">البحث في امتدادات أخرى</string>
<string name="test_extensions">اختبار كافة الملحقات</string>
<string name="biometric_setting">اقفل باستخدام المقاييس الحيوية</string>
<string name="biometric_unsupported">المصادقة البيومترية غير مدعومة على هذا الجهاز</string>
<string name="biometric_setting_summary">افتح التطبيق باستخدام بصمة الإصبع ومعرف الوجه ورقم التعريف الشخصي والنمط وكلمة المرور.</string>
<string name="biometric_authentication_title">فتح سحابة البث</string>
<string name="password_pin_authentication_title">مصادقة كلمة المرور/رقم التعريف الشخصي</string>
<string name="biometric_prompt_description">تم إغلاق هذه الشاشة بسبب عدة محاولات فاشلة. الرجاء إعادة تشغيل التطبيق.</string>
<string name="biometric_warning">لقد تم الآن نسخ بيانات CloudStream احتياطيًا. على الرغم من أن احتمال حدوث ذلك منخفض جدًا، إلا أن جميع الأجهزة يمكن أن تتصرف بشكل مختلف. في الحالات النادرة، التي يتم فيها منعك من الوصول إلى التطبيق، قم بمسح بيانات التطبيق بالكامل واستعادتها من نسخة احتياطية. نحن نأسف جدًا لأي إزعاج ناتج عن هذا.</string>
<string name="resume_remaining" formatted="true">%s
\nمتبقي</string>
<string name="favorite">المفضلة</string>
<string name="unfavorite">إزالة من المفضلة</string>
<string name="repo_copy_label">اسم و عنوان المخزن</string>
<string name="clipboard_permission_error">خطأ في الوصول الي حافظة النسخ، برجاء المحاولة مرة اخرى.</string>
<string name="toast_copied">تم النسخ!</string>
<string name="clipboard_unknown_error">خطأ في عملية النسخ، برجاء نسخ ال logcat و ارساله الى مسؤولين دعم التطبيق.</string>
<string name="battery_dialog_title">تعطيل تحسين البطارية</string>
<string name="app_unrestricted_toast">تم ضبط استخدام بطارية التطبيق بالفعل على غير مقيد</string>
<string name="app_info_intent_error">غير قادر على فتح معلومات تطبيق CloudStream.</string>
<string name="audio_book_singular">كتاب صوتي</string>
<string name="ok">حسناً</string>
<string name="battery_dialog_message">لضمان عدم انقطاع التنزيلات والإشعارات للبرامج التلفزيونية المشتركة، يحتاج CloudStream إلى إذن للتشغيل في الخلفية. بالضغط على موافق، سيتم توجيهك إلى معلومات التطبيق. هناك، انتقل إلى استخدام بطارية التطبيق
\nواضبط استخدام البطارية على غير مقيد. يرجى ملاحظة أن هذا الإذن لا يعني أن CS3 سوف يستنزف البطارية. ولن يعمل إلا في الخلفية عند الضرورة، كما هو الحال عند تلقي الإشعارات أو تنزيل مقاطع الفيديو من الملحقات الرسمية. إذا اخترت الإلغاء، فيمكنك ضبط هذا الإعداد لاحقًا في الإعدادات العامة.</string>
<string name="music_singlar">موسيقى</string>
<string name="custom_media_singluar">الوسائط</string>
</resources>

View File

@ -64,7 +64,6 @@
<string name="advanced_search">البحث المتقدم</string>
<string name="player_size_settings_des">إزالة الحدود السوداء</string>
<string name="player_subtitles_settings">ترجمات</string>
<string name="eigengraumode_settings_des">يضيف خيار السرعة في المشغل</string>
<string name="double_tap_to_seek_settings">انقر نقرا مزدوجا للبحث</string>
<string name="double_tap_to_pause_settings">انقر نقرًا مزدوجًا للإيقاف المؤقت</string>
<string name="double_tap_to_seek_amount_settings">اللاعب يبحث عن المبلغ (بالثواني)</string>

View File

@ -58,7 +58,7 @@
<string name="download_failed">Изтеглянето се провали</string>
<string name="download_canceled">Изтеглянето е отменено</string>
<string name="download_done">Изтеглянето е завършено</string>
<string name="stream">Поток</string>
<string name="stream">Поток на данни</string>
<string name="error_loading_links_toast">Грешка при зареждане на връзки</string>
<string name="download_storage_text">Вътрешна памет</string>
<string name="app_dubbed_text">Дублаж</string>
@ -119,8 +119,7 @@
<string name="player_subtitles_settings_des">Настройки на субтитрите на плейъра</string>
<string name="chromecast_subtitles_settings">Chromecast субтитри</string>
<string name="chromecast_subtitles_settings_des">Настройки за субтитри на Chromecast</string>
<string name="eigengraumode_settings">Режим Eigengravy (промяна скорост на възпроизвеждане)</string>
<string name="eigengraumode_settings_des">Добавя опция за скорост в плейъра</string>
<string name="eigengraumode_settings">Промяна на скорост</string>
<string name="swipe_to_seek_settings">Плъзнете за преместване</string>
<string name="swipe_to_seek_settings_des">Плъзнете пръст от една страна на друга, за да контролирате позицията си във видеоклип</string>
<string name="swipe_to_change_settings">Плъзнете, за да промените настройките</string>
@ -282,7 +281,7 @@
<string name="legal_notice">Опровержение</string>
<string name="category_general">Общ</string>
<string name="random_button_settings">Случаен бутон</string>
<string name="random_button_settings_desc">Показване на произволен бутон на началната страница</string>
<string name="random_button_settings_desc">Показване на произволен бутон на началната страница и библиотека</string>
<string name="provider_lang_settings">Езици на доставчика</string>
<string name="app_layout">Оформление на приложението</string>
<string name="preferred_media_settings">Предпочитана медия</string>
@ -299,10 +298,10 @@
<string name="bottom_title_settings">Местоположение на заглавието на плаката</string>
<string name="bottom_title_settings_des">Поставете заглавието под плаката</string>
<string name="example_password">парола123</string>
<string name="example_username">Моето готино потребителско име</string>
<string name="example_username">Потребителско име</string>
<string name="example_email">hello@world.com</string>
<string name="example_ip">127.0.0.1</string>
<string name="example_site_name">Моят готин сайт</string>
<string name="example_site_name">НовоИмеНаСайт</string>
<string name="example_site_url">example.com</string>
<string name="example_lang_name">Езиков код (en)</string>
<!--
@ -386,7 +385,7 @@
<string name="subtitles_filter_lang">Филтриране по предпочитан медиен език</string>
<string name="extras">Екстри</string>
<string name="trailer">Трейлър</string>
<string name="network_adress_example">Връзка към потока</string>
<string name="network_adress_example">https://example.com/example.mp4</string>
<string name="referer">Обратно към</string>
<string name="next">Следващ</string>
<string name="provider_languages_tip">Гледайте видеоклипове на тези езици</string>
@ -419,9 +418,7 @@
<string formatted="true" name="plugins_updated">Актуализирани %d плъгини</string>
<string name="blank_repo_message">CloudStream няма инсталирани сайтове по подразбиране. Трябва да инсталирате сайтовете от хранилища.
\n
\nПоради безмозъчно премахване на DMCA от Sky UK Limited 🤮 не можем да свържем сайта на хранилището в приложението.
\n
\nПрисъединете се към нашия Дискорд или търсете онлайн.</string>
\nПрисъединете се към нашия Дискорд или потърсете онлайн.</string>
<string name="view_public_repositories_button">Вижте хранилищата на общността</string>
<string name="view_public_repositories_button_short">Публичен списък</string>
<string name="uppercase_all_subtitles">Всички субтитри с главни букви</string>
@ -505,9 +502,9 @@
<string name="watch_quality_pref_data">Предпочитано качество за гледане (Мобилни данни)</string>
<string name="start">Начало</string>
<string name="automatic_plugin_download_mode_title">Избери режим, да филтрира изтегляне на добавки</string>
<string name="jsdelivr_proxy">raw.githubusercontent.com Прокси</string>
<string name="jsdelivr_proxy">raw.githubusercontent.com прокси</string>
<string name="jsdelivr_enabled">Неуспешно свързване с GitHub. Включване на jsDelivr прокси…</string>
<string name="jsdelivr_proxy_summary">Заобикаля блокирането на GitHub с помощта на jsDelivr. Може да доведе до забавяне на актуализациите с няколко дни.</string>
<string name="jsdelivr_proxy_summary">Заобикали блокирането на GitHub адреси с помощта на jsDelivr. Може да доведе до забавяне на актуализациите с няколко дни.</string>
<string name="category_provider_test">Тест на доставчик</string>
<string name="disable">Деактивиране</string>
<string name="pref_category_android_tv">Android TV</string>
@ -545,4 +542,63 @@
\nНяма да се зареждат никакви разширения при стартиране, докато файлът не бъде премахнат.</string>
<string name="already_voted">Вече сте гласували</string>
<string name="set_default">Задаване по подразбиране</string>
<string name="pin_error_length">ПИН трябва да е 4 символа</string>
<string name="sort_updated_old">Актуализирано (от старо към ново)</string>
<string name="profile_background_des">Фон на профила</string>
<string name="favorites_list_name">Любими</string>
<string name="favorite_removed">%s премахнати от любими</string>
<string name="action_add_to_favorites">Добави в любими</string>
<string name="action_remove_from_favorites">Премахване от любими</string>
<string name="enter_pin">Въведи ПИН код</string>
<string name="pin">ПИН</string>
<string name="pin_error_incorrect">Неверен ПИН. Моля опитайте отново.</string>
<string name="select_an_account">Избери профил</string>
<string name="logged_account" formatted="true">Вписан като %s</string>
<string name="skip_startup_account_select_pref">Пропуснете избора на акаунт при стартиране</string>
<string name="links_reloaded_toast">Презаредени Линкове</string>
<string name="enter_pin_with_name" formatted="true">Вкарай ПИН за %s</string>
<string name="action_subscribe">Абонирай се</string>
<string name="action_unsubscribe">Отписване</string>
<string name="duplicate_title">Открит е Потенциален дубликат</string>
<string name="android_tv_interface_on_seek_settings">Показан играч - сума за търсене</string>
<string name="qualities">Качества</string>
<string name="rotate_video">Завърти</string>
<string name="android_tv_interface_off_seek_settings">Скрит играч - сума за търсене</string>
<string name="android_tv_interface_off_seek_settings_summary">Сумата за търсене, използвана, когато играчът е скрит</string>
<string name="sort_updated_new">Актуализирано (от ново към старо)</string>
<string name="quality_profile_help">Тук можете да промените начина на подреждане на източниците. Ако даден видеоклип има по-висок приоритет, той ще се показва по-високо в избора на източник. Сборът от приоритета на източника и приоритета на качеството е приоритетът на видеото.
\n
\nИзточник A: 3
\nКачество B: 7
\nЩе има комбиниран видео приоритет от 10.
\n
\nЗАБЕЛЕЖКА: Ако сумата е 10 или повече, играчът автоматично ще пропусне зареждането, когато тази връзка се зареди!</string>
<string name="duplicate_replace">Замени</string>
<string name="duplicate_replace_all">Замени Всички</string>
<string name="duplicate_message_single" formatted="true">Изглежда, че потенциално дублиран елемент вече съществува във вашата библиотека: „%s“.
\n
\nИскате ли все пак да добавите този елемент, да замените съществуващия или да отмените действието?</string>
<string name="duplicate_message_multiple" formatted="true">Във вашата библиотека са намерени потенциални дублиращи се елементи:
\n
\n%s
\n
\nИскате ли все пак да добавите този елемент, да замените съществуващите или да отмените действието?</string>
<string name="lock_profile">Заключи Профил</string>
<string name="enter_current_pin">Вкарай Сегашен ПИН</string>
<string name="manage_accounts">Управлявай Профили</string>
<string name="edit_account">Редактирай профил</string>
<string name="use_default_account">Използвай Акаунт по Подразбиране</string>
<string name="rotate_video_desc">Показване на бутон за превключване за ориентация на екрана</string>
<string name="backup_frequency">Честота на запазване</string>
<string name="favorite_added">%s добавени в любими</string>
<string name="duplicate_add">Добави</string>
<string name="auto_rotate_video_desc">Разрешете автоматичното превключване на ориентацията на екрана въз основа на ориентацията на видеото</string>
<string name="auto_rotate_video">Автоматично завъртане</string>
<string name="android_tv_interface_on_seek_settings_summary">Използваният размер на превъртане, когато плеърът е видим</string>
<string name="test_extensions">Тествай всички добавки</string>
<string name="subscribe_tooltip">Известие за нов епизод</string>
<string name="result_search_tooltip">Търси в други екстеншъни</string>
<string name="recommendations_tooltip">Покажи предложения</string>
<string name="speed_setting_summary">Добавя опция за промяна на скоростта в плеъра</string>
<string name="test_extensions_summary">Този тест е направен за програмисти и не проверява работата на никакви добавки.</string>
</resources>

View File

@ -118,7 +118,6 @@
<string name="chromecast_subtitles_settings">ক্রোমক্যাস্ট এ সাবটাইটেলসমূহ</string>
<string name="chromecast_subtitles_settings_des">ক্রোমক্যাস্ট এ সাবটাইটেল সমূহের সেটিংস</string>
<string name="player_size_settings_des">কালো প্রান্ত অপসারণ করুন</string>
<string name="eigengraumode_settings_des">প্লেয়ার এ দ্রুততা এর অপশন যুক্ত করে</string>
<string name="search">অনুসন্ধান করুন</string>
<string name="category_account">অ্যাকাউন্টসমূহ</string>
<string name="bug_report_settings_on">কোনো উপাত্ত পাঠাবে না</string>

View File

@ -11,99 +11,99 @@
<!-- IS NOT NEEDED TO TRANSLATE AS THEY ARE ONLY USED FOR SCREEN READERS AND WONT SHOW UP TO NORMAL USERS -->
<string name="result_poster_img_des">Poster</string>
<string name="search_poster_img_des">Pôster</string>
<string name="episode_poster_img_des">Episode Poster</string>
<string name="home_main_poster_img_des">Main Poster</string>
<string name="home_next_random_img_des">Next Random</string>
<string name="go_back_img_des">Go back</string>
<string name="home_change_provider_img_des">Change Provider</string>
<string name="preview_background_img_des">Preview Background</string>
<string name="episode_poster_img_des">Pôster do episódio</string>
<string name="home_main_poster_img_des">Pôster Principal</string>
<string name="home_next_random_img_des">Próximo Aleatório</string>
<string name="go_back_img_des">Voltar</string>
<string name="home_change_provider_img_des">Alterar Provedor</string>
<string name="preview_background_img_des">Visualizar plano de fundo</string>
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
<string name="player_speed_text_format" formatted="true">Velocidade (%.2fx)</string>
<string name="rated_format" formatted="true">Nota: %.1f</string>
<string name="rated_format" formatted="true">Avaliado: %.1f</string>
<string name="new_update_format" formatted="true">Nova atualização encontrada!
\n%1$s -&gt; %2$s</string>
<string name="filler" formatted="true">Filler</string>
<string name="filler" formatted="true">Preenchimento</string>
<string name="duration_format" formatted="true">%d min</string>
<string name="app_name">CloudStream</string>
<string name="title_home">Início</string>
<string name="title_search">Procurar</string>
<string name="title_search">Pesquisar</string>
<string name="title_downloads">Downloads</string>
<string name="title_settings">Configurações</string>
<string name="search_hint">Procurar…</string>
<string name="search_hint_site" formatted="true">Procurar no %s…</string>
<string name="search_hint_site" formatted="true">Pesquisar %s…</string>
<string name="no_data">Sem dados</string>
<string name="episode_more_options_des">Mais Opções</string>
<string name="episode_more_options_des">Mais opções</string>
<string name="next_episode">Próximo episódio</string>
<string name="result_tags">Gêneros</string>
<string name="result_share">Compartilhar</string>
<string name="result_open_in_browser">Abrir no Navegador</string>
<string name="skip_loading">Pular Carregamento</string>
<string name="result_open_in_browser">Abrir no navegador</string>
<string name="skip_loading">Pular carregamento</string>
<string name="loading">Carregando…</string>
<string name="type_watching">Assistindo</string>
<string name="type_on_hold">Em espera</string>
<string name="type_completed">Completado</string>
<string name="type_dropped">Deixado</string>
<string name="type_completed">Concluído</string>
<string name="type_dropped">Desistido</string>
<string name="type_plan_to_watch">Planejando assistir</string>
<string name="type_re_watching">Reassistindo</string>
<string name="play_movie_button">Assistir Filme</string>
<string name="play_movie_button">Reproduzir filme</string>
<string name="play_torrent_button">Transmitir Torrent</string>
<string name="pick_source">Fontes</string>
<string name="pick_subtitle">Legendas</string>
<string name="reload_error">Tentar reconectar</string>
<string name="go_back">Voltar</string>
<string name="play_episode">Assistir Episódio</string>
<string name="reload_error">Tentando conectar novamente</string>
<string name="go_back">Volte</string>
<string name="play_episode">Reproduzir episódio</string>
<!--<string name="need_storage">Permirtir baixar episódios</string>-->
<string name="download">Baixar</string>
<string name="downloaded">Baixado</string>
<string name="download">Download</string>
<string name="downloaded">Download concluído</string>
<string name="downloading">Baixando</string>
<string name="download_paused">Download Pausado</string>
<string name="download_started">Download Iniciado</string>
<string name="download_failed">Download Falhado</string>
<string name="download_canceled">Download Cancelado</string>
<string name="download_done">Download Finalizado</string>
<string name="download_paused">Download pausado</string>
<string name="download_started">Download iniciado</string>
<string name="download_failed">Download falhou</string>
<string name="download_canceled">Download cancelado</string>
<string name="download_done">Download concluído</string>
<string name="stream">Transmitir</string>
<string name="error_loading_links_toast">Erro Carregando Links</string>
<string name="download_storage_text">Armazenamento Interno</string>
<string name="error_loading_links_toast">Erro ao carregar links</string>
<string name="download_storage_text">Armazenamento interno</string>
<string name="app_dubbed_text">Dub</string>
<string name="app_subbed_text">Sub</string>
<string name="popup_delete_file">Deletar Arquivo</string>
<string name="popup_play_file">Assistir Arquivo</string>
<string name="popup_resume_download">Retomar Download</string>
<string name="popup_pause_download">Pausar Download</string>
<string name="pref_disable_acra">Desativar relatório automático de erros</string>
<string name="home_more_info">Mais info</string>
<string name="popup_delete_file">Deletar arquivo</string>
<string name="popup_play_file">Reproduzir arquivo</string>
<string name="popup_resume_download">Retomar download</string>
<string name="popup_pause_download">Pausar download</string>
<string name="pref_disable_acra">Desative o relatório automático de erros</string>
<string name="home_more_info">Mais informações</string>
<string name="home_expanded_hide">Esconder</string>
<string name="home_play">Assistir</string>
<string name="home_info">Info</string>
<string name="filter_bookmarks">Filtrar Marcadores</string>
<string name="home_play">Reproduzir</string>
<string name="home_info">Informações</string>
<string name="filter_bookmarks">Filtrar marcadores</string>
<string name="error_bookmarks_text">Marcadores</string>
<string name="action_remove_from_bookmarks">Remover</string>
<string name="action_add_to_bookmarks">Selecionar marcador</string>
<string name="action_add_to_bookmarks">Definir como assistido/não assistido</string>
<string name="sort_apply">Aplicar</string>
<string name="sort_copy">Copiar</string>
<string name="sort_close">Fechar</string>
<string name="sort_clear">Limpar</string>
<string name="sort_save">Salvar</string>
<string name="player_speed">Velocidade do Reprodutor</string>
<string name="subtitles_settings">Configurar Legendas</string>
<string name="subs_text_color">Cor do Texto</string>
<string name="subs_outline_color">Cor do Contorno</string>
<string name="subs_background_color">Cor do Fundo</string>
<string name="subs_window_color">Cor da Janela</string>
<string name="subs_edge_type">Tipo de Borda</string>
<string name="subs_subtitle_elevation">Elevação da Legenda</string>
<string name="player_speed">Velocidade de reprodução</string>
<string name="subtitles_settings">Configurações de legendas</string>
<string name="subs_text_color">Cor do texto</string>
<string name="subs_outline_color">Cor do contorno</string>
<string name="subs_background_color">Cor de fundo</string>
<string name="subs_window_color">Cor da janela</string>
<string name="subs_edge_type">Tipo de borda</string>
<string name="subs_subtitle_elevation">Elevação da legenda</string>
<string name="subs_font">Fonte</string>
<string name="subs_font_size">Tamanho da Fonte</string>
<string name="subs_font_size">Tamanho da fonte</string>
<string name="search_provider_text_providers">Pesquisar usando fornecedor</string>
<string name="search_provider_text_types">Pesquisar usando genêros</string>
<string name="search_provider_text_types">Pesquisar usando tipos</string>
<string name="benene_count_text">%d Benenes doados aos desenvolvedores</string>
<string name="benene_count_text_none">Nenhuma Benenes doada</string>
<string name="subs_auto_select_language">Autosseleção de Lingua</string>
<string name="subs_download_languages">Baixar Linguas</string>
<string name="subs_subtitle_languages">Lingua da legenda</string>
<string name="subs_hold_to_reset_to_default">Segure para retornar a configuração padrão</string>
<string name="subs_import_text" formatted="true">Importe fontes colocando elas em %s</string>
<string name="continue_watching">Continue Assistindo</string>
<string name="subs_auto_select_language">Seleção automática de idioma</string>
<string name="subs_download_languages">Baixar idiomas</string>
<string name="subs_subtitle_languages">Idioma da Legenda</string>
<string name="subs_hold_to_reset_to_default">Segure para redefinir para o padrão</string>
<string name="subs_import_text" formatted="true">Importe fontes colocando-as em %s</string>
<string name="continue_watching">Continuar assistindo</string>
<string name="action_remove_watching">Remover</string>
<string name="action_open_watching">Mais Info</string>
<string name="action_open_play">@string/home_play</string>
@ -122,8 +122,7 @@
<string name="player_subtitles_settings_des">Configurações de legendas do Player</string>
<string name="chromecast_subtitles_settings">Legendas do Chromecast</string>
<string name="chromecast_subtitles_settings_des">Configurações de legendas do Chromecast</string>
<string name="eigengraumode_settings">Modo Eigengravy</string>
<string name="eigengraumode_settings_des">Adiciona um botão de velocidade no player</string>
<string name="eigengraumode_settings">Velocidade de playback</string>
<string name="swipe_to_seek_settings">Deslize para avançar o vídeo</string>
<string name="swipe_to_seek_settings_des">Deslize de lado à lado para controlar a posição no vídeo</string>
<string name="swipe_to_change_settings">Deslize para mudar as configurações</string>
@ -145,8 +144,8 @@
<string name="backup_failed">Permissões de armazenamento faltando. Por favor tente novamente.</string>
<string name="backup_failed_error_format">Erro no backup de %s</string>
<string name="search">Procurar</string>
<string name="category_account">Contas</string>
<string name="category_updates">Atualizações e backup</string>
<string name="category_account">Contas e Segurança</string>
<string name="category_updates">Atualizações e Backup</string>
<string name="settings_info">Info</string>
<string name="advanced_search">Procura Avançada</string>
<string name="advanced_search_des">Mostrar resultados separados por fornecedor</string>
@ -167,7 +166,7 @@
<string name="discord">Junte-se ao Discord</string>
<string name="benene">Dar um benene para os desenvolvedores</string>
<string name="benene_des">Benene dada</string>
<string name="app_language">Linguagem do App</string>
<string name="app_language">Idioma do aplicativo</string>
<string name="no_chromecast_support_toast">Esse fornecedor não possui suporte para Chromecast</string>
<string name="no_links_found_toast">Nenhum link encontrado</string>
<string name="copy_link_toast">Link copiado para área de transferência</string>
@ -280,8 +279,8 @@
<string name="legal_notice_text" translatable="false">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.</string>
<string name="category_general">Geral</string>
<string name="random_button_settings">Botão Aleatório</string>
<string name="random_button_settings_desc">Mostra o botão Aleatório na página inicial</string>
<string name="provider_lang_settings">Linguagem dos fornecedores</string>
<string name="random_button_settings_desc">Mostrar botão aleatório na página inicial e na biblioteca</string>
<string name="provider_lang_settings">Linguagem das extensões</string>
<string name="app_layout">Layout do App</string>
<string name="preferred_media_settings">Mídia preferida</string>
<string name="subtitles_encoding">Codificação das legendas</string>
@ -296,11 +295,11 @@
<string name="bottom_title_settings_des">Coloca o título debaixo do poster</string>
<!-- account stuff -->
<string name="example_password">senha123</string>
<string name="example_username">MeuNomeLegal</string>
<string name="example_username">Nome de usuário</string>
<string name="example_email">oi@mundo.com</string>
<string name="example_ip">127.0.0.1</string>
<string name="example_site_name">MeuSiteLegal</string>
<string name="example_site_url">examplo.com</string>
<string name="example_site_name">NovoNomedoSite</string>
<string name="example_site_url">https://example.com</string>
<string name="example_lang_name">Codigo da Língua (bp)</string>
<!--
<string name="mal_account_settings" translatable="false">MAL</string>
@ -348,7 +347,7 @@
https://en.wikipedia.org/w/index.php?title=Pangram&oldid=225849300
https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog
-->
<string name="subtitles_example_text">Já fiz vinho com toque de kiwi para belga sexy.</string>
<string name="subtitles_example_text">A rápida raposa marrom salta sobre o cachorro preguiçoso</string>
<string name="recommended">Recomendada</string>
<string name="player_loaded_subtitles" formatted="true">%s carregada</string>
<string name="player_load_subtitles">Carregar de arquivo</string>
@ -419,8 +418,6 @@
<string name="plugins_not_downloaded" formatted="true">Não transferido: %d</string>
<string name="blank_repo_message">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.</string>
<string name="view_public_repositories_button">Ver repositórios da comunidade</string>
<string name="view_public_repositories_button_short">Lista pública</string>
@ -429,23 +426,23 @@
<string name="single_plugin_disabled" formatted="true">%s (Desativado)</string>
<string name="autoplay_next_settings">Reproduzir automaticamente próximo episódio</string>
<string name="autoplay_next_settings_des">Começa o próximo episódio quando o atual termina</string>
<string name="enable_nsfw_on_providers">Ativar NSFW em fornecedores compatíveis</string>
<string name="enable_nsfw_on_providers">Ativar NSFW em extensões compatíveis</string>
<string name="category_providers">Fornecedores</string>
<string name="revert">Reverter</string>
<string name="pref_category_actions">Ações</string>
<string name="already_voted">votou com sucesso</string>
<string name="update_notification_downloading">Baixando atualização do aplicativo…</string>
<string name="referer">Referencias</string>
<string name="referer">Referenciador (opcional)</string>
<string name="pref_category_app_updates">Atualizações do App</string>
<string name="play_with_app_name">Tocar com CloudStream</string>
<string name="play_with_app_name">Assistir com o CloudStream</string>
<string name="automatic_plugin_download_summary">Automaticamente instale todos os plugins não instalados dos repositórios adicionados.</string>
<string name="play_trailer_button">Reproduzir Trailer</string>
<string name="play_trailer_button">Reproduzir trailer</string>
<string name="browser">Navegador</string>
<string name="pref_category_backup">Copia de Segurança</string>
<string name="android_tv_interface_off_seek_settings_summary">A Barra de Progresso pode ser usada quando o player estiver oculto</string>
<string name="subscription_list_name">Inscrito</string>
<string name="empty_library_logged_in_message">Essa lista está vazia. Tente mudar para outra.</string>
<string name="play_livestream_button">Reproduzir Livestream</string>
<string name="play_livestream_button">Reproduzir transmissão ao vivo</string>
<string name="test_log">Log do Teste</string>
<string name="automatic_plugin_download">Baixar plugins automaticamente</string>
<string name="automatic_plugin_download_mode_title">Selecione o modo para filtrar os plugins baixados</string>
@ -476,7 +473,7 @@
<string name="nsfw_singular">Conteúdo +18</string>
<string name="help">Ajuda</string>
<string name="redo_setup_process">Processo de configuração de Redo</string>
<string name="update_notification_failed">Não pudemos instalar a nova versão do App</string>
<string name="update_notification_failed">Não foi possível instalar a nova versão do aplicativo</string>
<string name="apk_installer_package_installer">instalador de pacotes</string>
<string name="sort_by">Organizar por</string>
<string name="sort_rating_desc">Votação (Alta para Baixa)</string>
@ -493,7 +490,7 @@
<string name="safe_mode_file">Arquivo de modo de segurança encontrado!
\nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido.</string>
<string name="subscription_new">Inscrito em %s</string>
<string name="subscription_episode_released">Episódio %d lançado</string>
<string name="subscription_episode_released">Episódio %d lançado!</string>
<string name="set_default">Selecionar padrão</string>
<string name="subscription_deleted">Inscrição cancelada de %s</string>
<string name="apk_installer_settings_des">Alguns aparelhos não possuem suporte para este pacote de instalação. Tente a opção legada se a atualização não instalar.</string>
@ -544,7 +541,7 @@
<string name="player_settings_play_in_mpv">MPV</string>
<string name="skip_type_mixed_op">Abrindo mistura</string>
<string name="player_settings_play_in_vlc">VLC</string>
<string name="apply_on_restart">Aplicar quando reiniciar</string>
<string name="apply_on_restart">Reinicie o aplicativo para ver as alterações.</string>
<string name="safe_mode_crash_info">Visualização info de crash</string>
<string name="audio_tracks">Faixas de áudio</string>
<string name="sort_updated_new">Adicionado em (novo para antigo)</string>
@ -558,7 +555,7 @@
<string name="pref_category_looks">Aparência</string>
<string name="disable">Desativar</string>
<string name="use">Usar</string>
<string name="network_adress_example">Link da stream</string>
<string name="network_adress_example">https://example.com/example.mp4</string>
<string name="pref_category_gestures">Gestos</string>
<string name="plugin_downloaded">Plugin baixado</string>
<string name="jsdelivr_enabled">Não foi possível se conectar ao GitHub. Ativando proxy JsDelivr…</string>
@ -572,7 +569,74 @@
<string name="category_provider_test">Provedor de teste</string>
<string name="pref_category_player_layout">Layout</string>
<string name="pref_category_defaults">Padrões</string>
<string name="jsdelivr_proxy">Proxy: raw.githubusercontent.com</string>
<string name="jsdelivr_proxy_summary">Contorna o bloqueio do GitHub usando jsDelivr. Pode atrasar as atualizações por alguns dias.</string>
<string name="jsdelivr_proxy">Proxy do GitHub</string>
<string name="jsdelivr_proxy_summary">Contorne o bloqueio de URLs \"raw\" do GitHub usando jsDelivr. Pode atrasar as atualizações por alguns dias.</string>
<string name="pref_category_bypass">Rotas alternativas</string>
<string name="favorites_list_name">Favoritos</string>
<string name="favorite_added">%s adicionado aos favoritos</string>
<string name="duplicate_title">Duplicata em potencial encontrada</string>
<string name="duplicate_add">Adicionar</string>
<string name="duplicate_replace">Substituir</string>
<string name="duplicate_message_multiple" formatted="true">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?</string>
<string name="enter_pin">Insira o PIN</string>
<string name="enter_pin_with_name" formatted="true">Insira o PIN para %s</string>
<string name="enter_current_pin">Insira o PIN atual</string>
<string name="pin_error_incorrect">PIN incorreto. Por favor, tente novamente.</string>
<string name="pin_error_length">O PIN deve ter 4 caracteres</string>
<string name="select_an_account">Selecione uma conta</string>
<string name="manage_accounts">Gerenciar contas</string>
<string name="skip_startup_account_select_pref">Ignorar a seleção da conta na inicialização</string>
<string name="rotate_video_desc">Exibir um botão para alternar a orientação da tela</string>
<string name="favorite_removed">%s removido dos favoritos</string>
<string name="action_add_to_favorites">Adicionar aos favoritos</string>
<string name="action_remove_from_favorites">Remover dos favoritos</string>
<string name="rotate_video">Girar</string>
<string name="lock_profile">Bloquear perfil</string>
<string name="pin">PIN</string>
<string name="links_reloaded_toast">Links recarregados</string>
<string name="backup_frequency">Frequência de backup</string>
<string name="duplicate_replace_all">Substitua tudo</string>
<string name="duplicate_message_single" formatted="true">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?</string>
<string name="action_subscribe">Inscrever-se</string>
<string name="action_unsubscribe">Cancelar inscrição</string>
<string name="use_default_account">Usar conta padrão</string>
<string name="edit_account">Editar conta</string>
<string name="logged_account" formatted="true">Conectado como %s</string>
<string name="auto_rotate_video_desc">Habilite a troca automática de orientação da tela com base na orientação do vídeo</string>
<string name="auto_rotate_video">Rotação automática</string>
<string name="subscribe_tooltip">Notificação de novo episódio</string>
<string name="result_search_tooltip">Pesquisar em outras extensões</string>
<string name="recommendations_tooltip">Mostrar recomendações</string>
<string name="speed_setting_summary">Adiciona uma opção de velocidade no reprodutor</string>
<string name="test_extensions">Testar todas as extensões</string>
<string name="test_extensions_summary">Esse teste é feito somente para desenvolvedores e não verifica ou nega o funcionamento de qualquer extensão.</string>
<string name="biometric_authentication_title">Desbloquear CloudStream</string>
<string name="biometric_warning">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.</string>
<string name="biometric_setting">Bloquear com Biometria</string>
<string name="password_pin_authentication_title">Autenticação de Senha/PIN</string>
<string name="biometric_unsupported">A autenticação biométrica não é compatível com este dispositivo</string>
<string name="biometric_setting_summary">Desbloquear o aplicativo com impressão digital, ID facial, PIN, padrão e senha.</string>
<string name="biometric_prompt_description">Esta tela foi fechada devido a diversas tentativas malsucedidas. Por favor reinicie o aplicativo.</string>
<string name="resume_remaining" formatted="true">%s
\nrestante(s)</string>
<string name="favorite">Favorito</string>
<string name="unfavorite">Não favorito</string>
<string name="toast_copied">copiado!</string>
<string name="clipboard_permission_error">Erro ao acessar a área de transferência. Tente novamente.</string>
<string name="repo_copy_label">Nome e URL do repositório</string>
<string name="clipboard_unknown_error">Erro ao copiar. Copie o logcat e entre em contato com o suporte do aplicativo.</string>
<string name="battery_dialog_message">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.</string>
<string name="ok">Ok</string>
<string name="battery_dialog_title">Desativar otimização de bateria</string>
<string name="app_unrestricted_toast">O uso da bateria do app já está definido como irrestrito</string>
<string name="app_info_intent_error">Não foi possível abrir as informações do aplicativo CloudStream.</string>
<string name="music_singlar">Música</string>
<string name="audio_book_singular">Áudio-livro</string>
<string name="custom_media_singluar">Mídia</string>
</resources>

View File

@ -117,8 +117,7 @@
<string name="player_subtitles_settings_des">Nastavení titulků přehrávače</string>
<string name="chromecast_subtitles_settings">Titulky Chromecastu</string>
<string name="chromecast_subtitles_settings_des">Natavení titulků Chromecastu</string>
<string name="eigengraumode_settings">Rychlostní režim</string>
<string name="eigengraumode_settings_des">Přidá do přehrávače možnost rychlosti</string>
<string name="eigengraumode_settings">Rychlost přehrávání</string>
<string name="swipe_to_seek_settings">Přejet pro posun</string>
<string name="swipe_to_seek_settings_des">Přejeďte prstem ze strany na stranu pro ovládání své pozice ve videu</string>
<string name="swipe_to_change_settings">Přejet pro změnu nastavení</string>
@ -140,7 +139,7 @@
<string name="backup_failed">Chybí oprávnění k úložišti. Zkuste to prosím znovu.</string>
<string name="backup_failed_error_format">Chyba při zálohování %s</string>
<string name="search">Search</string>
<string name="category_account">Účty</string>
<string name="category_account">Účty a zabezpečení</string>
<string name="category_updates">Aktualizace a záloha</string>
<string name="settings_info">Informace</string>
<string name="advanced_search">Pokročilé hledání</string>
@ -266,7 +265,7 @@
<string name="category_general">Obecné</string>
<string name="random_button_settings">Náhodné tlačítko</string>
<string name="random_button_settings_desc">Zobrazit na domovské stránce a v knihovně náhodné tlačítko</string>
<string name="provider_lang_settings">Jazyk poskytovatelů</string>
<string name="provider_lang_settings">Jazyky rozšíření</string>
<string name="app_layout">Rozložení aplikace</string>
<string name="preferred_media_settings">Preferovaná média</string>
<string name="subtitles_encoding">Kódování titulků</string>
@ -281,7 +280,7 @@
<string name="bottom_title_settings_des">Umístit název pod plakát</string>
<!-- account stuff -->
<string name="example_password">heslo123</string>
<string name="example_username">MojeSuperJmeno</string>
<string name="example_username">Uživatelské jméno</string>
<string name="example_email">ahoj@svete.cz</string>
<string name="example_ip">127.0.0.1</string>
<!--
@ -384,7 +383,7 @@
<string name="plugins_downloaded" formatted="true">Staženo: %d</string>
<string name="audio_tracks">Zvukové stopy</string>
<string name="video_tracks">Videostopy</string>
<string name="apply_on_restart">Použít při restartu</string>
<string name="apply_on_restart">Restartujte aplikaci pro použití změn.</string>
<string name="safe_mode_title">Bezpečný režim povolen</string>
<string name="extension_size">Velikost</string>
<string name="extension_authors">Autoři</string>
@ -393,7 +392,7 @@
<string name="automatic_plugin_download_summary">Automaticky instalovat všechny dosud nenainstalované doplňky z přidaných repozitářů.</string>
<string name="others">Ostatní</string>
<string name="trailer">Trailer</string>
<string name="network_adress_example">Odkaz na stream</string>
<string name="network_adress_example">https://example.com/priklad.mp4</string>
<string name="skip_setup">Přeskočit nastavení</string>
<string name="add_repository">Přidat repozitář</string>
<string name="plugin_deleted">Doplněk odstraněn</string>
@ -427,8 +426,8 @@
<string name="pref_category_looks">Vzhled</string>
<string name="pref_category_links">Odkazy</string>
<string name="pref_category_ui_features">Funkce</string>
<string name="example_site_name">MůjSuperWeb</string>
<string name="enable_nsfw_on_providers">Povolit NSFW u podporovaných poskytovatelů</string>
<string name="example_site_name">NovýNázevWebu</string>
<string name="enable_nsfw_on_providers">Povolit NSFW u podporovaných rozšíření</string>
<string name="category_providers">Poskytovatelé</string>
<string name="crash_reporting_title">Hlášení pádů</string>
<string name="previous">Předchozí</string>
@ -437,11 +436,9 @@
<string name="plugin_downloaded">Doplněk stažen</string>
<string name="is_adult">18+</string>
<string name="batch_download_start_format" formatted="true">Spuštěno stahování %1$d %2$s…</string>
<string name="blank_repo_message">CloudStream nemá ve výchozím nastavení nainstalované žádné weby. Stránky je třeba nainstalovat z úložišť.
<string name="blank_repo_message">CloudStream nemá ve výchozím nastavení nainstalované žádné zdroje. Je třeba je nainstalovat z repozitářů.
\n
\nKvůli nesmyslnému podání stížnosti DMCA společností Sky UK Limited 🤮 nemůžeme v aplikaci propojit stránky repozitářů.
\n
\nPřipojte se k našemu Discordu nebo hledejte na internetu.</string>
\nPřipojte se na náš Discord nebo hledejte na internetu.</string>
<string name="plugins_disabled" formatted="true">Zakázáno: %d</string>
<string name="plugins_updated" formatted="true">Aktualizováno %d doplňků</string>
<string name="safe_mode_crash_info">Zobrazit informace o pádu</string>
@ -490,7 +487,7 @@
<string name="pref_category_gestures">Gesta</string>
<string name="add_site_pref">Klonovat web</string>
<string name="add_site_summary">Přidat klon existujícího webu s jinou adresou URL</string>
<string name="example_site_url">example.com</string>
<string name="example_site_url">https://example.com</string>
<string name="example_lang_name">Kód jazyka (cs)</string>
<string name="download_all_plugins_from_repo">Stáhnout všechny doplňky z tohoto repozitáře\?</string>
<string name="single_plugin_disabled" formatted="true">%s (zakázáno)</string>
@ -513,11 +510,11 @@
<string name="next_episode_time_day_format" formatted="true">%1$dd %2$dh %3$dm</string>
<string name="view_public_repositories_button">Zobrazit komunitní repozitáře</string>
<string name="update_started">Aktualizace zahájena</string>
<string name="stream">Stream</string>
<string name="stream">Síťový stream</string>
<string name="autoplay_next_settings">Automaticky přehrát další epizodu</string>
<string name="livestreams">Živé přenosy</string>
<string name="subtitles_filter_lang">Filtrování podle preferovaného jazyka médií</string>
<string name="referer">Referent</string>
<string name="referer">Referent (volitelný)</string>
<string name="next">Další</string>
<string name="provider_languages_tip">Sledovat videa v těchto jazycích</string>
<string name="batch_download_finish_format" formatted="true">Staženo %1$d %2$s</string>
@ -546,11 +543,11 @@
<string name="subscription_deleted">Odhlášen odběr od %s</string>
<string name="subscription_episode_released">Byla vydána epizoda %d!</string>
<string name="subscription_list_name">Odebíráno</string>
<string name="jsdelivr_proxy">Proxy raw.githubusercontent.com</string>
<string name="jsdelivr_proxy">Proxy GitHub</string>
<string name="jsdelivr_enabled">Nelze se připojit k serveru GitHub. Zapínání proxy jsDelivr…</string>
<string name="watch_quality_pref_data">Upřednostněná kvalita sledování (mobilní data)</string>
<string name="revert">Vrátit zpět</string>
<string name="jsdelivr_proxy_summary">Obchází blokování GitHubu pomocí jsDelivr. Může způsobit zpoždění aktualizací o několik dní.</string>
<string name="jsdelivr_proxy_summary">Obejít blokování přímých adres GitHub pomocí jsDelivr. Může způsobit zpoždění aktualizací o několik dní.</string>
<string name="pref_category_bypass">Obcházení ISP</string>
<string name="profile_number">Profil %d</string>
<string name="wifi">Wi-Fi</string>
@ -615,5 +612,33 @@
<string name="auto_rotate_video">Automatické otáčení</string>
<string name="rotate_video">Otočení</string>
<string name="auto_rotate_video_desc">Zapnout automatické otáčení obrazovky v závislosti na orientaci videa</string>
<string name="copyTitle">Název zkopírován!</string>
<string name="test_extensions">Otestovat všechna rozšíření</string>
<string name="test_extensions_summary">Tento test slouží pouze pro vývojáře a neověřuje funkčnost jakéhokoli rozšíření.</string>
<string name="result_search_tooltip">Hledat v ostatních rozšířeních</string>
<string name="subscribe_tooltip">Oznámení o nové epizodě</string>
<string name="recommendations_tooltip">Zobrazit doporučené</string>
<string name="speed_setting_summary">Přidá nastavení rychlosti do přehrávače</string>
<string name="biometric_authentication_title">Odemknout CloudStream</string>
<string name="biometric_setting">Zamknout biometrikou</string>
<string name="password_pin_authentication_title">Ověření heslem/PINem</string>
<string name="biometric_unsupported">Biometrické ověření není na tomto zařízení podporováno</string>
<string name="biometric_setting_summary">Odemkněte aplikaci otiskem prstu, obličejem, PINem, gestem nebo heslem.</string>
<string name="biometric_prompt_description">Tato obrazovka byla po několika nezdařilých pokusech uzavřena. Restartujte prosím aplikaci.</string>
<string name="biometric_warning">Vaše data z aplikace CloudStream byla nyní zálohována. Ačkoli je tato možnost velmi malá, různá zařízení se mohou chovat různě. Ve výjimečném případě, že se vám přístup k aplikaci zablokuje, data aplikace zcela vymažte a obnovte je ze zálohy. Velmi se omlouváme za případné nepříjemnosti z toho plynoucí.</string>
<string name="unfavorite">Odebrat z oblíbených</string>
<string name="resume_remaining" formatted="true">%s
\nzbývá</string>
<string name="favorite">Přidat do oblíbených</string>
<string name="repo_copy_label">Název a adresa repozitáře</string>
<string name="clipboard_unknown_error">Chyba při kopírování, zkopírujte prosím protokol a kontaktujte podporu aplikace.</string>
<string name="toast_copied">Zkopírováno!</string>
<string name="clipboard_permission_error">Chyba při přístupu ke schránce, zkuste to prosím znovu.</string>
<string name="ok">OK</string>
<string name="app_unrestricted_toast">Využití baterie aplikací je již nastaveno na neomezené</string>
<string name="app_info_intent_error">Nepodařilo se otevřít informace o aplikaci CloudStream.</string>
<string name="music_singlar">Hudba</string>
<string name="custom_media_singluar">Média</string>
<string name="battery_dialog_title">Zakažte optimalizace baterie</string>
<string name="battery_dialog_message">Aby bylo zajištěno nepřetržité stahování a upozornění na odebírané seriály, potřebuje aplikace CloudStream povolení ke spuštění na pozadí. Stisknutím tlačítka OK budete přesměrováni na informace o aplikaci. Tam přejděte na položku Využití baterie aplikací a nastavte možnost Využití baterie na hodnotu Neomezené. Upozorňujeme, že toto povolení neznamená, že CS3 bude vybíjet baterii. Na pozadí bude pracovat pouze v případě potřeby, například při přijímání oznámení nebo stahování videí z oficiálních rozšíření. Pokud se rozhodnete toto nastavení zrušit, můžete jej později upravit v Obecných nastaveních.</string>
<string name="audio_book_singular">Audiokniha</string>
</resources>

View File

@ -69,7 +69,7 @@
<string name="download_failed">Download fehlgeschlagen</string>
<string name="download_canceled">Download abgebrochen</string>
<string name="download_done">Download abgeschlossen</string>
<string name="stream">Stream</string>
<string name="stream">Netzwerk-Stream</string>
<string name="error_loading_links_toast">Fehler beim Laden von Links</string>
<string name="download_storage_text">Interner Speicher</string>
<string name="app_dubbed_text">Dub</string>
@ -129,8 +129,7 @@
<string name="player_subtitles_settings_des">Player-Untertiteleinstellungen</string>
<string name="chromecast_subtitles_settings">Chromecast-Untertitel</string>
<string name="chromecast_subtitles_settings_des">Chromecast-Untertiteleinstellungen</string>
<string name="eigengraumode_settings">Eigengravy-Modus</string>
<string name="eigengraumode_settings_des">Fügt eine Geschwindigkeitsoption im Player hinzu</string>
<string name="eigengraumode_settings">Wiedergabegeschwindigkeit</string>
<string name="swipe_to_seek_settings">Wischen zum vor- und zurückspulen</string>
<string name="swipe_to_seek_settings_des">Nach links oder rechts wischen, um die Zeit im Videoplayer zu steuern</string>
<string name="swipe_to_change_settings">Wischen, um Einstellungen zu ändern</string>
@ -151,7 +150,7 @@
<string name="restore_success">Sicherungsdatei geladen</string>
<string name="backup_failed">Speicherberechtigungen fehlen. Bitte erneut versuchen.</string>
<string name="search">Suche</string>
<string name="category_account">Konten</string>
<string name="category_account">Konten und Sicherheit</string>
<string name="category_updates">Updates und Datensicherung</string>
<string name="settings_info">Info</string>
<string name="advanced_search">Erweiterte Suche</string>
@ -285,11 +284,11 @@
<string name="legal_notice">Haftungsausschluss</string>
<string name="category_general">Allgemein</string>
<string name="random_button_settings">Zufalls-Button</string>
<string name="random_button_settings_desc">Zeige Zufallsgenerator Schaltfläche auf der Startseite</string>
<string name="provider_lang_settings">Anbieter-Sprachen</string>
<string name="random_button_settings_desc">Zeige Knopf mit Zufallsgenerator auf Startseite und Bibliothek</string>
<string name="provider_lang_settings">Erweiterungssprachen</string>
<string name="app_layout">App-Layout</string>
<string name="preferred_media_settings">Bevorzugte Medien</string>
<string name="enable_nsfw_on_providers">NSFW bei unterstützten Anbietern aktivieren</string>
<string name="enable_nsfw_on_providers">NSFW bei unterstützten Erweiterungen aktivieren</string>
<string name="subtitles_encoding">Untertitel-Kodierung</string>
<string name="category_providers">Anbieter</string>
<string name="category_ui">Layout</string>
@ -302,11 +301,11 @@
<string name="bottom_title_settings">Vorschaubildtitel Platzierung</string>
<string name="bottom_title_settings_des">Titel unter Vorschaubild platzieren</string>
<string name="example_password">passwort123</string>
<string name="example_username">MeinCoolerUsername</string>
<string name="example_username">Benutzername</string>
<string name="example_email">hello@world.com</string>
<string name="example_ip">127.0.0.1</string>
<string name="example_site_name">MeineCooleSeite</string>
<string name="example_site_url">example.com</string>
<string name="example_site_name">NeuerSeitenNamen</string>
<string name="example_site_url">https://example.com</string>
<string name="example_lang_name">Sprachencode (en)</string>
<string name="login_format" formatted="true">%1$s %2$s</string>
<string name="account">Account</string>
@ -381,8 +380,8 @@
<string name="subtitles_filter_lang">Nach bevorzugter Mediensprache filtern</string>
<string name="extras">Extras</string>
<string name="trailer">Trailer</string>
<string name="network_adress_example">Link zum Stream</string>
<string name="referer">Referent</string>
<string name="network_adress_example">https://example.com/example.mp4</string>
<string name="referer">Referent (optional)</string>
<string name="next">Weiter</string>
<string name="provider_languages_tip">Videos in diesen Sprachen ansehen</string>
<string name="previous">Vorherige</string>
@ -413,8 +412,6 @@
<string name="plugins_not_downloaded" formatted="true">Nicht heruntergeladen: %d</string>
<string name="blank_repo_message">CloudStream hat standardmäßig keine Websites installiert. Websites müssen aus Repositories installiert werden.
\n
\nAufgrund eines hirnlosen DMCA-Takedowns durch Sky UK Limited 🤮 können wir die Repository-Site nicht in der App verlinken.
\n
\nTrete unserem Discord Server bei oder suche online.</string>
<string name="view_public_repositories_button">Community-Repositories anzeigen</string>
<string name="view_public_repositories_button_short">Öffentliche Liste</string>
@ -518,11 +515,11 @@
<string name="start">Start</string>
<string name="restart">Neustarten</string>
<string name="watch_quality_pref_data">Bevorzugte Videoqualität (mobile Daten)</string>
<string name="jsdelivr_proxy_summary">Umgehung der GitHub Sperre mit jsdelivr. Kann zu einigen Tagen Verzögerung bei Updates führen.</string>
<string name="jsdelivr_proxy_summary">Umgehung der Sperre von GitHub-URLs mit jsdelivr. Kann zu einigen Tagen Verzögerung bei Updates führen.</string>
<string name="subscription_new">%s abonniert</string>
<string name="subscription_deleted">%s deabonniert</string>
<string name="subscription_episode_released">Episode %d erschienen!</string>
<string name="jsdelivr_proxy">raw.githubusercontent.com Proxy</string>
<string name="jsdelivr_proxy">GitHub Proxy</string>
<string name="jsdelivr_enabled">GitHub konnte nicht erreicht werden. Der jsDelivr-Proxy wird aktiviert …</string>
<string name="subscription_in_progress_notification">Abonnierte Serien werden aktualisiert</string>
<string name="revert">Rückgängig</string>
@ -576,17 +573,38 @@
<string name="skip_startup_account_select_pref">Kontoauswahl beim Starten überspringen</string>
<string name="manage_accounts">Konten verwalten</string>
<string name="edit_account">Konto bearbeiten</string>
<string name="duplicate_message_multiple" formatted="true">Es wurden potentielle Duplikate in ihrer Bibliothek gefunden:
<string name="duplicate_message_multiple" formatted="true">Es wurden potentielle Duplikate in deiner Bibliothek gefunden:
\n
\n%s
\n
\nWollen sie dieses Element trotzdem hinzufügen, das existierende Element ersetzen oder diese Aktion abbrechen?</string>
<string name="duplicate_message_single" formatted="true">Ihre Bibliothek enthält möglicherweise schon ein Duplikat dieses Elements: \'%s\'
\nMöchtest du dieses Element dennoch hinzufügen, das existierende ersetzen oder diese Aktion abbrechen?</string>
<string name="duplicate_message_single" formatted="true">Deine Bibliothek enthält möglicherweise schon ein Duplikat dieses Elements: \'%s\'
\n
\nWollen sie dieses Element trotzdem hinzufügen, das existierende ersetzen oder diese Aktion abbrechen?</string>
\nMöchtest du dieses Element dennoch hinzufügen, das existierende ersetzen oder diese Aktion abbrechen?</string>
<string name="links_reloaded_toast">Links wurden neu geladen</string>
<string name="rotate_video">Drehen</string>
<string name="rotate_video_desc">Zeige einen Umschalter für Bildschirmorientierung an</string>
<string name="auto_rotate_video">Automatisch drehen</string>
<string name="copyTitle">Titel kopiert!</string>
<string name="result_search_tooltip">In anderen Erweiterungen suchen</string>
<string name="recommendations_tooltip">Empfehlungen anzeigen</string>
<string name="speed_setting_summary">Fügt dem Player eine Geschwindigkeitsoption hinzu</string>
<string name="subscribe_tooltip">Benachrichtigung für neue Episoden</string>
<string name="auto_rotate_video_desc">Automatisches Umschalten der Bildschirmausrichtung basierend auf der Videoausrichtung aktivieren</string>
<string name="test_extensions">Teste alle Erweiterungen</string>
<string name="test_extensions_summary">Dieser Test ist nur für Entwickler gedacht und bestimmt nicht die Funktionsfähigkeit einer Erweiterung.</string>
<string name="biometric_authentication_title">Entsperre CloudStream</string>
<string name="biometric_setting">Sperre mit Biometric</string>
<string name="password_pin_authentication_title">Passwort/PIN Authentifizierung</string>
<string name="biometric_unsupported">Biometrische Authentifizierung wird auf diesem Gerät nicht unterstützt</string>
<string name="biometric_setting_summary">Entsperre die App via Fingerabdruck, FaceID, PIN, Muster und Passwort.</string>
<string name="unfavorite">kein Favorit</string>
<string name="biometric_prompt_description">Dieser Bildschirm wurde nach einigen Fehlversuchen geschlossen. Starte die App neu.</string>
<string name="biometric_warning">Ihre CloudStream-Daten wurden gesichert. Obwohl die Wahrscheinlichkeit dieses seltenen Falles sehr gering ist, verhalten sich alle Geräte unterschiedlich. Falls Sie im schlimmsten Fall den Zugriff zur App verlieren, löschen Sie die App-Daten vollständig und stellen Sie die Sicherung wieder her. Jegliche Unannehmlichkeiten, die Ihnen dadurch entstehen, bedauern wir sehr.</string>
<string name="resume_remaining" formatted="true">%s
\nausstehend</string>
<string name="favorite">Favorit</string>
<string name="toast_copied">Kopiert!</string>
<string name="clipboard_unknown_error">Beim kopieren ist ein Fehler aufgetreten, bitte kopieren sie logical und wenden sich an den Support.</string>
<string name="clipboard_permission_error">Fehler beim zugriff auf die Zwischenablage, bitte erneut versuchen.</string>
<string name="repo_copy_label">Repository Name und URL</string>
</resources>

View File

@ -92,7 +92,6 @@
<string name="chromecast_subtitles_settings">Υπότιτλοι για Chromecast</string>
<string name="chromecast_subtitles_settings_des">Ρυθμίσεις υποτίτλων για Chromecast</string>
<string name="eigengraumode_settings">Eigengravy Mode</string>
<string name="eigengraumode_settings_des">Προσθέτει την επιλογή ταχύτητας στο πρόγραμμα αναπαραγωγής</string>
<string name="swipe_to_seek_settings">Σύρετε για αναζήτηση</string>
<string name="swipe_to_seek_settings_des">Σύρετε από πλευρά σε πλευρά για να ελέγξετε το σημείο του βίντεο στο οποίο βρίσκεστε</string>
<string name="swipe_to_change_settings">Σύρετε για να αλλάξετε ρυθμίσεις</string>

View File

@ -77,4 +77,54 @@
<string name="player_speed_text_format" formatted="true">Rapido (%.2fx)</string>
<string name="search_hint">Serĉi…</string>
<string name="download">Elŝuti</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%1$s Ep %2$d</string>
<string name="cast_format" formatted="true">Rolantaro: %s</string>
<string name="next_episode_format" formatted="true">Epizodo %d estos publikigita en</string>
<string name="next_episode_time_day_format" formatted="true">%1$dt %2$dh %3$dm</string>
<string name="next_episode_time_hour_format" formatted="true">%1$dh %2$dm</string>
<string name="next_episode_time_min_format" formatted="true">%dm</string>
<string name="new_update_format" formatted="true">Nova ĝisdatigo trovita!
\n%1$s -&gt; %2$s</string>
<string name="filler" formatted="true">Speciala epizodo</string>
<string name="app_name">CloudStream</string>
<string name="download_started">Elŝuto Komencite</string>
<string name="reload_error">Reprovi konekton…</string>
<string name="stream">Elsendfluo</string>
<string name="home_next_random_img_des">Sekva Hazarda</string>
<string name="download_canceled">Elŝuto Nuligite</string>
<string name="episode_more_options_des">Pli da Opcioj</string>
<string name="next_episode">Sekva epizodo</string>
<string name="search_poster_img_des">Afiŝo</string>
<string name="play_episode">Ludi Epizodon</string>
<string name="preview_background_img_des">Antaŭrigardi Fono</string>
<string name="home_change_provider_img_des">Ŝanĝi Provizanton</string>
<string name="rated_format" formatted="true">Rangita: %.1f</string>
<string name="search_hint_site" formatted="true">Serĉi %s…</string>
<string name="no_data">Neniuj Datumoj</string>
<string name="type_watching">Spektante</string>
<string name="type_on_hold">En paŭzo</string>
<string name="type_completed">Kompletigita</string>
<string name="type_re_watching">Re-spektante</string>
<string name="play_torrent_button">Elsendflui Torenton</string>
<string name="download_done">Elŝuto Farite</string>
<string name="update_started">Ĝisdatigo Komencite</string>
<string name="error_loading_links_toast">Eraro okazis dum la Ŝarĝado de Ligiloj</string>
<string name="type_dropped">Forlasita</string>
<string name="duration_format" formatted="true">%d min</string>
<string name="play_with_app_name">Ludi per CloudStream</string>
<string name="result_share">Kunhavigi</string>
<string name="play_livestream_button">Ludi tuj-elsendfluon</string>
<string name="download_paused">Elŝuto Paŭzite</string>
<string name="pick_subtitle">Subtekstoj</string>
<string name="skip_loading">Preterpasi la ŝarĝado</string>
<string name="browser">Retumilo</string>
<string name="loading">Ŝarĝante…</string>
<string name="type_plan_to_watch">Plani Spekti</string>
<string name="play_trailer_button">Ludi Antaŭfilmon</string>
<string name="episode_poster_img_des">Epizoda Afiŝo</string>
<string name="home_main_poster_img_des">Ĉefa Afiŝo</string>
<string name="result_open_in_browser">Malfermi en Retumilo</string>
<string name="downloaded">Elŝutite</string>
<string name="downloading">Elŝutante</string>
<string name="download_failed">Elŝuto Malsukcesite</string>
</resources>

View File

@ -152,7 +152,7 @@
<string name="download_failed">Descarga fallida</string>
<string name="download_canceled">Descarga Cancelada</string>
<string name="download_done">Descarga Finalizada</string>
<string name="stream">Transmitir</string>
<string name="stream">Flujo de datos</string>
<string name="error_loading_links_toast">Error cargando enlaces</string>
<string name="download_storage_text">Almacenamiento Interno</string>
<string name="app_dubbed_text">Doblado</string>
@ -179,7 +179,6 @@
<string name="picture_in_picture_des">Continúa la reproducción en un reproductor miniatura encima de otras aplicaciones</string>
<string name="player_size_settings">Botón de cambio de tamaño del reproductor</string>
<string name="player_size_settings_des">Eliminar bordes negros</string>
<string name="eigengraumode_settings_des">Agrega la opción de velocidad en el reproductor</string>
<string name="subs_auto_select_language">Seleccionar idioma automáticamente</string>
<string name="subs_download_languages">Descargar Idiomas</string>
<string name="subs_subtitle_languages">Idioma del subtítulo</string>
@ -200,7 +199,7 @@
<string name="normal_no_plot">Trama no encontrada</string>
<string name="torrent_no_plot">Descripción no encontrada</string>
<string name="show_log_cat">Mostrar Logcat 🐈</string>
<string name="eigengraumode_settings">Modo Eigengravy</string>
<string name="eigengraumode_settings">Velocidad de reproducción</string>
<string name="swipe_to_seek_settings">Deslice para avanzar/retroceder</string>
<string name="swipe_to_change_settings">Deslice para cambiar la configuración</string>
<string name="swipe_to_change_settings_des">Deslice hacia arriba o hacia abajo en el lado izquierdo o derecho para cambiar el brillo o el volumen</string>
@ -226,7 +225,7 @@
<string name="backup_failed">Faltan permisos de almacenamiento. Por favor intente de nuevo.</string>
<string name="backup_failed_error_format">Error de backup de %s</string>
<string name="search">Buscar</string>
<string name="category_account">Cuentas</string>
<string name="category_account">Cuentas y seguridad</string>
<string name="category_updates">Actualizaciones y copias de seguridad</string>
<string name="settings_info">Información</string>
<string name="advanced_search">Búsqueda Avanzada</string>
@ -271,27 +270,25 @@
<string name="pref_category_app_updates">Actualizaciones de la aplicación</string>
<string name="automatic">Automático</string>
<string name="pref_category_actions">Acciones</string>
<string name="enable_nsfw_on_providers">Activar NSFW (contenido adulto) en proveedores soportados</string>
<string name="enable_nsfw_on_providers">Activar NSFW en las extensiones compatibles</string>
<string name="category_providers">Proveedores</string>
<string name="pref_category_defaults">Predeterminados</string>
<string name="preferred_media_settings">Medios preferidos</string>
<string name="pref_category_gestures">Gestos</string>
<string name="provider_lang_settings">Idiomas del proveedor</string>
<string name="provider_lang_settings">Idiomas de la extensión</string>
<string name="player_loaded_subtitles" formatted="true">%s Cargado</string>
<string name="subtitles_remove_bloat">Eliminar excedente (bloat) de los subtítulos</string>
<string name="skip_setup">Omitir configuración</string>
<string name="preferred_media_subtext">Qué quieres ver</string>
<string name="uppercase_all_subtitles">Poner en MAYÚSCULAS todos los subtítulos</string>
<string name="apply_on_restart">Se aplicarán los cambios al reiniciar la App</string>
<string name="apply_on_restart">Se aplicarán los cambios al reiniciar la App.</string>
<string name="player_settings_play_in_app">Reproductor interno</string>
<string name="extension_language">Idioma</string>
<string name="apk_installer_legacy">Legacy (método antiguo)</string>
<string name="apk_installer_package_installer">Instalador de paquetes</string>
<string name="blank_repo_message">CloudStream no tiene sitios instalados de forma predeterminada. Necesita instalarlos desde repositorios.
<string name="blank_repo_message">CloudStream no tiene sitios instalados por defecto. Necesitas instalar los sitios desde los repositorios.
\n
\nDebido a que dieron de baja el proyecto de manera arbitraria por infracción a la lay de derechos de autor (DMCA) de parte de Sky UK Limited 🤮, no podemos vincular el sitio del repositorio en la aplicación.
\n
\nÚnete a nuestro Discord o busca en línea.</string>
\nÚnase a nuestro Discord o busque en línea.</string>
<string name="download_all_plugins_from_repo">¿Descargar todos los plugins de este repositorio?</string>
<string name="updates_settings">Mostrar actualizaciones de la aplicación</string>
<string name="apk_installer_settings">Instalador de APK</string>
@ -401,10 +398,10 @@
<string name="bottom_title_settings">Ubicación del título en el póster</string>
<string name="bottom_title_settings_des">Coloca el título debajo del póster</string>
<string name="example_password">contraseña123</string>
<string name="example_username">MiNombreDeUsuarioGenial</string>
<string name="example_username">Nombre de usuario</string>
<string name="example_ip">127.0.0.1</string>
<string name="example_site_name">MiSitioGenial</string>
<string name="example_site_url">ejemplo.com</string>
<string name="example_site_name">NewSiteName</string>
<string name="example_site_url">https://example.com</string>
<string name="sync_score_format" formatted="true">%d / 10</string>
<string name="none">Ninguno</string>
<string name="max">Máximo</string>
@ -441,8 +438,8 @@
<string name="pref_category_cache">Caché</string>
<string name="create_account">Crear cuenta</string>
<string name="error_invalid_url">URL Inválida</string>
<string name="network_adress_example">Enlace a la transmisión</string>
<string name="referer">Referente</string>
<string name="network_adress_example">https://example.com/example.mp4</string>
<string name="referer">Referencia (opcional)</string>
<string name="next">Siguiente</string>
<string name="previous">Anterior</string>
<string name="setup_done">Hecho</string>
@ -522,9 +519,9 @@
<string name="subscription_deleted">Darse de baja de %s</string>
<string name="subscription_in_progress_notification">Actualizando los programas suscritos</string>
<string name="subscription_episode_released">¡Episodio %d publicado!</string>
<string name="jsdelivr_proxy">Proxy raw.githubusercontent.com</string>
<string name="jsdelivr_proxy">Proxy de GitHub</string>
<string name="jsdelivr_enabled">No se ha podido acceder a GitHub. Activando el proxy jsDelivr…</string>
<string name="jsdelivr_proxy_summary">Omite el bloqueo de GitHub mediante jsDelivr. Lo que puede provocar que las actualizaciones se retrasen unos días.</string>
<string name="jsdelivr_proxy_summary">Eludir el bloqueo de URLs crudas de github usando jsDelivr. Puede causar que las actualizaciones se retrasen unos días.</string>
<string name="revert">Revertir</string>
<string name="pref_category_bypass">ISP Bypasses</string>
<string name="watch_quality_pref_data">Calidad de visualización preferida (Datos móviles)</string>
@ -591,5 +588,33 @@
<string name="auto_rotate_video">Giro automático</string>
<string name="rotate_video">Girar</string>
<string name="auto_rotate_video_desc">Activar el cambio automático de la orientación de la pantalla en función de la orientación del vídeo</string>
<string name="copyTitle">¡Título copiado!</string>
<string name="test_extensions_summary">Esta prueba está destinada únicamente a desarrolladores y no verifica ni niega el funcionamiento de ninguna extensión.</string>
<string name="subscribe_tooltip">Notificación de nuevo episodio</string>
<string name="result_search_tooltip">Buscar en otras extensiones</string>
<string name="recommendations_tooltip">Mostrar recomendaciones</string>
<string name="speed_setting_summary">Añade una opción de velocidad en el reproductor</string>
<string name="test_extensions">Probar todas las extensiones</string>
<string name="biometric_setting">Bloquear biometricamente</string>
<string name="password_pin_authentication_title">Autenticación mediante contraseña/PIN</string>
<string name="biometric_setting_summary">Desbloquea la aplicación con huella dactilar, Face ID, PIN, patrón y contraseña.</string>
<string name="biometric_authentication_title">Desbloquear CloudStream</string>
<string name="biometric_unsupported">La autenticación biométrica no es compatible con este dispositivo</string>
<string name="biometric_prompt_description">Esta pantalla se cerró después de algunos intentos fallidos. Reinicie la aplicación.</string>
<string name="biometric_warning">Ahora se ha realizado una copia de seguridad de sus datos de CloudStream. Aunque la posibilidad de que esto ocurra es muy baja, todos los dispositivos pueden comportarse de forma diferente. En el raro caso de que no puedas acceder a la aplicación, borra completamente los datos de la aplicación y restaura desde una copia de seguridad. Sentimos mucho las molestias que esto pueda ocasionarte.</string>
<string name="favorite">Favorito</string>
<string name="unfavorite">No favorito</string>
<string name="resume_remaining" formatted="true">%s
\nrestante</string>
<string name="repo_copy_label">Nombre del repositorio y su URL</string>
<string name="toast_copied">¡Copiado!</string>
<string name="clipboard_unknown_error">Error al copiar. Por favor, copie el logcat y comuníquese con el soporte de la aplicación.</string>
<string name="clipboard_permission_error">Error al acceder al portapapeles. Inténtelo de nuevo.</string>
<string name="ok">De acuerdo</string>
<string name="battery_dialog_title">Desactivar optimización de batería</string>
<string name="music_singlar">Música</string>
<string name="app_unrestricted_toast">El uso de la batería de la aplicación está configurado sin restricciones</string>
<string name="app_info_intent_error">No se puede abrir la información de la aplicación CloudStream.</string>
<string name="custom_media_singluar">Media</string>
<string name="audio_book_singular">Audiolibro</string>
<string name="battery_dialog_message">Para garantizar descargas y notificaciones ininterrumpidas para programas de televisión suscritos, CloudStream necesita permiso para ejecutarse en segundo plano. Al presionar OK, se le dirigirá a información de la aplicación. Allí, desplácese hasta Uso de la batería de la aplicación y establezca el uso de la batería en Sin restricciones. Tenga en cuenta que este permiso no significa que CS3 agotará su batería. Solo funcionará en segundo plano cuando sea necesario, como cuando reciba notificaciones o descargue videos de extensiones oficiales. Si decide cancelar, puede ajustar esta configuración más adelante en los ajustes generales.</string>
</resources>

View File

@ -33,16 +33,16 @@
<string name="next_episode_time_hour_format" formatted="true">%1$dساعت %2$dدقیقه</string>
<string name="next_episode_time_min_format" formatted="true">%dدقیقه</string>
<string name="home_main_poster_img_des">پوستر اصلی</string>
<string name="torrent">تورنت</string>
<string name="torrent">تورنتها</string>
<string name="free_storage">آزاد</string>
<string name="documentaries">مستند ها</string>
<string name="documentaries">مستندها</string>
<string name="ova">انیمیشن ویدیویی اصلی</string>
<string name="max">حداکثر</string>
<string name="movies">فیلم‌ها</string>
<string name="tv_series">سریال های تلویزیونی</string>
<string name="asian_drama">درام های آسیایی</string>
<string name="asian_drama">درامهای آسیایی</string>
<string name="anime">انیمه</string>
<string name="cartoons">کارتونها</string>
<string name="cartoons">کارتونها</string>
<string name="used_storage">استفاده شده</string>
<string name="app_storage">برنامه</string>
<string name="go_back_img_des">بازگشت</string>
@ -52,7 +52,7 @@
<string name="action_open_watching">اطلاعات بیشتر</string>
<string name="torrent_plot">شرح</string>
<string name="subs_subtitle_languages">زبان زیرنویس</string>
<string name="player_subtitles_settings">زیرنویس</string>
<string name="player_subtitles_settings">زیرنویسها</string>
<string name="action_remove_from_bookmarks">حذف</string>
<string name="download_started">بارگیری آغاز شد</string>
<string name="pref_disable_acra">غیرفعال کردن گذارش باگ خودکار</string>
@ -69,7 +69,6 @@
<string name="torrent_no_plot">شرحی یافت نشد</string>
<string name="subtitles_settings">تنظیمات زیرنویس</string>
<string name="subs_window_color">رنگ پنجره</string>
<string name="eigengraumode_settings_des">افزودن گرینهٔ سرعت در پخش‌کننده</string>
<string name="download_canceled">بارگیری لغو شد</string>
<string name="home_expanded_hide">پنهان کردن</string>
<string name="sort_apply">اعمال کردن</string>
@ -87,4 +86,109 @@
<string name="picture_in_picture">تصویر در تصویر</string>
<string name="sort_close">بستن</string>
<string name="home_next_random_img_des">اتاق بعدی</string>
<string name="preview_background_img_des">پیش‌نمایش پس‌زمینه</string>
<string name="filler" formatted="true">فیلتر</string>
<string name="duration_format" formatted="true">%d دقیقه</string>
<string name="app_name">CloudStream</string>
<string name="play_with_app_name">بازی با CloudStream</string>
<string name="title_home">خانه</string>
<string name="title_search">جست‌وجو</string>
<string name="title_settings">تنظیمات</string>
<string name="search_hint">جست‌وجو…</string>
<string name="search_hint_site" formatted="true">جست‌و‌جوی %s…</string>
<string name="result_tags">ژانرها</string>
<string name="play_livestream_button">پخش پخش‌زنده</string>
<string name="pick_subtitle">زیرنویس‌ها</string>
<string name="reload_error">تلاش دوباره برای اتصال…</string>
<string name="type_re_watching">تماشای دوباره</string>
<string name="pick_source">منابع</string>
<string name="no_data">داده‌ای نیست</string>
<string name="episode_more_options_des">گزینه‌های بیشتر</string>
<string name="next_episode">قسمت پسین</string>
<string name="skip_loading">گذر از بارگذاری</string>
<string name="subscribe_tooltip">اعلان قسمت جدید</string>
<string name="result_search_tooltip">جست‌و‌جو در سایر افزونه‌ها</string>
<string name="recommendations_tooltip">نمایش پیشنهادات</string>
<string name="links_reloaded_toast">پیوندها بازبارگیری شدند</string>
<string name="player_speed">سرعت پخش‌کننده</string>
<string name="type_watching">در حال تماشا</string>
<string name="title_downloads">بارگیری‌ها</string>
<string name="player_speed_text_format" formatted="true">سرعت (%.2f برابر)</string>
<string name="new_update_format" formatted="true">بروزرسانی جدید پیدا شد!
\n%1$s -&gt; %2$s</string>
<string name="play_movie_button">پخش فیلم</string>
<string name="browser">مرورگر</string>
<string name="play_episode">پخش قسمت</string>
<string name="action_add_to_bookmarks">تنظیم وضعیت تماشا</string>
<string name="download_storage_text">حافظه درونی(داخلی)</string>
<string name="result_share">اشتراک‌گذاری</string>
<string name="play_trailer_button">پخش پیش‌پرده</string>
<string name="error_loading_links_toast">خطای بارگذاری پیوندها</string>
<string name="error_bookmarks_text">نشانک‌ها</string>
<string name="type_completed">به پایان رسیده</string>
<string name="result_open_in_browser">باز کردن در مرورگر</string>
<string name="type_plan_to_watch">برنامه‌ریزی برای تماشا</string>
<string name="subs_hold_to_reset_to_default">برای بازنشانی به پیشفرض نگه‌دارید</string>
<string name="library">کتابخانه</string>
<string name="status_ongoing">درادامه</string>
<string name="delete_message" formatted="true">این فرآیند بطور کامل %s را حذف می‌کند
\nآیا از این کار اطمینان دارید؟</string>
<string name="repo_copy_label">نام مخزن و نشانی</string>
<string name="toast_copied">رونویسی شد!</string>
<string name="settings_info">درباره</string>
<string name="subs_font">قلم</string>
<string name="subs_font_size">اندازه قلم</string>
<string name="swipe_to_change_settings">برای تغییر تنظیمات بکشید</string>
<string name="swipe_to_change_settings_des">برای تغییر میزان روشنایی یا صدا در سمت چپ و راست به بالا یا پایین بکشید</string>
<string name="automatic_plugin_updates">بروزرسانی خودکار افزونه</string>
<string name="start">آغاز</string>
<string name="app_language">زبان برنامه</string>
<string name="play_episode_toast">پخش قسمت</string>
<string name="year">سال</string>
<string name="movies_singular">فیلم</string>
<string name="tv_series_singular">سریال</string>
<string name="anime_singular">انیمه</string>
<string name="subs_outline_color">رنگ حاشیه متن</string>
<string name="player_size_settings">دکمه تغییر‌اندازه پخش‌کننده</string>
<string name="speed_setting_summary">افزودن گزینه سرعت در پخش‌کننده</string>
<string name="category_updates">بروزرسانی‌ و پشتیبانی</string>
<string name="kitsu_settings">نمایش پوستر از طریق Kitsu</string>
<string name="advanced_search">جستجوی پیشرفته</string>
<string name="season">فصل</string>
<string name="episode">قسمت</string>
<string name="season_short">ف</string>
<string name="episode_short">ق</string>
<string name="other_singular">ویدئو</string>
<string name="source_error">خطای منبع</string>
<string name="test_log">گزارش</string>
<string name="player_size_settings_des">حذف حاشیه سیاه</string>
<string name="player_subtitles_settings_des">تنظیمات زیرنویس پخش‌کننده</string>
<string name="category_account">حساب‌ها و امنیت</string>
<string name="show_log_cat">نمایش گزارش‌پیوسته 🐈</string>
<string name="copy_link_toast">پیوند در بریده‌دان رونویسی شد</string>
<string name="no_links_found_toast">هیچ پیوندی یافت‌نشد</string>
<string name="asian_drama_singular">درام آسیایی</string>
<string name="automatic_plugin_download">بارگیری خودکار افزونه‌ها</string>
<string name="documentaries_singular">مستند</string>
<string name="eigengraumode_settings">سرعت پخش</string>
<string name="no_episodes_found">هیچ قسمتی یافت‌نشد</string>
<string name="rated_format" formatted="true">امتیاز: %.1f</string>
<string name="subs_import_text" formatted="true">قلم‌ها را با گذاشتن در %s وارد کنید</string>
<string name="resume">ادامه</string>
<string name="subs_default_reset_toast">بازگردانی به مقدار پیشفرض</string>
<string name="go_back_30">−۳۰</string>
<string name="go_forward_30">+۳۰</string>
<string name="delete_file">حذف پرونده</string>
<string name="show_trailers_settings">نمایش پیش‌پرده‌ها</string>
<string name="episodes">قسمت‌ها</string>
<string name="resume_time_left" formatted="true">%dد
\nباقیمانده</string>
<string name="github">گیتهاب</string>
<string name="pref_filter_search_quality">نهان کردن کیفیت ویدئو انتخابی در نتایج جستجو</string>
<string name="cancel">لغو</string>
<string name="resume_remaining" formatted="true">%s
\nباقیمانده</string>
<string name="action_default">پیش‌فرض</string>
<string name="cartoons_singular">کارتون</string>
<string name="torrent_singular">تورنت</string>
</resources>

View File

@ -48,7 +48,7 @@
<string name="popup_resume_download">Reprendre le téléchargement</string>
<string name="popup_pause_download">Mettre en pause le téléchargement</string>
<string name="pref_disable_acra">Désactiver le rapport de bug automatique</string>
<string name="home_more_info">Plus d\'informations</string>
<string name="home_more_info">Plus d\'information</string>
<string name="home_expanded_hide">Cacher</string>
<string name="home_main_poster_img_des">Affiche principale</string>
<string name="home_play">Lecture</string>
@ -146,7 +146,7 @@
<string name="resize_fit">Adapter à l\'écran</string>
<string name="app_layout">Disposition de l\'application</string>
<string name="tv_layout">Disposition TV</string>
<string name="provider_lang_settings">Language des fournisseurs</string>
<string name="provider_lang_settings">Langues des extensions</string>
<string name="preferred_media_settings">Médias préférées</string>
<string name="automatic">Auto</string>
<string name="cast_format">Distribution : %s</string>
@ -171,7 +171,7 @@
<string name="video_ram_description">Causera des crashs si la valeur choisie est trop élevée sur un appareil avec peu de ram comme une télévision android.</string>
<string name="video_disk_description">Cause des problèmes si la valeur choisie est trop élevée sur un appareil avec peu de stockage comme une télévision android.</string>
<string name="random_button_settings">Bouton aléatoire</string>
<string name="random_button_settings_desc">Afficher un bouton aléatoire sur la page d\'accueil</string>
<string name="random_button_settings_desc">Afficher un bouton aléatoire sur la page d\'accueil et la librairie</string>
<string name="emulator_layout">Disposition émulateur</string>
<string name="bottom_title_settings">Position du titre du poster</string>
<string name="bottom_title_settings_des">Mettre le titre sous le poster</string>
@ -255,8 +255,7 @@
<string name="player_subtitles_settings_des">Paramètres des sous-titres du lecteur</string>
<string name="chromecast_subtitles_settings">Sous-titres Chromecast</string>
<string name="chromecast_subtitles_settings_des">Paramètres des sous-titres Chromecast</string>
<string name="eigengraumode_settings">Mode Eigengravy</string>
<string name="eigengraumode_settings_des">Ajout d\'une option de vitesse dans le lecteur</string>
<string name="eigengraumode_settings">Vitesse de lecture</string>
<string name="swipe_to_seek_settings">Balayez pour chercher</string>
<string name="swipe_to_seek_settings_des">Balayez d\'un côté ou de l\'autre pour contrôler la position dans la vidéo</string>
<string name="swipe_to_change_settings">Balayez pour modifier les paramètres</string>
@ -280,7 +279,7 @@
<string name="backup_failed">Permissions de stockage manquantes. Veuillez réessayer.</string>
<string name="backup_failed_error_format">Erreur de sauvegarde %s</string>
<string name="search">Recherche</string>
<string name="category_account">Comptes</string>
<string name="category_account">Comptes et Sécurité</string>
<string name="category_updates">Mises à jour et sauvegarde</string>
<string name="settings_info">Info</string>
<string name="advanced_search">Recherche avancée</string>
@ -327,7 +326,7 @@
<string name="remove_site_pref">Supprimer le site</string>
<string name="add_site_summary">Ajoute un clone à un site déjà existant, avec une URL différente</string>
<string name="nginx_url_pref">URL du serveur NGINX</string>
<string name="example_site_name">MonSiteTresCool</string>
<string name="example_site_name">Nouveau Nom du site</string>
<string name="error_invalid_id">ID invalide</string>
<string name="automatic_plugin_download_summary">Installer automatiquement les plugins qui sont dans les repository mais qui n\'ont pas encore été installés.</string>
<string name="resume_time_left" formatted="true">%dm
@ -339,9 +338,9 @@
<string name="show_dub">Étiquette Dub (doublage anglais)</string>
<string name="show_sub">Étiquette sous titre</string>
<string name="show_title">Titre</string>
<string name="enable_nsfw_on_providers">Active le NSFW sur les fournisseurs ayant cette option</string>
<string name="enable_nsfw_on_providers">Activer NSFW sur les extensions prises en charge</string>
<string name="example_password">motdepasse123</string>
<string name="example_username">MonNomDutilisateur</string>
<string name="example_username">Nom d\'utilisateur</string>
<string name="example_lang_name">Code de la langue (fr)</string>
<string name="create_account">Créer un compte</string>
<string name="player_load_subtitles_online">Charger depuis internet</string>
@ -357,7 +356,7 @@
<string name="subtitles_encoding">Encodage des sous titres</string>
<string name="category_providers">Fournisseurs</string>
<string name="category_ui">Disposition</string>
<string name="example_site_url">exemple.com</string>
<string name="example_site_url">https://exemple.com</string>
<string name="title">Titre</string>
<string name="resolution">Résolution</string>
<string name="quality_ts">TS</string>
@ -390,8 +389,8 @@
<string name="login_format" formatted="true">%1$s %2$s</string>
<string name="subtitles_filter_lang">Filtrez par langue préférée</string>
<string name="extras">Extras</string>
<string name="network_adress_example">Lien vers le stream</string>
<string name="referer">Référenceur</string>
<string name="network_adress_example">https://exemple.com/exemple.mp4</string>
<string name="referer">Référent (facultatif)</string>
<string name="apk_installer_settings">Installeur APK</string>
<string name="go_forward_30">+30</string>
<string name="quality_cam">Cam</string>
@ -461,9 +460,7 @@
<string name="apk_installer_package_installer">Installateur de paquet</string>
<string name="plugin">plugins</string>
<string name="delete_repository_plugins">Cela supprimera également tous les plugins du repository</string>
<string name="blank_repo_message">CloudStream n\'a pas de sites installés par défaut. Vous devez installer les sites à partir de dépôts.
\n
\nEn raison d\'un retrait DMCA irréfléchi de Sky UK Limited 🤮, nous ne pouvons pas lier le site du dépôt dans l\'application.
<string name="blank_repo_message">CloudStream n\'a aucun site installé par défaut. Vous devez installer les sites à partir de dépôts.
\n
\nRejoignez notre Discord ou cherchez en ligne.</string>
<string name="extension_language">Langage</string>
@ -518,7 +515,7 @@
<string name="subscription_deleted">Désabonné de %s</string>
<string name="restart">Redémarrer</string>
<string name="subscription_list_name">Abonné</string>
<string name="jsdelivr_proxy">Proxy raw.githubusercontent.com</string>
<string name="jsdelivr_proxy">Proxy GitHub</string>
<string name="pref_category_bypass">Contournements de FAI</string>
<string name="subscription_episode_released">L\'épisode %d est sorti !</string>
<string name="test_failed">Échec</string>
@ -585,4 +582,17 @@
\n
\nSouhaitez-vous ajouter cet élément, remplacer l\'élément existant ou annuler l\'action ?</string>
<string name="enter_current_pin">Entrer le code PIN actuel</string>
<string name="rotate_video">Pivoter</string>
<string name="links_reloaded_toast">Les liens ont été rechargés</string>
<string name="rotate_video_desc">Afficher un bouton pour lorientation de lécran</string>
<string name="auto_rotate_video">Rotation automatique</string>
<string name="auto_rotate_video_desc">Activer la rotation automatique de lécran en fonction de lorientation vidéo</string>
<string name="subscribe_tooltip">Notification de nouvel épisode</string>
<string name="result_search_tooltip">Rechercher dans d\'autres extensions</string>
<string name="speed_setting_summary">Ajoute une option de vitesse dans le lecteur</string>
<string name="test_extensions">Testez toutes les extensions</string>
<string name="recommendations_tooltip">Afficher les recommandations</string>
<string name="test_extensions_summary">Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension.</string>
<string name="toast_copied">Copié!</string>
<string name="repo_copy_label">Nom du dépôt et adresse internet</string>
</resources>

View File

@ -121,7 +121,6 @@
<string name="chromecast_subtitles_settings">Subtítulos de Chromecast</string>
<string name="chromecast_subtitles_settings_des">Configuración de subtítulos de Chromecast</string>
<string name="eigengraumode_settings">Modo Eigengravy</string>
<string name="eigengraumode_settings_des">Engadir a opción de velocidade no reprodutor</string>
<string name="swipe_to_seek_settings">Deslice para avanzar/retroceder</string>
<string name="swipe_to_seek_settings_des">Deslice o dedo de lado a lado para controlar a posición nun video</string>
<string name="swipe_to_change_settings">Deslice para cambiar a configuración</string>

View File

@ -66,7 +66,6 @@
<string name="picture_in_picture_des">अन्य ऐप्स के ऊपर एक लघु प्लेयर में प्लेबैक जारी रखता है</string>
<string name="player_size_settings_des">काले बॉर्डर हटाएँ</string>
<string name="player_subtitles_settings_des">प्लेयर उपशीर्षक सेटिंग्स</string>
<string name="eigengraumode_settings_des">प्लेयर में स्पीड विकल्प जोड़ता है</string>
<string name="swipe_to_seek_settings_des">किसी वीडियो में अपनी स्थिति नियंत्रित करने के लिए एक ओर से दूसरी ओर स्वाइप करें</string>
<string name="swipe_to_change_settings_des">चमक या वॉल्यूम बदलने के लिए बाईं या दाईं ओर ऊपर या नीचे स्लाइड करें</string>
<string name="double_tap_to_seek_settings_des">आगे या पीछे करने के लिए दाईं या बाईं ओर दो बार टैप करें</string>
@ -82,7 +81,7 @@
<string name="discord">Discord से जुड़िये</string>
<string name="benene">इस प्रोग्राम के निर्माता को केला दें</string>
<string name="benene_des">केले दे दिए गए</string>
<string name="app_language">एप्प की भाषा</string>
<string name="app_language">प की भाषा</string>
<string name="no_chromecast_support_toast">यह प्रोवाइडर क्रोमकास्ट का समर्थन नहीं करता</string>
<string name="no_links_found_toast">कोई लिंक नहीं मिले</string>
<string name="copy_link_toast">लिंक क्लिपबोर्ड पर कॉपी किया गया</string>
@ -105,7 +104,7 @@
<string name="synopsis">सारांश</string>
<string name="free_storage">ख़ाली</string>
<string name="used_storage">इस्तेमाल में</string>
<string name="app_storage">एप्</string>
<string name="app_storage"></string>
<string name="movies">मूवीज</string>
<string name="tv_series">टीवी सीरियल</string>
<string name="cartoons">कार्टून</string>
@ -192,4 +191,5 @@
<string name="pin">पिन</string>
<string name="links_reloaded_toast">लिंक पुन्ह खुली</string>
<string name="enter_current_pin">वर्तमान पिन दर्ज करें</string>
<string name="stream">नेटवर्क स्ट्रीम</string>
</resources>

View File

@ -34,7 +34,7 @@
<string name="filler" formatted="true">Umetak</string>
<string name="duration_format" formatted="true">%d min</string>
<string name="app_name">CloudStream</string>
<string name="play_with_app_name">Otvori s CloudStream-om</string>
<string name="play_with_app_name">Reproduciraj s CloudStream-om</string>
<string name="title_home">Početna stranica</string>
<string name="title_search">Pretraži</string>
<string name="title_downloads">Preuzimanja</string>
@ -72,7 +72,7 @@
<string name="download_failed">Preuzimanje nije uspjelo</string>
<string name="download_canceled">Preuzimanje otkazano</string>
<string name="download_done">Preuzimanje dovršeno</string>
<string name="stream">Stream</string>
<string name="stream">Mrežni stream</string>
<string name="error_loading_links_toast">Pogreška pri učitavanju veza</string>
<string name="download_storage_text">Unutarnja pohrana</string>
<string name="app_dubbed_text">Dub</string>
@ -133,8 +133,7 @@
<string name="player_subtitles_settings_des">Postavke titlova playera</string>
<string name="chromecast_subtitles_settings">Chromecast Titlovi</string>
<string name="chromecast_subtitles_settings_des">Postavke Chromecast titlova</string>
<string name="eigengraumode_settings">Eigengravy način</string>
<string name="eigengraumode_settings_des">Dodaje opciju brzine u playeru</string>
<string name="eigengraumode_settings">Brzina reprodukcije</string>
<string name="swipe_to_seek_settings">Prijeđi prstom za traženje</string>
<string name="swipe_to_seek_settings_des">Prijeđite prstom ulijevo ili udesno kako biste kontrolirali player</string>
<string name="swipe_to_change_settings">Klizni za promjenu postavki</string>
@ -158,7 +157,7 @@
<string name="backup_failed">Nedostaju dozvole za pohranu, pokušaj ponovo.</string>
<string name="backup_failed_error_format">Pogreška pri sigurnosnom kopiranju %s</string>
<string name="search">Pretraži</string>
<string name="category_account">Računi</string>
<string name="category_account">Računi i sigurnost</string>
<string name="category_updates">Ažuriranja i sigurnosne kopije</string>
<string name="settings_info">Informacije</string>
<string name="advanced_search">Napredno pretraživanje</string>
@ -299,10 +298,10 @@
<string name="category_general">Općenito</string>
<string name="random_button_settings">Random gumb</string>
<string name="random_button_settings_desc">Prikaži gumb za slučajni odabir reprodukcija na početnoj stranici i biblioteci</string>
<string name="provider_lang_settings">Jezici pružatelja usluga</string>
<string name="provider_lang_settings">Jezici proširenja</string>
<string name="app_layout">Izgled aplikacije</string>
<string name="preferred_media_settings">Preferirani mediji</string>
<string name="enable_nsfw_on_providers">Omogući NSFW na podržanim pružateljima usluga</string>
<string name="enable_nsfw_on_providers">Omogućava NSFW na podržanim proširenjima</string>
<string name="subtitles_encoding">Kodiranje titlova</string>
<string name="category_providers">Pružatelji usluga</string>
<string name="category_ui">Raspored</string>
@ -316,11 +315,11 @@
<string name="bottom_title_settings_des">Stavlja naslov ispod postera</string>
<!-- account stuff -->
<string name="example_password">lozinka123</string>
<string name="example_username">MojeCoolIme</string>
<string name="example_username">Korisničko ime</string>
<string name="example_email">bok@svijete.com</string>
<string name="example_ip">127.0.0.1</string>
<string name="example_site_name">MojaCoolStranica</string>
<string name="example_site_url">primjer.com</string>
<string name="example_site_name">NovoImeStranice</string>
<string name="example_site_url">https://primjer.com</string>
<string name="example_lang_name">Šifra jezika (en)</string>
<string name="login_format" formatted="true">%1$s %2$s</string>
<string name="account">račun</string>
@ -402,8 +401,8 @@
<string name="subtitles_filter_lang">Filtriraj po željenom jeziku medija</string>
<string name="extras">Extras</string>
<string name="trailer">Trailer</string>
<string name="network_adress_example">Veza na stream</string>
<string name="referer">Upućivač</string>
<string name="network_adress_example">https://primjer.com/primjer.mp4</string>
<string name="referer">Referent (nije obavezno)</string>
<string name="next">Sljedeće</string>
<string name="provider_languages_tip">Gledaj videozapise na ovim jezicima</string>
<string name="previous">Prethodno</string>
@ -434,8 +433,6 @@
<string name="plugins_not_downloaded" formatted="true">Nepreuzeto: %d</string>
<string name="blank_repo_message">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.</string>
<string name="view_public_repositories_button">Pregledajte repozitorije zajednice</string>
<string name="view_public_repositories_button_short">Javni popis</string>
@ -549,9 +546,9 @@
<string name="subscription_deleted">Otkazana pretplata sa %s</string>
<string name="revert">Vraćanje</string>
<string name="pref_category_bypass">ISP zaobilaznice</string>
<string name="jsdelivr_proxy">raw.githubusercontent.com Proxy</string>
<string name="jsdelivr_proxy">GitHub Proxy</string>
<string name="jsdelivr_enabled">Neuspješno dohvaćanje GitHuba. Uključuje se jsdelivr proxy …</string>
<string name="jsdelivr_proxy_summary">Zaobilazi blokiranje GitHuba koristeći jsdelivr. Može odgoditi ažuriranja za nekoliko dana.</string>
<string name="jsdelivr_proxy_summary">Zaobilazi blokiranje neobrađenih GitHub URL-ova koristeći jsDelivr. Može uzrokovati kašnjenje ažuriranja nekoliko dana.</string>
<string name="watch_quality_pref_data">Preferirana kvaliteta gledanja (podatkovna mobilna mreža)</string>
<string name="profile_number">Profil %d</string>
<string name="wifi">Wi-Fi</string>
@ -614,7 +611,22 @@
<string name="rotate_video_desc">Prikaži gumb za prebacivanje orijentacije zaslona</string>
<string name="auto_rotate_video_desc">Omogućuje automatsko mijenjanje orijentacije zaslona na temelju orijentacije videa</string>
<string name="auto_rotate_video">Automatsko rotiranje</string>
<string name="copyTitle">Naslov je kopiran!</string>
<string name="rotate_video_key">rotiraj_video_tipka</string>
<string name="auto_rotate_video_key">automatski_rotiraj_video_tipka</string>
<string name="subscribe_tooltip">Obavijest za novu epizodu</string>
<string name="result_search_tooltip">Pretraži u ostalim proširenjima</string>
<string name="speed_setting_summary">Dodaje opciju brzine u playeru</string>
<string name="test_extensions">Testiraj sva proširenja</string>
<string name="test_extensions_summary">Ovaj je test namijenjen samo programerima i ne provjerava niti negira rad bilo kojeg proširenja.</string>
<string name="recommendations_tooltip">Prikaži preporuke</string>
<string name="repo_copy_label">Ime repozitorija i URL</string>
<string name="toast_copied">kopirano!</string>
<string name="biometric_setting">Zaključaj s biometrijskim podatcima</string>
<string name="resume_remaining" formatted="true">%s
\npreostalo</string>
<string name="clipboard_permission_error">Greška u pristupanju međuspremnika. Pokušaj ponovo.</string>
<string name="biometric_authentication_title">Otključaj CloudStream</string>
<string name="password_pin_authentication_title">Lozinka/PIN autentifikacija</string>
<string name="biometric_unsupported">Ovaj uređaj ne podržava biometrijsku autentifikaciju</string>
<string name="biometric_prompt_description">Ovaj je ekran zatvoren zbog višestrukih neuspjelih pokušaja. Pokrenite aplikaciju ponovo.</string>
</resources>

View File

@ -37,7 +37,7 @@
<string name="status">Állapot</string>
<string name="show_title">Forrás címe</string>
<string name="history">Előzmények</string>
<string name="eigengraumode_settings">Eigengravy mód</string>
<string name="eigengraumode_settings">Visszajátszás sebessége</string>
<string name="subs_subtitle_elevation">Felirat magassága</string>
<string name="download_started">Letöltés elkezdve</string>
<string name="normal_no_plot">Nem található cselekmény</string>
@ -71,7 +71,7 @@
<string name="pick_subtitle">Feliratok</string>
<string name="go_back">Vissza</string>
<string name="download_done">Letöltés kész</string>
<string name="stream">Stream</string>
<string name="stream">Hálózati stream</string>
<string name="error_loading_links_toast">Hiba a linkek betöltésekor</string>
<string name="download_storage_text">Belső tárhely</string>
<string name="app_dubbed_text">Dub</string>
@ -124,7 +124,6 @@
<string name="player_subtitles_settings_des">Lejátszó feliratok beállításai</string>
<string name="chromecast_subtitles_settings">Chromecast Feliratok</string>
<string name="chromecast_subtitles_settings_des">Chromecast feliratok beállításai</string>
<string name="eigengraumode_settings_des">Sebességbeállítást ad hozzá a lejátszóhoz</string>
<string name="play_episode">Epizód lejátszása</string>
<string name="downloaded">Letöltve</string>
<string name="download_paused">Letöltés szüneteltetve</string>
@ -171,11 +170,11 @@
<string name="ova">OVA</string>
<string name="others">Egyebek</string>
<string name="tv_series_singular">Sorozat</string>
<string name="anime_singular">@string/anime</string>
<string name="anime_singular">Anime</string>
<string name="source_error">Forráshiba</string>
<string name="nsfw">NSFW</string>
<string name="cartoons_singular">Rajzfilm</string>
<string name="ova_singular">@string/ova</string>
<string name="ova_singular">OVA</string>
<string name="live_singular">Élőadás</string>
<string name="nsfw_singular">NSFW</string>
<string name="other_singular">Videó</string>
@ -184,7 +183,7 @@
<string name="asian_drama_singular">Ázsiai dráma</string>
<string name="episode_action_reload_links">Linkek újratöltése</string>
<string name="episode_action_copy_link">Link másolás</string>
<string name="episode_action_download_mirror">Link letöltés</string>
<string name="episode_action_download_mirror">Letöltés mirror</string>
<string name="episode_action_auto_download">Automatikus letöltés</string>
<string name="backup_success">Adatok eltárolva</string>
<string name="backup_failed_error_format">Hiba a biztonsági mentés során %s</string>
@ -223,9 +222,9 @@
<string name="remote_error">Távoli hiba</string>
<string name="render_error">Render hiba</string>
<string name="unexpected_error">Váratlan lejátszó hiba</string>
<string name="storage_error">Letöltés hiba, ellenőrízze a tárolási engedélyeket</string>
<string name="storage_error">Letöltés hiba, ellenőrizze a tárolási engedélyeket</string>
<string name="episode_action_chromecast_episode">Chromecast epizód</string>
<string name="episode_action_chromecast_mirror">Chromecast link</string>
<string name="episode_action_chromecast_mirror">Chromecast mirror</string>
<string name="episode_action_play_in_app">Lejátszás az alkalmazásban</string>
<string name="episode_action_play_in_format">Lejátszás %s</string>
<string name="episode_action_play_in_browser">Lejátszás böngészőben</string>
@ -233,7 +232,7 @@
<string name="reload_error">Újracsatlakozás…</string>
<string name="swipe_to_seek_settings_des">Húzd balra vagy jobbra a videólejátszóban az idő vezérléséhez</string>
<string name="swipe_to_change_settings">Csúsztassa ujját a beállítások módosításához</string>
<string name="swipe_to_change_settings_des">Csúsztassa felfelé vagy lefelé a bal vagy jobb oldalon a fényerő vagy a hangerő megváltoztatásához</string>
<string name="swipe_to_change_settings_des">Csúsztassa felf/le az ujját a bal/jobb oldalon a fényerő vagy a hangerő megváltoztatásához</string>
<string name="backup_settings">Biztonsági mentés</string>
<string name="benene_count_text_none">0 Banán a fejlesztőknek</string>
<string name="swipe_to_seek_settings">Húzd el, hogy beless</string>
@ -243,7 +242,7 @@
<string name="double_tap_to_pause_settings">Dupla koppintás a szüneteltetéshez</string>
<string name="double_tap_to_seek_amount_settings">Lejátszó keresési értéke (Másodpercben)</string>
<string name="double_tap_to_seek_settings_des">Koppintson kétszer a jobb vagy bal oldalra az előre vagy hátra ugráshoz</string>
<string name="double_tap_to_pause_settings_des">Koppintson kétszer középen a szüneteltetéshez</string>
<string name="double_tap_to_pause_settings_des">Koppintson kétszer középre a szüneteltetéshez</string>
<string name="use_system_brightness_settings">Rendszer fényerejének használata</string>
<string name="use_system_brightness_settings_des">Rendszer fényerejének használata az appban a sötét átfedés helyett</string>
<string name="episode_sync_settings">Előrehaladás frissítése</string>
@ -286,7 +285,7 @@
<string name="pref_category_ui_features">Funkciók</string>
<string name="watch_quality_pref_data">Előnyben részesített videóminőség (mobilinternet)</string>
<string name="limit_title">Videolejátszó cím max karakterek</string>
<string name="jsdelivr_enabled">Nem sikerült elérni a GitHubot, a jsdelivr proxy engedélyezése.</string>
<string name="jsdelivr_enabled">Nem sikerült elérni a GitHubot, a jsdelivr proxy bekapcsolva…</string>
<string name="pref_category_extensions">Bővítmények</string>
<string name="category_general">Általános</string>
<string name="subtitles_encoding">Felirat kódolása</string>
@ -295,10 +294,10 @@
<string name="category_provider_test">Szolgáltató teszt</string>
<string name="test_failed">Sikertelen</string>
<string name="video_disk_description">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.</string>
<string name="enable_nsfw_on_providers">Korhatáros tartalmak engedélyezése a támogatott szolgáltatóknál</string>
<string name="enable_nsfw_on_providers">Korhatáros tartalmak engedélyezése a támogatott kiegészítőknél</string>
<string name="category_ui">Elrendezés</string>
<string name="jsdelivr_proxy">raw.githubusercontent.com Proxy</string>
<string name="pref_category_player_features">A lejátszó funkciói</string>
<string name="jsdelivr_proxy">GitHub Proxy</string>
<string name="pref_category_player_features">Lejátszó funkciók</string>
<string name="watch_quality_pref">Előnyben részesített videóminőség (WiFi-n)</string>
<string name="dns_pref_summary">Hasznos az internetszolgáltató blokkjainak megkerüléséhez</string>
<string name="pref_category_player_layout">Elrendezés</string>
@ -306,10 +305,10 @@
<string name="nginx_url_pref">NGINX szerver URL-címe</string>
<string name="display_subbed_dubbed_settings">Szinkronizált/feliratozott animék megjelenítése</string>
<string name="pref_category_defaults">Alapértelmezettek</string>
<string name="random_button_settings_desc">Megjelenít egy gombot a Kezdőlapon, amely egy véletlenszerű filmet vagy TV sorozatot választ a Kezdőlapról</string>
<string name="random_button_settings_desc">Véletlenszerű gomb megjelenítése a Könyvtárban és Főoldalon</string>
<string name="download_path_pref">Letöltési útvonal</string>
<string name="pref_category_cache">Gyorsítótár</string>
<string name="provider_lang_settings">Szolgáltatók nyelvei</string>
<string name="provider_lang_settings">Kiegészítők nyelvei</string>
<string name="test_log">Napló</string>
<string name="library">Könyvtár</string>
<string name="pref_category_bypass">internetszolgáltató-kikerülések</string>
@ -322,7 +321,7 @@
<string name="preferred_media_settings">Előnyben részesített média</string>
<string name="pref_category_links">Hivatkozások</string>
<string name="video_buffer_clear_settings">Videó és kép gyorsítótár törlése</string>
<string name="jsdelivr_proxy_summary">A jsdelivr használatával a GitHub blokkolása megkerülhető. Néhány nappal késleltetheti a frissítéseket.</string>
<string name="jsdelivr_proxy_summary">A jsDelivr használatával a tiszta GitHub blokkolása megkerülhető. Néhány nappal késleltetheti a frissítéseket.</string>
<string name="video_ram_description">Ö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.</string>
<string name="player_load_subtitles_online">Betöltés az internetről</string>
<string name="video_tracks">Videósávok</string>
@ -333,13 +332,13 @@
<string name="update_notification_downloading">Alkalmazásfrissítés letöltése…</string>
<string name="sort_updated_new">Frissítve (újabbtól a régebbihez)</string>
<string name="empty_library_no_accounts_message">Ú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</string>
<string name="empty_library_logged_in_message">Úgy tűnik, ez a lista üres, próbálj meg egy másikra váltani</string>
\nJelentkezz be egy könyvtár fiókba, vagy adj hozzá műsorokat a helyi könyvtárodhoz.</string>
<string name="empty_library_logged_in_message">Úgy tűnik, ez a lista üres, próbálj meg egy másikra váltani.</string>
<string name="max">Max</string>
<string name="quality_4k">4K</string>
<string name="quality_sdr">SDR</string>
<string name="create_account">Fiók létrehozása</string>
<string name="example_site_url">pelda.com</string>
<string name="example_site_url">https://példa.hu</string>
<string name="subtitle_offset">Feliratok szinkronizálása</string>
<string name="update_notification_installing">Alkalmazásfrissítés telepítése…</string>
<string name="clipboard_too_large">Túl sok szöveg. Nem lehet a vágólapra menteni.</string>
@ -349,7 +348,7 @@
<string name="update">Frissítés</string>
<string name="sync_total_episodes_none">/\?\?</string>
<string name="subtitles_shadow">Árnyék</string>
<string name="trailer">Filmelőzetes</string>
<string name="trailer">Előzetes</string>
<string name="preferred_media_subtext">Mit szeretnél látni</string>
<string name="batch_download_nothing_to_download_format" formatted="true">Minden %s már letöltött</string>
<string name="extension_install_first">Először telepítse a bővítményt</string>
@ -357,13 +356,13 @@
<string name="pref_category_looks">Kinézet</string>
<string name="app_layout">Alkalmazás elrendezés</string>
<string name="upload_sync">Szinkronizálás</string>
<string name="authenticated_user_fail" formatted="true">Nem sikerült bejelentkezni a következőként: %s</string>
<string name="authenticated_user_fail" formatted="true">Nem sikerült bejelentkezni a %s-nál</string>
<string name="min">Min</string>
<string name="subtitle_offset_hint">1000 ms</string>
<string name="recommended">Ajánlott</string>
<string name="actor_main"></string>
<string name="error_invalid_data">Érvénytelen adatok</string>
<string name="network_adress_example">Link a streamhez</string>
<string name="network_adress_example">https://példa.hu/példa.mp4</string>
<string name="plugin_load_fail" formatted="true">Nem sikerült betölteni: %s</string>
<string name="batch_download_start_format" formatted="true">Elkezdődött a(z) %1$d %2$s letöltése…</string>
<string name="download_all_plugins_from_repo">Töltse le az összes bővítményt ebből a tárolóból\?</string>
@ -372,16 +371,16 @@
<string name="player_settings_play_in_mpv">MPV</string>
<string name="app_not_found_error">Alkalmazás nem található</string>
<string name="apk_installer_package_installer">PackageInstaller</string>
<string name="sort_by">Rendezés e szerint:</string>
<string name="sort_by">Rendezés e szerint</string>
<string name="subscription_new">Feliratkozott a következőre: %s</string>
<string name="example_site_name">MenőWeboldalam</string>
<string name="example_site_name">ÚjOldalNév</string>
<string name="quality_dvd">DVD</string>
<string name="plugins_updated" formatted="true">%d plugin frissítve</string>
<string name="extension_rating" formatted="true">Értékelés: %s</string>
<string name="clear_history">Előzmények törlése</string>
<string name="no">Nem</string>
<string name="subscription_list_name">Feliratkozva</string>
<string name="subtitle_offset_extra_hint_later_format">Használd ezt, ha a feliratok %d ms-sel korábban jelennek meg.</string>
<string name="subtitle_offset_extra_hint_later_format">Használd ezt, ha a feliratok %d ms-sel korábban jelennek meg</string>
<string name="category_player">Lejátszó</string>
<string name="resolution_and_title">Felbontás és cím</string>
<string name="player_pref">Előnyben részesített videolejátszó</string>
@ -420,7 +419,7 @@
<string name="uppercase_all_subtitles">Minden felirat nagybetűs</string>
<string name="skip_type_intro">Intro</string>
<string name="subscription_deleted">Leiratkozott a következőről: %s</string>
<string name="subtitles_remove_bloat">Bloat eltávolítása a feliratokról</string>
<string name="subtitles_remove_bloat">Szükségtelen elemek eltávolítása a feliratokról</string>
<string name="subtitles_filter_lang">Szűrés előnyben részesített médianyelv szerint</string>
<string name="confirm_exit_dialog">Biztos vagy benne, hogy ki akarsz lépni\?</string>
<string name="sort">Rendezés</string>
@ -431,9 +430,7 @@
<string name="delete_repository_plugins">Ez az összes tároló bővítményt is törli</string>
<string name="blank_repo_message">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.</string>
\nCsatlakozz a Discord-unkhoz vagy keress online.</string>
<string name="extension_version">Verzió</string>
<string name="action_mark_as_watched">Megjelölés megtekintettként</string>
<string name="action_remove_from_watched">Eltávolítás a megnézettek közül</string>
@ -466,7 +463,7 @@
<string name="sort_alphabetical_a">Betűrendben (A-tól a Z-ig)</string>
<string name="sort_updated_old">Frissítve (régebbitől az újabbig)</string>
<string name="example_password">jelszó123</string>
<string name="example_username">AzÉnMenőFelhasználónevem</string>
<string name="example_username">Felhasználónév</string>
<string name="example_ip">127.0.0.1</string>
<string name="switch_account">Fiókváltás</string>
<string name="add_account">Fiók hozzáadása</string>
@ -481,14 +478,14 @@
<string name="extensions">Bővítmények</string>
<string name="add_repository">Tároló hozzáadása</string>
<string name="repository_name_hint">Tároló neve</string>
<string name="repository_url_hint">Tárhely URL címe</string>
<string name="repository_url_hint">Repó URL</string>
<string name="plugin_loaded">Bővítmény betöltve</string>
<string name="plugin_downloaded">Bővítmény letöltve</string>
<string name="skip_type_creddits">Közreműködők</string>
<string name="sort_alphabetical_z">Betűrendben (Z-től az A-ig)</string>
<string name="select_library">Könyvtár kiválasztása</string>
<string name="safe_mode_file">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.</string>
<string name="safe_mode_file">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.</string>
<string name="normal">Normál</string>
<string name="player_loaded_subtitles" formatted="true">%s betöltve</string>
<string name="skip_setup">Beállítás kihagyása</string>
@ -503,7 +500,7 @@
<string name="android_tv_interface_off_seek_settings_summary">Az átugrás mértéke, amikor a lejátszó el van rejtve</string>
<string name="legal_notice">Jogi nyilatkozat</string>
<string name="android_tv_interface_on_seek_settings">Lejátszó megjelenítve - Ugrási Érték</string>
<string name="android_tv_interface_off_seek_settings">Lejátszó elrejtve - Ugrási Érték</string>
<string name="android_tv_interface_off_seek_settings">Lejátszó Elrejtve - Ugrási Érték</string>
<string name="add_site_pref">Klónozott oldal</string>
<string name="add_site_summary">Egy meglévő webhely klónjának hozzáadása, más URL-címmel</string>
<string name="tv_layout">TV elrendezés</string>
@ -513,4 +510,86 @@
<string name="backup_frequency">Mentési gyakoriság</string>
<string name="sync_score">Értékelt</string>
<string name="disable">Kikapcsolás</string>
<string name="quality_cam_rip">Kamera Rip</string>
<string name="skip_type_op">Nyitás</string>
<string name="quality_workprint">WP</string>
<string name="subscribe_tooltip">Új epizód értesítés</string>
<string name="result_search_tooltip">Keresés más kiegészítőkben</string>
<string name="recommendations_tooltip">Ajánlatok mutatása</string>
<string name="speed_setting_summary">Sebesség opció megjelenítése a lejátszóban</string>
<string name="test_extensions">Minden kiegészítő tesztelése</string>
<string name="test_extensions_summary">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.</string>
<string name="login_format" formatted="true">%1$s %2$s</string>
<string name="quality_ts">TS</string>
<string name="action_subscribe">Feliratkozás</string>
<string name="action_unsubscribe">Leiratkozás</string>
<string name="links_reloaded_toast">Linkek Újratöltve</string>
<string name="skip_type_ed">Zárás</string>
<string name="referer">Hivatkozó (opcionális)</string>
<string name="no_plugins_found_error">Nem találhatóak pluginek a repóban</string>
<string name="no_repository_found_error">Repó nem található, ellenőrizze a címet vagy próbálja VPN-el</string>
<string name="player_settings_play_in_web">Web Videó Cast</string>
<string name="skip_type_format" formatted="true">%s kihagyása</string>
<string name="enable_skip_op_from_database_des">A kihagyási felugró ablakok mutatása nyitás/zárás esetén</string>
<string name="set_default">Alapbeállítás</string>
<string name="use">Használ</string>
<string name="profile_number">%d profil</string>
<string name="subtitle_offset_extra_hint_before_format">Használja ezt ha a felirat %d ms-ot késik</string>
<string name="quality_cam_hd">Kamera HD</string>
<string name="poster_image">Poszter Kép</string>
<string name="quality_tc">TC</string>
<string name="mobile_data">Mobil adat</string>
<string name="wifi">Wi-Fi</string>
<string name="edit">Szerkeszt</string>
<string name="quality_cam">Kamera</string>
<string name="subtitles_depressed">Nyomott</string>
<string name="skip_type_mixed_ed">Kevert zárás</string>
<string name="skip_type_mixed_op">Kevert nyitás</string>
<string name="help">Segítség</string>
<string name="profiles">Profilok</string>
<string name="action_remove_from_favorites">Eltávolítás kedvencekből</string>
<string name="enter_current_pin">Adja meg a jelenlegi PIN-t</string>
<string name="quality_profile_help">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!</string>
<string name="duplicate_message_multiple" formatted="true">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?</string>
<string name="skip_startup_account_select_pref">Fiók választás kihagyása belépéskor</string>
<string name="use_default_account">Használjon alapértelmezett fiókot</string>
<string name="rotate_video">Elforgatás</string>
<string name="profile_background_des">Profil háttér</string>
<string name="favorites_list_name">Kedvencek</string>
<string name="favorite_added">%s hozzáadva a kedvencekhez</string>
<string name="favorite_removed">%s eltávolítva a kedvencekből</string>
<string name="action_add_to_favorites">Hozzáadás a kedvencekhez</string>
<string name="duplicate_message_single" formatted="true">Ú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?</string>
<string name="enter_pin">Adja meg a PIN-t</string>
<string name="lock_profile">Profil Zárolása</string>
<string name="select_an_account">Válasszon egy fiókot</string>
<string name="manage_accounts">Fiókok kezelése</string>
<string name="edit_account">Fiók módosítása</string>
<string name="logged_account" formatted="true">Belépve mint %s</string>
<string name="rotate_video_desc">Jelenítsen meg egy kapcsolót a képorientáció váltáshoz</string>
<string name="duplicate_title">Potenciális Dupla Találat</string>
<string name="enter_pin_with_name" formatted="true">Adja meg a PIN-t a %s-hoz</string>
<string name="pin">PIN</string>
<string name="pin_error_incorrect">Hibás PIN. Próbálja újra.</string>
<string name="unable_to_inflate">A UI hibásan jelenítődött meg, ez egy JELENTŐS BUG ezért kérjük jelentse be %s</string>
<string name="already_voted">Már korábban szavazott</string>
<string name="duplicate_add">Hozzáadás</string>
<string name="duplicate_replace">Kicserélés</string>
<string name="duplicate_replace_all">Mind Kicserélése</string>
<string name="qualities">Minőségek</string>
<string name="pin_error_length">A PIN 4 karakter hosszú kell legyen</string>
<string name="auto_rotate_video">Auto elforgatás</string>
<string name="auto_rotate_video_desc">Az automatikus videó orientáció alapján való képernyő elforgatás bekapcsolása</string>
</resources>

View File

@ -115,8 +115,7 @@
<string name="player_subtitles_settings_des">Pengaturan subtitle pemutar</string>
<string name="chromecast_subtitles_settings">Subtitle Chromecast</string>
<string name="chromecast_subtitles_settings_des">Pengaturan subtitle Chromecast</string>
<string name="eigengraumode_settings">Mode Eigengravy</string>
<string name="eigengraumode_settings_des">Menambahkan opsi kecepatan di pemutar</string>
<string name="eigengraumode_settings">Kecepatan pemutaran</string>
<string name="swipe_to_seek_settings">Geser untuk mengubah waktu</string>
<string name="swipe_to_seek_settings_des">Geser dari sisi ke sisi untuk mengontrol posisi dalam video</string>
<string name="swipe_to_change_settings">Geser untuk mengubah pengaturan</string>
@ -138,8 +137,8 @@
<string name="backup_failed">Izin penyimpanan tidak ditemukan, mohon coba lagi.</string>
<string name="backup_failed_error_format">Error saat mencadang %s</string>
<string name="search">Cari</string>
<string name="category_account">Kredit dan akun</string>
<string name="category_updates">Update dan cadangan</string>
<string name="category_account">Akun dan Keamanan</string>
<string name="category_updates">Update dan Cadangan</string>
<string name="settings_info">Info</string>
<string name="advanced_search">Pencarian Lanjutan</string>
<string name="advanced_search_des">Memberikan hasil pencarian yang dipisahkan berdasarkan provider</string>
@ -263,8 +262,8 @@
<string name="legal_notice_text" translatable="false">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.</string>
<string name="category_general">Umum</string>
<string name="random_button_settings">Tombol Acak</string>
<string name="random_button_settings_desc">Tampilkan tombol acak di Beranda</string>
<string name="provider_lang_settings">Bahasa provider</string>
<string name="random_button_settings_desc">Tampilkan tombol acak di Beranda dan Pustaka</string>
<string name="provider_lang_settings">Bahasa ekstensi</string>
<string name="app_layout">Tata Letak Aplikasi</string>
<string name="preferred_media_settings">Media yang lebih diinginkan</string>
<string name="category_ui">Antarmuka pengguna</string>
@ -373,10 +372,10 @@
<string name="pref_category_defaults">Bawaan</string>
<string name="pref_category_looks">Tampilan</string>
<string name="pref_category_ui_features">Fitur</string>
<string name="enable_nsfw_on_providers">Tampilkan konten NSFW</string>
<string name="enable_nsfw_on_providers">Mengaktifkan NSFW pada Ekstensi yang didukung</string>
<string name="play_trailer_button">Putar Cuplikan</string>
<string name="play_livestream_button">Putar Siaran</string>
<string name="stream">Siaran</string>
<string name="stream">Aliran jaringan</string>
<string name="subs_subtitle_languages">Bahasa Subtitel</string>
<string name="autoplay_next_settings">Putar otomatis episode selanjutnya</string>
<string name="autoplay_next_settings_des">Putar episode selanjutnya, setelah ini berakhir</string>
@ -398,22 +397,20 @@
<string name="season_format">%1$s %2$d%3$s</string>
<string name="livestreams">Siaran langsung</string>
<string name="remove_site_pref">Hapus Website</string>
<string name="example_username">UsernameKeren</string>
<string name="example_username">Username</string>
<string name="example_email">contoh@email.com</string>
<string name="example_ip">127.0.0.1</string>
<string name="example_site_name">Websiteku</string>
<string name="example_site_url">contoh.com</string>
<string name="example_site_name">NamaSitus Baru</string>
<string name="example_site_url">https://contoh.com</string>
<string name="extras">Ekstra</string>
<string name="preferred_media_subtext">Apa yang ingin anda lihat</string>
<string name="plugin_deleted">Plugin terhapus</string>
<string name="plugins_updated" formatted="true">%d plugin diperbarui</string>
<string name="view_public_repositories_button">Lihat Repositori dari Group</string>
<string name="view_public_repositories_button_short">List Umum</string>
<string name="blank_repo_message">CloudStream tidak memiliki sumber video secara bawaan. Kamu harus menginstall dari repositori.
<string name="blank_repo_message">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.</string>
\nBergabunglah dengan Discord kami atau cari secara online.</string>
<string name="repository_url_hint">Alamat Repositori</string>
<string name="create_account">Buat Akun</string>
<string name="error">Error</string>
@ -435,7 +432,7 @@
<string name="safe_mode_title">Semua Umur</string>
<string name="single_plugin_disabled" formatted="true">%s (Tidak aktif)</string>
<string name="tracks">Trek</string>
<string name="apply_on_restart">Terapkan saat dimuat ulang</string>
<string name="apply_on_restart">Terapkan saat dimuat ulang untuk melihat perubahan.</string>
<string name="extension_description">Keterangan</string>
<string name="extension_version">Versi</string>
<string name="extension_status">Status</string>
@ -457,7 +454,7 @@
<string name="delete_repository_plugins">Pilih ini untuk menghapus semua repositori plugin</string>
<string name="skip_setup">Lewati pengaturan</string>
<string name="error_invalid_url">Alamat salah</string>
<string name="network_adress_example">Alamat streaming</string>
<string name="network_adress_example">https://contoh.com/contoh.mp4</string>
<string name="next">Selanjutnya</string>
<string name="previous">Sebelumnya</string>
<string name="app_layout_subtext">Ubah tampilan aplikasi</string>
@ -482,7 +479,7 @@
<string name="pref_category_gestures">Gerakan</string>
<string name="apk_installer_settings_des">Beberapa perangkat tidak mendukung penginstal paket mode baru. Coba mode lama jika pembaruan tidak dapat diinstal.</string>
<string name="pref_category_actions">Aksi</string>
<string name="referer">Referer</string>
<string name="referer">Memberi referensi (opsional)</string>
<string name="yes">Ya</string>
<string name="extension_install_first">Install ekstensi terlebih dahulu</string>
<string name="all_languages_preference">Semua Bahasa</string>
@ -545,9 +542,9 @@
<string name="subscription_new">Berlangganan ke %s</string>
<string name="subscription_deleted">Berhenti berlangganan di %s</string>
<string name="subscription_episode_released">Episode %d telah rilis!</string>
<string name="jsdelivr_proxy">Proxy raw.githubusercontent.com</string>
<string name="jsdelivr_proxy">Proxy GitHub</string>
<string name="jsdelivr_enabled">Tidak dapat menjangkau GitHub. Mengaktifkan proxy jsDelivr…</string>
<string name="jsdelivr_proxy_summary">Melewati pemblokiran GitHub menggunakan jsDelivr. Dapat menyebabkan pembaruan tertunda beberapa hari.</string>
<string name="jsdelivr_proxy_summary">Lewati pemblokiran raw URL github menggunakan jsDelivr. Dapat menyebabkan pembaruan tertunda selama beberapa hari.</string>
<string name="pref_category_bypass">Bypass ISP</string>
<string name="revert">Pulihkan</string>
<string name="watch_quality_pref_data">Kualitas nonton yang diinginkan (Data Seluler)</string>
@ -607,4 +604,38 @@
<string name="skip_startup_account_select_pref">Lewati pemilihan akun saat startup</string>
<string name="manage_accounts">Kelola Akun</string>
<string name="edit_account">Edit akun</string>
<string name="rotate_video">Putar</string>
<string name="rotate_video_desc">Menampilkan tombol sakelar untuk orientasi layar</string>
<string name="links_reloaded_toast">Tautan Dimuat Ulang</string>
<string name="auto_rotate_video_desc">Mengaktifkan peralihan otomatis orientasi layar berdasarkan orientasi video</string>
<string name="auto_rotate_video">Putar otomatis</string>
<string name="result_search_tooltip">Cari di ekstensi lainnya</string>
<string name="speed_setting_summary">Menambahkan opsi percepat di pemutar</string>
<string name="test_extensions_summary">Tes ini hanya ditujukan untuk pengembang dan tidak memverifikasi atau menolak kerja ekstensi apa pun.</string>
<string name="subscribe_tooltip">Notifikasi episode baru</string>
<string name="recommendations_tooltip">Tampilkan rekomendasi</string>
<string name="test_extensions">Menguji semua Ekstensi</string>
<string name="biometric_warning">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.</string>
<string name="password_pin_authentication_title">Otentikasi Kata Sandi/PIN</string>
<string name="biometric_unsupported">Otentikasi biometrik tidak didukung di perangkat ini</string>
<string name="biometric_setting_summary">Buka kunci aplikasi dengan Sidik Jari, ID Wajah, PIN, Pola, dan Kata Sandi.</string>
<string name="biometric_prompt_description">Layar ini ditutup setelah mengalami beberapa kali percobaan yang gagal. Anda harus memulai ulang aplikasi ini.</string>
<string name="unfavorite">Batalkan favorit</string>
<string name="biometric_authentication_title">Buka kunci CloudStream</string>
<string name="resume_remaining" formatted="true">%s
\ntersisa</string>
<string name="favorite">Favorit</string>
<string name="biometric_setting">Kunci dengan Biometrik</string>
<string name="repo_copy_label">Nama dan URL repositori</string>
<string name="clipboard_permission_error">Gagal mengakses Papan Klip, mohon coba lagi.</string>
<string name="toast_copied">disalin!</string>
<string name="clipboard_unknown_error">Gagal menyalin, mohon salin logcat dan hubungi pengembang aplikasi.</string>
<string name="ok">Oke</string>
<string name="battery_dialog_title">Matikan pengoptimalan Baterai</string>
<string name="app_unrestricted_toast">Pemakaian baterai untuk aplikasi ini sudah diatur menjadi tidak dibatasi</string>
<string name="app_info_intent_error">Gagal membuka info aplikasi CloudStream.</string>
<string name="music_singlar">Musik</string>
<string name="audio_book_singular">Buku Audio</string>
<string name="custom_media_singluar">Media</string>
<string name="battery_dialog_message">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.</string>
</resources>

View File

@ -61,7 +61,7 @@
<string name="download_failed">Download fallito</string>
<string name="download_canceled">Download cancellato</string>
<string name="download_done">Download completato</string>
<string name="stream">Stream</string>
<string name="stream">Flusso di rete</string>
<string name="error_loading_links_toast">Errore durante il caricamento dei link</string>
<string name="download_storage_text">Archiviazione interna</string>
<string name="app_dubbed_text">Doppiato</string>
@ -122,8 +122,7 @@
<string name="player_subtitles_settings_des">Impostazioni sottotitoli lettore</string>
<string name="chromecast_subtitles_settings">Sottotitoli Chromecast</string>
<string name="chromecast_subtitles_settings_des">Impostazioni sottotitoli Chromecast</string>
<string name="eigengraumode_settings">Modalità Eigengravy</string>
<string name="eigengraumode_settings_des">Aggiungi opzione velocità nel player</string>
<string name="eigengraumode_settings">Velocità di riproduzione</string>
<string name="swipe_to_seek_settings">Scorri per mandare avanti/indietro</string>
<string name="swipe_to_seek_settings_des">Scorri da un lato all\'altro per controllare la tua posizione in un video</string>
<string name="swipe_to_change_settings">Scorri per cambiare le impostazioni</string>
@ -147,7 +146,7 @@
<string name="backup_failed">Permessi di archiviazione mancanti. Per favore riprova.</string>
<string name="backup_failed_error_format">Errore nel backup %s</string>
<string name="search">Cerca</string>
<string name="category_account">Accounts</string>
<string name="category_account">Account e sicurezza</string>
<string name="category_updates">Aggiornamenti e Backup</string>
<string name="settings_info">Info</string>
<string name="advanced_search">Ricerca avanzata</string>
@ -287,11 +286,11 @@
<string name="legal_notice">Avvertenza</string>
<string name="category_general">Generale</string>
<string name="random_button_settings">Random</string>
<string name="random_button_settings_desc">Mostra pulsante Random nella homepage</string>
<string name="provider_lang_settings">Lingua provider</string>
<string name="random_button_settings_desc">Mostra pulsante casuale nella home page e nella libreria</string>
<string name="provider_lang_settings">Lingue estensione</string>
<string name="app_layout">Layout app</string>
<string name="preferred_media_settings">Media preferito</string>
<string name="enable_nsfw_on_providers">Abilita NSFW sui provider supportati</string>
<string name="enable_nsfw_on_providers">Abilita NSFW sulle estensioni supportate</string>
<string name="subtitles_encoding">Encoding Sottotitoli</string>
<string name="category_providers">Provider</string>
<string name="category_ui">Interfaccia utente</string>
@ -305,11 +304,11 @@
<string name="bottom_title_settings_des">Titolo sotto il poster</string>
<!-- account stuff -->
<string name="example_password">password123</string>
<string name="example_username">IlMioUsername</string>
<string name="example_username">Nome utente</string>
<string name="example_email">hello@world.com</string>
<string name="example_ip">127.0.0.1</string>
<string name="example_site_name">IlMioSito</string>
<string name="example_site_url">example.com</string>
<string name="example_site_name">NuovoNomeSito</string>
<string name="example_site_url">https://example.com</string>
<string name="example_lang_name">Codice lingua (it)</string>
<string name="login_format" formatted="true">%1$s %2$s</string>
<string name="account">account</string>
@ -391,8 +390,8 @@
<string name="subtitles_filter_lang">Filtra in base alla lingua preferita</string>
<string name="extras">Extra</string>
<string name="trailer">Trailer</string>
<string name="network_adress_example">Link allo stream</string>
<string name="referer">Referer</string>
<string name="network_adress_example">https://example.com/example.mp4</string>
<string name="referer">Referente (facoltativo)</string>
<string name="next">Prossimo</string>
<string name="provider_languages_tip">Guarda video in queste lingue</string>
<string name="previous">Precedente</string>
@ -424,9 +423,7 @@
<string name="plugins_updated" formatted="true">Aggiornati %d plugin</string>
<string name="blank_repo_message">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.</string>
\nJoin our Discord or search online.</string>
<string name="view_public_repositories_button">Vedi le repository della community</string>
<string name="view_public_repositories_button_short">Lista pubblica</string>
<string name="uppercase_all_subtitles">Tutti i sottotitoli in maiuscolo</string>
@ -435,7 +432,7 @@
<string name="tracks">Tracce</string>
<string name="audio_tracks">Traccia audio</string>
<string name="video_tracks">Traccia video</string>
<string name="apply_on_restart">Applica al riavvio</string>
<string name="apply_on_restart">Riavvia app per visualizzare le modifiche.</string>
<string name="safe_mode_title">Safe mode attiva</string>
<string name="safe_mode_description">Tutte le estensioni sono state disabilitate a causa di un arresto anomalo per aiutarti a trovare l\'estensione che causa il problema.</string>
<string name="safe_mode_crash_info">Vedi informazioni del crash</string>
@ -539,12 +536,12 @@
<string name="stop">Ferma</string>
<string name="test_passed">Superato</string>
<string name="test_failed">Fallito</string>
<string name="jsdelivr_proxy">Proxy raw.githubusercontent.com</string>
<string name="jsdelivr_proxy">Proxy GitHub</string>
<string name="subscription_deleted">Disiscritto da %s</string>
<string name="subscription_list_name">Iscritto</string>
<string name="subscription_new">Iscritto a %s</string>
<string name="jsdelivr_enabled">Impossibile raggiungere GitHub. Attivazione proxy jsDelivr…</string>
<string name="jsdelivr_proxy_summary">Aggira il blocco di GitHub usando jsDelivr. Potrebbe causare un ritardo degli aggiornamenti di alcuni giorni.</string>
<string name="jsdelivr_proxy_summary">Evita il blocco degli URL github non elaborati utilizzando jsDelivr. Potrebbe causare un ritardo degli aggiornamenti di alcuni giorni.</string>
<string name="pref_category_bypass">Baypass ISP</string>
<string name="revert">Ripristina</string>
<string name="subscription_in_progress_notification">Aggiornando shows a cui sei iscritto</string>
@ -602,10 +599,42 @@
<string name="logged_account" formatted="true">Entrato come %s</string>
<string name="enter_pin_with_name" formatted="true">Inserisci il PIN per %s</string>
<string name="lock_profile">Blocca profilo</string>
<string name="use_default_account">Usa Account Default</string>
<string name="use_default_account">Usa account predefinito</string>
<string name="skip_startup_account_select_pref">Salta la selezione dell\'account all\'avvio</string>
<string name="manage_accounts">Gestisci Accounts</string>
<string name="edit_account">Modifica account</string>
<string name="links_reloaded_toast">Collegamenti ricaricati</string>
<string name="rotate_video">Ruota</string>
<string name="rotate_video_desc">Visualizza un pulsante di commutazione per l\'orientamento dello schermo</string>
<string name="auto_rotate_video_desc">Abilita la commutazione automatica dell\'orientamento dello schermo in base all\'orientamento del video</string>
<string name="auto_rotate_video">Rotazione automatica</string>
<string name="result_search_tooltip">Cerca in altre estensioni</string>
<string name="recommendations_tooltip">Mostra consigli</string>
<string name="speed_setting_summary">Aggiunge un\'opzione di velocità nel lettore</string>
<string name="test_extensions">Prova tutte le estensioni</string>
<string name="test_extensions_summary">Questo test è pensato solo per gli sviluppatori e non verifica o nega il funzionamento di alcuna estensione.</string>
<string name="subscribe_tooltip">Notifica nuovo episodio</string>
<string name="biometric_authentication_title">Sblocca CloudStream</string>
<string name="biometric_setting">Blocca con biometria</string>
<string name="password_pin_authentication_title">Autenticazione con password/PIN</string>
<string name="biometric_unsupported">L\'autenticazione biometrica non è supportata su questo dispositivo</string>
<string name="biometric_setting_summary">Sblocca app con impronta digitale, Face ID, PIN, sequenza e password.</string>
<string name="biometric_prompt_description">Questa schermata è stata chiusa a causa di più tentativi falliti. Riavvia l\'app.</string>
<string name="biometric_warning">È 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.</string>
<string name="unfavorite">Non preferito</string>
<string name="resume_remaining" formatted="true">%s
\nresiduo</string>
<string name="favorite">Preferito</string>
<string name="repo_copy_label">Nome e URL del repository</string>
<string name="toast_copied">copiato!</string>
<string name="clipboard_permission_error">Errore durante l\'accesso agli Appunti. Riprova.</string>
<string name="clipboard_unknown_error">Errore durante la copia. Copia logcat e contatta il supporto dell\'app.</string>
<string name="ok">OK</string>
<string name="battery_dialog_title">Disabilita ottimizzazione della batteria</string>
<string name="app_info_intent_error">Impossibile aprire le informazioni sull\'app CloudStream.</string>
<string name="custom_media_singluar">Media</string>
<string name="battery_dialog_message">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\".</string>
<string name="app_unrestricted_toast">L\'utilizzo della batteria dell\'app è già impostato su \"Senza restrizioni\"</string>
<string name="music_singlar">Musica</string>
<string name="audio_book_singular">Audiolibro</string>
</resources>

View File

@ -271,7 +271,6 @@
<string name="browser">‪דפדפן</string>
<string name="subs_window_color">צבע חלון</string>
<string name="show_log_cat">הצג לוג</string>
<string name="eigengraumode_settings_des">הוסף אפשרות מהירות בנגן</string>
<string name="double_tap_to_seek_settings">לחץ פעמיים כדי להציץ</string>
<string name="double_tap_to_pause_settings">לחץ פעמיים כדי לעצור</string>
<string name="use_system_brightness_settings_des">התשתמש בבהירות המערכת בנגן האפליקציה במקום שכבת-על כהה</string>

View File

@ -237,9 +237,9 @@
<string name="player_subtitles_settings_des">プレーヤーの字幕設定</string>
<string name="chromecast_subtitles_settings">Chromecastの字幕</string>
<string name="chromecast_subtitles_settings_des">Chromecastの字幕設定</string>
<string name="eigengraumode_settings_des">プレーヤーに速度オプションを追加します</string>
<string name="swipe_to_seek_settings">スワイプして探す</string>
<string name="autoplay_next_settings">次のエピソードを自動再生する</string>
<string name="autoplay_next_settings_des">現在のエピソードが終了したら次のエピソードを開始する</string>
<string name="subs_hold_to_reset_to_default">長押しするとデフォルトにリセットされます</string>
<string name="popup_resume_download">ダウンロードを再開</string>
</resources>

View File

@ -111,7 +111,6 @@
<string name="chromecast_subtitles_settings">Chromecast 자막</string>
<string name="chromecast_subtitles_settings_des">Chromecast 자막 설정</string>
<string name="eigengraumode_settings">배속 모드</string>
<string name="eigengraumode_settings_des">플레이어에 속도 옵션을 추가합니다</string>
<string name="swipe_to_seek_settings">스와이프하여 탐색</string>
<string name="swipe_to_seek_settings_des">좌우로 스와이프하여 동영상 위치 제어하기</string>
<string name="swipe_to_change_settings">스와이프하여 설정 변경</string>

View File

@ -67,7 +67,6 @@
<string name="delete">Ištrinti</string>
<string name="cancel">Atšaukti</string>
<string name="start">Pradėti</string>
<string name="eigengraumode_settings_des">Prideda greičio pasirinkti grotuve</string>
<string name="cartoons_singular">Filmukas</string>
<string name="download_canceled">Atsiuntimas atšauktas</string>
<string name="advanced_search">Išplėstinė paieška</string>

View File

@ -124,7 +124,6 @@
<string name="chromecast_subtitles_settings">Chromecast subtitri</string>
<string name="chromecast_subtitles_settings_des">Chromecast subtitru iestāfijumi</string>
<string name="eigengraumode_settings">Eigengravy Mode</string>
<string name="eigengraumode_settings_des">Pievieno atskaņošanas ātrumu playerim</string>
<string name="swipe_to_seek_settings">Novelc lai paradītu</string>
<string name="swipe_to_seek_settings_des">Novelc no māla lidz malai lai pozicionētu video</string>
<string name="swipe_to_change_settings">Novēlu lai mainītu iestādījums</string>

View File

@ -90,7 +90,6 @@
<string name="player_subtitles_settings">Преводи</string>
<string name="player_subtitles_settings_des">Поставки на плеерот за преводи</string>
<string name="eigengraumode_settings">Режим на Eigengravy</string>
<string name="eigengraumode_settings_des">Додава можност за брзина на снимка во плеерот</string>
<string name="swipe_to_seek_settings">Повлечете за да барате</string>
<string name="swipe_to_seek_settings_des">Повлечете од страна на страна за да ја контролирате вашата позиција во видеото</string>
<string name="swipe_to_change_settings">Повлечете за да ги промените поставките</string>
@ -336,7 +335,7 @@
<string name="pref_category_ui_features">Карактеристики</string>
<string name="asian_drama_singular">Азиска драма</string>
<string name="extras">Додатоци</string>
<string name="random_button_settings_desc">Се прикажува копче на почетната страница што може да избере случаен филм или ТВ серија од почетната страница</string>
<string name="random_button_settings_desc">Прикажи случајно копче на почетната страница и библиотеката</string>
<string name="extension_types">Поддржано</string>
<string name="category_account">Сметки</string>
<string name="skip_type_intro">Вовед</string>
@ -531,4 +530,65 @@
<string name="home_change_provider_img_des">Смени провајдер</string>
<string name="go_back_img_des">Оди назад</string>
<string name="cast_format" formatted="true">Актери: %s</string>
<string name="favorite_added">%s додадени на фаворити</string>
<string name="favorite_removed">%s избришено од фаворити</string>
<string name="automatic_plugin_download_mode_title">Одбери опција за филтрирање на превземени плагини</string>
<string name="no_repository_found_error">Складиштето не е пронајдено, проверете го URL-то и пробајте VPN</string>
<string name="favorites_list_name">Фаворити</string>
<string name="action_add_to_favorites">Додадено во фаворити</string>
<string name="action_remove_from_favorites">Избриши од фаворити</string>
<string name="duplicate_replace_all">Замени ги сите</string>
<string name="duplicate_message_single" formatted="true">Се чини дека потенцијално дупликат ставка веќе постои во вашата библиотека: „%s“.
\n
\nДали сепак сакате да ја додадете оваа ставка, да ја замените постојната или да го откажете дејството?</string>
<string name="duplicate_message_multiple" formatted="true">Во вашата библиотека се пронајдени потенцијални дупликати ставки:
\n
\n%s
\n
\nДали сепак сакате да ја додадете оваа ставка, да ги замените постоечките или да го откажете дејството?</string>
<string name="enter_pin_with_name" formatted="true">Внеси ПИН за %s</string>
<string name="pin_error_length">ПИН-от мора да биде 4 карактери</string>
<string name="manage_accounts">Менаџирај кориснички сметки</string>
<string name="edit_account">Измени корисничка сметка</string>
<string name="logged_account" formatted="true">Логиран како %s</string>
<string name="skip_startup_account_select_pref">Прескокнете го изборот на корисничка сметка при стартување</string>
<string name="use_default_account">Користете ја стандардната сметка</string>
<string name="rotate_video">Ротирај</string>
<string name="rotate_video_desc">Прикажете копче за префрлување за ориентација на екранот</string>
<string name="already_voted">Веќе гласаше</string>
<string name="quality_profile_help">Овде можете да го промените начинот на кој се подредуваат изворите. Ако видеото има повисок приоритет, ќе се појави повисоко во изборот на изворот. Збирот на приоритетот на изворот и приоритетот на квалитетот е приоритет на видеото.
\n
\nИзвор А: 3
\nКвалитет Б: 7
\nЌе има комбиниран приоритет на видеото од 10.
\n
\nЗАБЕЛЕШКА: Ако сумата е 10 или повеќе, играчот автоматски ќе го прескокне вчитувањето кога ќе се вчита таа врска!</string>
<string name="select_an_account">Одбери корисничка сметка</string>
<string name="links_reloaded_toast">Повторно вчитани линкови</string>
<string name="disable">Оневозможи</string>
<string name="enter_pin">Внеси ПИН</string>
<string name="enter_current_pin">Внеси моментален ПИН</string>
<string name="pin">ПИН</string>
<string name="lock_profile">Заклучи профил</string>
<string name="pin_error_incorrect">Неточен ПИН. Пробај повторно.</string>
<string name="action_unsubscribe">Исклучи претплата</string>
<string name="profile_number">Профил %d</string>
<string name="action_subscribe">Претплати се</string>
<string name="profile_background_des">Позадина на профил</string>
<string name="mobile_data">Мобилен интернет</string>
<string name="set_default">Поставете стандардно</string>
<string name="qualities">Квалитети</string>
<string name="duplicate_title">Пронајдени потенцијални дупликати</string>
<string name="duplicate_add">Додади</string>
<string name="duplicate_replace">Замени</string>
<string name="wifi">Wi-Fi</string>
<string name="no_plugins_found_error">Не се најдени плагини во складиштето</string>
<string name="use">Користи</string>
<string name="edit">Измени</string>
<string name="profiles">Профили</string>
<string name="help">Помош</string>
<string name="unable_to_inflate">UI-то не можеше да се креира правилно, ова е ГОЛЕМ БАГ и треба веднаш да се пријави %s</string>
<string name="backup_frequency">Зачестеност на зачувување на бекап</string>
<string name="auto_rotate_video_desc">Овозможете автоматско префрлување на ориентацијата на екранот врз основа на видео ориентација</string>
<string name="auto_rotate_video">Автоматска ротација</string>
</resources>

View File

@ -3,14 +3,14 @@
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
<string name="player_speed_text_format" formatted="true">വേഗം (%.2fx)</string>
<string name="rated_format" formatted="true">റേറ്റിംഗ്: %.1f</string>
<string name="new_update_format" formatted="true">പുതിയ അപ്ഡേറ്റ്
<string name="new_update_format" formatted="true">പുതിയ അപ്ഡേറ്റ്!
\n%1$s -&gt; %2$s</string>
<string name="app_name">CloudStream</string>
<string name="app_name">ക്ലൗഡ് സ്ട്രീം</string>
<string name="title_home">ഹോം</string>
<string name="title_search">തിരയുക</string>
<string name="title_downloads">ഡൗൺലോഡ്സ്</string>
<string name="title_settings">സെറ്റിങ്‌സ്</string>
<string name="search_hint">തിരയുക</string>
<string name="search_hint">തിരയുക</string>
<string name="no_data">ടാറ്റ ലഭ്യമല്ല</string>
<string name="episode_more_options_des">കൂടുതൽ ഓപ്ഷൻസ്</string>
<string name="next_episode">അടുത്ത എപ്പിസോഡ്</string>
@ -84,8 +84,6 @@
<string name="player_size_settings_des">കറുത്ത അതിർത്തി നീക്കംചെയ്യുക</string>
<!-- <string name="player_subtitles_settings">Subtitles</string> -->
<string name="player_subtitles_settings_des">പ്ലേയർ സബ്‌ടൈറ്റിലുകളുടെ സെറ്റിങ്‌സ്</string>
<!-- <string name="eigengraumode_setthings">Eigengrau Mode</string> -->
<string name="eigengraumode_settings_des">വേഗം നിയന്ത്രിക്കാൻ ഓപ്ഷൻ ചേർക്കുക</string>
<!-- <string name="swipe_to_seek_setthings">Swipe to seek</string> -->
<string name="swipe_to_seek_settings_des">വീഡിയോപ്ലേയറിൽ സമയം നിയന്ത്രിക്കാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ സ്വൈപ്പുചെയ്യുക</string>
<!-- <string name="swipe_to_change_settings">Swipe to change settings</string> -->
@ -169,11 +167,11 @@
<string name="watch_quality_pref">ഔചിത്യ വീഡിയോ ക്വാളിറ്റി</string>
<string name="history">ചരിത്രം</string>
<string name="action_mark_as_watched">കണ്ടതാണെന്ന് അടയാളപ്പെടുത്തുക</string>
<string name="next_episode_time_day_format" formatted="true">%1$d%2$d</string>
<string name="next_episode_format" formatted="true">yg5t4r%dujyhtg</string>
<string name="next_episode_time_hour_format" formatted="true">%d മണിക്കൂർ %d മിനിറ്റ്</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%1$sghj%2$d</string>
<string name="cast_format" formatted="true">rtf:%</string>
<string name="next_episode_time_day_format" formatted="true">%d ദിവസങ്ങൾ %d മണിക്കൂർ %d മിനിറ്റ്</string>
<string name="next_episode_format" formatted="true">അധ്യായം%dൽ റിലീസ് ചെയ്യും</string>
<string name="next_episode_time_hour_format" formatted="true">%1$d മണിക്കൂർ %2$d മിനിറ്റ്</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%1$sഅധ്യാ%2$d</string>
<string name="cast_format" formatted="true">കാസ്റ്റ്:%s</string>
<string name="create_account">അക്കൗണ്ട് ഉണ്ടാക്കുക</string>
<string name="delayed_update_notice">പുറത്ത്പോകുന്നതോടുകൂടി ആപ് അപ്ഡേറ്റ് ആവുന്നതാണ്</string>
<string name="select_library">ലൈബ്രറി തിരഞ്ഞെടുക്കുക</string>
@ -181,8 +179,8 @@
<string name="play_trailer_button">ട്രെയിലർ പ്ലേ ചെയ്യുക</string>
<string name="play_livestream_button">ലൈവ് സ്ട്രീം പ്ലേ ചെയ്യുക</string>
<string name="filler" formatted="true">ഫില്ലർ</string>
<string name="duration_format" formatted="true">%d min</string>
<string name="play_with_app_name">ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് കളിക്കുക</string>
<string name="duration_format" formatted="true">%d മിനിറ്റ്</string>
<string name="play_with_app_name">ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് പ്രവർത്തിപ്പിക്കുക</string>
<string name="home_next_random_img_des">അടുത്ത ക്രമരഹിതമായ</string>
<string name="episode_poster_img_des">എപ്പിസോഡ് പോസ്റ്റർ</string>
<string name="update_started">അപ്ഡേറ്റ് ആരംഭിച്ചു</string>
@ -190,7 +188,7 @@
<string name="search_poster_img_des">പോസ്റ്റർ</string>
<string name="skip_loading">ലോഡിംഗ് ഒഴിവാക്കുക</string>
<string name="search_hint_site" formatted="true">തിരയുക %s…</string>
<string name="next_episode_time_min_format" formatted="true">%dm</string>
<string name="next_episode_time_min_format" formatted="true">%dമിനിറ്റ്</string>
<string name="go_back_img_des">മടങ്ങിപ്പോവുക</string>
<string name="preview_background_img_des">പശ്ചാത്തല പ്രിവ്യൂ</string>
<string name="result_poster_img_des">പോസ്റ്റർ</string>
@ -204,8 +202,6 @@
<string name="view_public_repositories_button_short">പൊതു പട്ടിക</string>
<string name="blank_repo_message">CloudStream-ന് സ്ഥിരസ്ഥിതിയായി സൈറ്റുകളൊന്നും ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. നിങ്ങൾ റിപ്പോസിറ്ററികളിൽ നിന്ന് സൈറ്റുകൾ ഇൻസ്റ്റാൾ ചെയ്യേണ്ടതുണ്ട്.
\n
\nസ്കൈ യുകെ ലിമിറ്റഡിലെ ഡോഗ്‌ഷിറ്റ് ആളുകളിൽ നിന്ന് DMCA നീക്കം ചെയ്‌തതിനാൽ 🤮 ഞങ്ങൾക്ക് ആപ്പിൽ റിപ്പോസിറ്ററി സൈറ്റ് ലിങ്ക് ചെയ്യാൻ കഴിയില്ല.
\n
\nഞങ്ങളുടെ ഡിസ്കോർഡിൽ ചേരുക അല്ലെങ്കിൽ ഓൺലൈനിൽ തിരയുക.</string>
<string name="sort_copy">പകർത്തുക</string>
<string name="uppercase_all_subtitles">എല്ലാ സബ്‌ടൈറ്റിലുകളും വലിയക്ഷരമാക്കുക</string>
@ -216,7 +212,7 @@
<string name="manage_accounts">അക്കൗണ്ടുകൾ കൈകാര്യം ചെയ്യുക</string>
<string name="coming_soon">ഉടൻ വരുന്നു…</string>
<string name="apply_on_restart">പുനരാരംഭിക്കുമ്പോൾ പ്രയോഗിക്കുക</string>
<string name="edit_account">അക്കൗണ്ട് എഡിറ്റ് ചെയ്യുക</string>
<string name="edit_account">അക്കൗണ്ട് തിരുത്തുക</string>
<string name="pin_error_incorrect">തെറ്റായ പിൻ. ദയവായി വീണ്ടും ശ്രമിക്കുക.</string>
<string name="stop">നിർത്തുക</string>
<string name="tracks">ട്രാക്കുകൾ</string>
@ -247,4 +243,41 @@
<string name="source_error">ഉറവിട പിശക്</string>
<string name="enter_current_pin">നിലവിലെ പിൻ നൽകുക</string>
<string name="audio_tracks">ഓഡിയോ ട്രാക്കുകൾ</string>
<string name="picture_in_picture">ചിത്രം-ഇൻ-ചിത്രം</string>
<string name="sort_updated_old">പുതുക്കിയത് (പഴയത് മുതൽ പുതിയത് വരെ)</string>
<string name="sort_rating_desc">റേറ്റിംഗ് (ഉയർന്നത് മുതൽ താഴ്ന്നത്)</string>
<string name="apk_installer_legacy">പാരമ്പര്യം</string>
<string name="subs_window_color">വിൻഡോ നിറം</string>
<string name="sort_clear">ക്ലിയർ</string>
<string name="test_log">ലോഗ്</string>
<string name="recommendations_tooltip">ശുപാർശകൾ കാണിക്കുക</string>
<string name="logged_account" formatted="true">%s ആയി ലോഗിൻ ചെയ്തു</string>
<string name="sort_by">ഇങ്ങനെ അടുക്കുക</string>
<string name="sort">അടുക്കുക</string>
<string name="edit">തിരുത്തുക</string>
<string name="sort_updated_new">പുതുക്കിയത് (പുതിയത് മുതൽ പഴയത് വരെ)</string>
<string name="nsfw_singular">NSFW</string>
<string name="update_notification_installing">ആപ്പ് അപ്ഡേറ്റ് ഇൻസ്റ്റാൾ ചെയ്യുന്നു…</string>
<string name="category_updates">അപ്ഡേറ്റുകളും ഒപ്പം ബാക്കപ്പും</string>
<string name="single_plugin_disabled" formatted="true">%s(അപ്രാപ്തമാക്കി)</string>
<string name="sort_rating_asc">റേറ്റിംഗ് (താഴ്ന്നത് മുതൽ ഉയർന്നത് വരെ)</string>
<string name="subs_text_color">വാചക നിറം</string>
<string name="update_notification_failed">ആപ്പിൻ്റെ പുതിയ പതിപ്പ് ഇൻസ്റ്റാൾ ചെയ്യാനായില്ല</string>
<string name="apk_installer_package_installer">പാക്കേജ് ഇൻസ്റ്റാളർ</string>
<string name="sort_alphabetical_a">അക്ഷരമാലാക്രമം (A മുതൽ Z വരെ)</string>
<string name="sort_alphabetical_z">അക്ഷരമാലാക്രമം (Z മുതൽ A വരെ)</string>
<string name="empty_library_logged_in_message">ഈ ലിസ്റ്റ് ശൂന്യമാണ്. മറ്റൊന്നിലേക്ക് മാറാൻ ശ്രമിക്കുക.</string>
<string name="clear_history">ചരിത്രം മായ്ക്കുക</string>
<string name="show_log_cat">ലോഗ്കാറ്റ് കാണിക്കുക 🐈</string>
<string name="empty_library_no_accounts_message">നിങ്ങളുടെ ലൈബ്രറി ശൂന്യമാണ് :(
\nഒരു ലൈബ്രറി അക്കൗണ്ടിൽ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ നിങ്ങളുടെ പ്രാദേശിക ലൈബ്രറിയിലേക്ക് ഷോകൾ ചേർക്കുക.</string>
<string name="other_singular">വീഡിയോ</string>
<string name="repo_copy_label">റിപ്പോസിറ്ററി നാമവും URL ഉം</string>
<string name="toast_copied">പകർത്തി!</string>
<string name="subscribe_tooltip">പുതിയ എപ്പിസോഡ് അറിയിപ്പ്</string>
<string name="result_search_tooltip">മറ്റ് വിപുലീകരണങ്ങളിൽ തിരയുക</string>
<string name="subtitles_settings">ഉപശീർഷകം ക്രമീകരണങ്ങൾ</string>
<string name="subs_edge_type">എഡ്ജ് തരം</string>
<string name="subs_outline_color">ഔട്ട്ലൈൻ നിറം</string>
<string name="subs_background_color">പശ്ചാത്തല നിറം</string>
</resources>

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="subtitles_settings">Preferenzi tas-sottotitli</string>
<string name="subs_text_color">Kulur tal-kitba</string>
<string name="subs_window_color">Kulur tat-Tieqa</string>
<string name="search_provider_text_types">Fittex bl-użu ta \'tipi</string>
<string name="subs_import_text" formatted="true">Importa fonts billi tpoġġihom ġo %s</string>
<string name="vpn_torrent">Dan il-fornitur huwa torrent, VPN huwa rakkomandat</string>
<string name="cast_format" formatted="true">Atturi: %s</string>
<string name="next_episode_format" formatted="true">L-episodju %d ha johrog fil</string>
<string name="next_episode_time_hour_format" formatted="true">%1$dh %2$dm</string>
<string name="next_episode_time_min_format" formatted="true">%dm</string>
<string name="result_poster_img_des">Kartellun</string>
<string name="search_poster_img_des">Kartellun</string>
<string name="episode_poster_img_des">Kartellun tal-episodju</string>
<string name="home_main_poster_img_des">Kartellun Principali</string>
<string name="home_next_random_img_des">Li jmiss bl\'addoċċ</string>
<string name="home_change_provider_img_des">Ibdel Il-fornitur</string>
<string name="player_speed_text_format" formatted="true">veloċità (%.2fx)</string>
<string name="rated_format" formatted="true">Klassifikazzjoni: %.1f</string>
<string name="new_update_format" formatted="true">Aġġornament ġdid misjub!
\n%1$s -&gt; %2$s</string>
<string name="duration_format" formatted="true">%d min</string>
<string name="app_name">CloudStream</string>
<string name="play_with_app_name">Ara bil-CloudStream</string>
<string name="title_home">Dar</string>
<string name="title_search">Fittex</string>
<string name="title_downloads">Imnizzel</string>
<string name="title_settings">Preferenzi</string>
<string name="search_hint">Fittex…</string>
<string name="search_hint_site" formatted="true">Fittex%s…</string>
<string name="no_data">Bla dejta</string>
<string name="episode_more_options_des">Iktar Preferenzi</string>
<string name="next_episode">L-episodju li\'jmiss</string>
<string name="result_tags">Ġeneri</string>
<string name="result_share">Aqsam</string>
<string name="result_open_in_browser">Iftah fil-brawser</string>
<string name="browser">Brawser</string>
<string name="skip_loading">Aqbez it-tagħbija</string>
<string name="loading">Tagħbija…</string>
<string name="type_watching">Jaraw</string>
<string name="type_on_hold">Stenna ftit</string>
<string name="type_completed">Lest</string>
<string name="type_dropped">Imwaqqa</string>
<string name="type_plan_to_watch">Pjana biex tara</string>
<string name="type_re_watching">Terġa\' tara</string>
<string name="play_trailer_button">Ibda t-trejler</string>
<string name="play_livestream_button">Ibda l-livestream</string>
<string name="play_torrent_button">Stream Torrent</string>
<string name="pick_source">Sorsi</string>
<string name="reload_error">Erġa\' pprova l-konnessjoni…</string>
<string name="go_back">Mur lura</string>
<string name="play_episode">Ibda l-episodju</string>
<string name="download_paused">Tniżżila ppawzata</string>
<string name="downloading">Qed jinżlu</string>
<string name="downloaded">Imniżżel</string>
<string name="download_canceled">Tniżżil ikkanċellat</string>
<string name="download_done">Lest it-tniżżil</string>
<string name="update_started">Beda l-aġġornament</string>
<string name="stream">Network stream</string>
<string name="error_loading_links_toast">Tagħbija tal-Links falliet</string>
<string name="links_reloaded_toast">Links regaw gew mogħbija</string>
<string name="download_storage_text">Ħażna Interna</string>
<string name="app_dubbed_text">Dub</string>
<string name="home_play">Ibda</string>
<string name="home_info">Info</string>
<string name="action_add_to_bookmarks">Issettja l-istatus ta-rajtux</string>
<string name="sort_apply">Applika</string>
<string name="sort_copy">Ikkopja</string>
<string name="sort_close">Għalaq</string>
<string name="sort_clear">Neħħi</string>
<string name="sort_save">Issevja</string>
<string name="repo_copy_label">Isem tar-repożitorju u URL</string>
<string name="toast_copied">Ikkupjat!</string>
<string name="subscribe_tooltip">Notifika ta\' episodju ġdid</string>
<string name="result_search_tooltip">Fittex f\'estensjonijiet oħra</string>
<string name="recommendations_tooltip">Uri r-rakkomandazzjonijiet</string>
<string name="player_speed">Veloċità tal-Plejer</string>
<string name="subs_outline_color">Kulur tal-Kontorn</string>
<string name="subs_background_color">Kulur tal-Isfond</string>
<string name="subs_edge_type">Tip tat-tarf</string>
<string name="subs_subtitle_elevation">Elevazzjoni tas-Sottotitolu</string>
<string name="subs_font">Font</string>
<string name="subs_font_size">Daqs tal-font</string>
<string name="search_provider_text_providers">Fittex bl-użu ta\' fornituri</string>
<string name="benene_count_text">%d Benenes mogħtija lil devs</string>
<string name="benene_count_text_none">Ebda Benenes mogħtija</string>
<string name="subs_auto_select_language">Agħżel il-Lingwa Awtomatikament</string>
<string name="subs_download_languages">Niżżel Lingwi</string>
<string name="subs_subtitle_languages">Lingwa tas-sottotitolu</string>
<string name="subs_hold_to_reset_to_default">Żomm biex tirrisettja għal default</string>
<string name="continue_watching">Kompli Ara</string>
<string name="action_remove_watching">Neħħi</string>
<string name="action_open_watching">Iktar informazzjoni</string>
<string name="action_open_play">@string/home_play</string>
<string name="vpn_might_be_needed">Jista\' jkun hemm bżonn ta\' VPN biex dan il-fornitur jaħdem b\'mod korrett</string>
<string name="provider_info_meta">Il-metadata mhix ipprovduta mis-sit, it-tagħbija tal-vidjo se tfalli jekk ma teżistix fuq is-sit.</string>
<string name="torrent_plot">Deskrizzjoni</string>
<string name="normal_no_plot">Lebda Plot misjub</string>
<string name="torrent_no_plot">Lebda Deskrizzjoni misjuba</string>
<string name="show_log_cat">Uri Logcat 🐈</string>
<string name="test_log">ġurnal</string>
<string name="picture_in_picture">Stampa f-istampa</string>
<string name="picture_in_picture_des">Ikompli d-daqq fi player minjatura fuq apps oħra</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%1$s Ep %2$d</string>
<string name="next_episode_time_day_format" formatted="true">%1$dd %2$dh %3$dm</string>
<string name="go_back_img_des">Mur Lura</string>
<string name="preview_background_img_des">Ara l\'isfond</string>
<string name="filler" formatted="true">Mili</string>
<string name="play_movie_button">Ibda l-film</string>
<string name="pick_subtitle">Sottotitli</string>
<string name="app_subbed_text">Sut</string>
<string name="popup_play_file">Ibda l-fajl</string>
<string name="download">Niżżel</string>
<string name="popup_delete_file">Hassar il-fajl</string>
<string name="popup_resume_download">Kompli Nizzel</string>
<string name="popup_pause_download">Ieqaf Nizzel</string>
<string name="pref_disable_acra">Iddiżattiva r-rappurtar awtomatiku tal-bugs</string>
<string name="home_more_info">Iktar Informazzjoni</string>
<string name="home_expanded_hide">Aħbi</string>
<string name="filter_bookmarks">Iffiltra l-Bookmarks</string>
<string name="download_started">Beda t-tniżżil</string>
<string name="error_bookmarks_text">Bookmarks</string>
<string name="action_remove_from_bookmarks">Neħħi</string>
<string name="download_failed">Falla t-tniżżil</string>
</resources>

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