Merge branch 'master' into feature/remote-sync
# Conflicts: # app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt # app/src/main/res/values/strings.xml
|
@ -242,7 +242,7 @@ dependencies {
|
|||
//implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
|
||||
|
||||
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204
|
||||
implementation("com.github.TeamNewPipe:NewPipeExtractor:v0.22.6")
|
||||
implementation("com.github.TeamNewPipe:NewPipeExtractor:master-SNAPSHOT")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
|
||||
|
||||
// Library/extensions searching with Levenshtein distance
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class Bestx : Chillx() {
|
||||
override val name = "Bestx"
|
||||
override val mainUrl = "https://bestx.stream"
|
||||
}
|
||||
|
||||
class Watchx : Chillx() {
|
||||
override val name = "Watchx"
|
||||
override val mainUrl = "https://watchx.top"
|
||||
}
|
||||
open class Chillx : ExtractorApi() {
|
||||
override val name = "Chillx"
|
||||
override val mainUrl = "https://chillx.top"
|
||||
override val requiresReferer = true
|
||||
|
||||
companion object {
|
||||
private const val KEY = "4VqE3#N7zt&HEP^a"
|
||||
}
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val master = Regex("MasterJS\\s*=\\s*'([^']+)").find(
|
||||
app.get(
|
||||
url,
|
||||
referer = referer
|
||||
).text
|
||||
)?.groupValues?.get(1)
|
||||
val encData = AppUtils.tryParseJson<AESData>(base64Decode(master ?: return))
|
||||
val decrypt = cryptoAESHandler(encData ?: return, KEY, false)
|
||||
|
||||
val source = Regex("""sources:\s*\[\{"file":"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
||||
val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1)
|
||||
|
||||
// required
|
||||
val headers = mapOf(
|
||||
"Accept" to "*/*",
|
||||
"Connection" to "keep-alive",
|
||||
"Sec-Fetch-Dest" to "empty",
|
||||
"Sec-Fetch-Mode" to "cors",
|
||||
"Sec-Fetch-Site" to "cross-site",
|
||||
"Origin" to mainUrl,
|
||||
)
|
||||
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
source ?: return,
|
||||
"$mainUrl/",
|
||||
Qualities.P1080.value,
|
||||
headers = headers,
|
||||
isM3u8 = true
|
||||
)
|
||||
)
|
||||
|
||||
AppUtils.tryParseJson<List<Tracks>>("[$tracks]")
|
||||
?.filter { it.kind == "captions" }?.map { track ->
|
||||
subtitleCallback.invoke(
|
||||
SubtitleFile(
|
||||
track.label ?: "",
|
||||
track.file ?: return@map null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cryptoAESHandler(
|
||||
data: AESData,
|
||||
pass: String,
|
||||
encrypt: Boolean = true
|
||||
): String {
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")
|
||||
val spec = PBEKeySpec(
|
||||
pass.toCharArray(),
|
||||
data.salt?.hexToByteArray(),
|
||||
data.iterations?.toIntOrNull() ?: 1,
|
||||
256
|
||||
)
|
||||
val key = factory.generateSecret(spec)
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
return if (!encrypt) {
|
||||
cipher.init(
|
||||
Cipher.DECRYPT_MODE,
|
||||
SecretKeySpec(key.encoded, "AES"),
|
||||
IvParameterSpec(data.iv?.hexToByteArray())
|
||||
)
|
||||
String(cipher.doFinal(base64DecodeArray(data.ciphertext.toString())))
|
||||
} else {
|
||||
cipher.init(
|
||||
Cipher.ENCRYPT_MODE,
|
||||
SecretKeySpec(key.encoded, "AES"),
|
||||
IvParameterSpec(data.iv?.hexToByteArray())
|
||||
)
|
||||
base64Encode(cipher.doFinal(data.ciphertext?.toByteArray()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.hexToByteArray(): ByteArray {
|
||||
check(length % 2 == 0) { "Must have an even length" }
|
||||
return chunked(2)
|
||||
.map { it.toInt(16).toByte() }
|
||||
|
||||
.toByteArray()
|
||||
}
|
||||
|
||||
data class AESData(
|
||||
@JsonProperty("ciphertext") val ciphertext: String? = null,
|
||||
@JsonProperty("iv") val iv: String? = null,
|
||||
@JsonProperty("salt") val salt: String? = null,
|
||||
@JsonProperty("iterations") val iterations: String? = null,
|
||||
)
|
||||
|
||||
data class Tracks(
|
||||
@JsonProperty("file") val file: String? = null,
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
@JsonProperty("kind") val kind: String? = null,
|
||||
)
|
||||
}
|
|
@ -58,7 +58,7 @@ open class DoodLaExtractor : ExtractorApi() {
|
|||
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
|
||||
return listOf(
|
||||
ExtractorLink(
|
||||
trueUrl,
|
||||
this.name,
|
||||
this.name,
|
||||
trueUrl,
|
||||
mainUrl,
|
||||
|
|
|
@ -5,6 +5,25 @@ import com.lagradost.cloudstream3.app
|
|||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||
|
||||
class Moviesm4u : Filesim() {
|
||||
override val mainUrl = "https://moviesm4u.com"
|
||||
override val name = "Moviesm4u"
|
||||
}
|
||||
|
||||
class FileMoonIn : Filesim() {
|
||||
override val mainUrl = "https://filemoon.in"
|
||||
override val name = "FileMoon"
|
||||
}
|
||||
|
||||
class StreamhideCom : Filesim() {
|
||||
override var name: String = "Streamhide"
|
||||
override var mainUrl: String = "https://streamhide.com"
|
||||
}
|
||||
|
||||
class Movhide : Filesim() {
|
||||
override var name: String = "Movhide"
|
||||
override var mainUrl: String = "https://movhide.pro"
|
||||
}
|
||||
|
||||
class Ztreamhub : Filesim() {
|
||||
override val mainUrl: String = "https://ztreamhub.com" //Here 'cause works
|
||||
|
@ -35,7 +54,7 @@ open class Filesim : ExtractorApi() {
|
|||
response.select("script[type=text/javascript]").map { script ->
|
||||
if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) {
|
||||
val unpackedscript = getAndUnpack(script.data())
|
||||
val m3u8Regex = Regex("file.\\\"(.*?m3u8.*?)\\\"")
|
||||
val m3u8Regex = Regex("file.\"(.*?m3u8.*?)\"")
|
||||
val m3u8 = m3u8Regex.find(unpackedscript)?.destructured?.component1() ?: ""
|
||||
if (m3u8.isNotEmpty()) {
|
||||
generateM3u8(
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
||||
open class Gofile : ExtractorApi() {
|
||||
override val name = "Gofile"
|
||||
override val mainUrl = "https://gofile.io"
|
||||
override val requiresReferer = false
|
||||
private val mainApi = "https://api.gofile.io"
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z]+)").find(url)?.groupValues?.get(1)
|
||||
val token = app.get("$mainApi/createAccount").parsedSafe<Account>()?.data?.get("token")
|
||||
app.get("$mainApi/getContent?contentId=$id&token=$token&websiteToken=12345")
|
||||
.parsedSafe<Source>()?.data?.contents?.forEach {
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
it.value["link"] ?: return,
|
||||
"",
|
||||
getQuality(it.value["name"]),
|
||||
headers = mapOf(
|
||||
"Cookie" to "accountToken=$token"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun getQuality(str: String?): Int {
|
||||
return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||
?: Qualities.Unknown.value
|
||||
}
|
||||
|
||||
data class Account(
|
||||
@JsonProperty("data") val data: HashMap<String, String>? = null,
|
||||
)
|
||||
|
||||
data class Data(
|
||||
@JsonProperty("contents") val contents: HashMap<String, HashMap<String, String>>? = null,
|
||||
)
|
||||
|
||||
data class Source(
|
||||
@JsonProperty("data") val data: Data? = null,
|
||||
)
|
||||
|
||||
}
|
|
@ -58,7 +58,7 @@ open class GuardareStream : ExtractorApi() {
|
|||
jsonVideoData.data.forEach {
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
it.file + ".${it.type}",
|
||||
this.name,
|
||||
this.name,
|
||||
it.file + ".${it.type}",
|
||||
mainUrl,
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.httpsify
|
||||
|
||||
open class Krakenfiles : ExtractorApi() {
|
||||
override val name = "Krakenfiles"
|
||||
override val mainUrl = "https://krakenfiles.com"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val id = Regex("/(?:view|embed-video)/([\\da-zA-Z]+)").find(url)?.groupValues?.get(1)
|
||||
val doc = app.get("$mainUrl/embed-video/$id").document
|
||||
val link = doc.selectFirst("source")?.attr("src")
|
||||
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
httpsify(link ?: return),
|
||||
"",
|
||||
Qualities.Unknown.value
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -6,6 +6,26 @@ import com.lagradost.cloudstream3.app
|
|||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import kotlin.random.Random
|
||||
|
||||
class Vidgomunimesb : StreamSB() {
|
||||
override var mainUrl = "https://vidgomunimesb.xyz"
|
||||
}
|
||||
|
||||
class Sbasian : StreamSB() {
|
||||
override var mainUrl = "https://sbasian.pro"
|
||||
override var name = "Sbasian"
|
||||
}
|
||||
|
||||
class Sbnet : StreamSB() {
|
||||
override var name = "Sbnet"
|
||||
override var mainUrl = "https://sbnet.one"
|
||||
}
|
||||
|
||||
class Keephealth : StreamSB() {
|
||||
override var name = "Keephealth"
|
||||
override var mainUrl = "https://keephealth.info"
|
||||
}
|
||||
|
||||
class Sbspeed : StreamSB() {
|
||||
override var name = "Sbspeed"
|
||||
|
@ -85,24 +105,62 @@ class Sblongvu : StreamSB() {
|
|||
override var mainUrl = "https://sblongvu.com"
|
||||
}
|
||||
|
||||
// This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt
|
||||
// The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE
|
||||
open class StreamSB : ExtractorApi() {
|
||||
override var name = "StreamSB"
|
||||
override var mainUrl = "https://watchsb.com"
|
||||
override val requiresReferer = false
|
||||
private val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
private val hexArray = "0123456789ABCDEF".toCharArray()
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val regexID =
|
||||
Regex("(embed-[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+|/e/[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+)")
|
||||
val id = regexID.findAll(url).map {
|
||||
it.value.replace(Regex("(embed-|/e/)"), "")
|
||||
}.first()
|
||||
val master = "$mainUrl/375664356a494546326c4b797c7c6e756577776778623171737/${encodeId(id)}"
|
||||
val headers = mapOf(
|
||||
"watchsb" to "sbstream",
|
||||
)
|
||||
val mapped = app.get(
|
||||
master.lowercase(),
|
||||
headers = headers,
|
||||
referer = url,
|
||||
).parsedSafe<Main>()
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
mapped?.streamData?.file ?: return,
|
||||
url,
|
||||
headers = headers
|
||||
).forEach(callback)
|
||||
|
||||
private fun bytesToHex(bytes: ByteArray): String {
|
||||
val hexChars = CharArray(bytes.size * 2)
|
||||
for (j in bytes.indices) {
|
||||
val v = bytes[j].toInt() and 0xFF
|
||||
|
||||
hexChars[j * 2] = hexArray[v ushr 4]
|
||||
hexChars[j * 2 + 1] = hexArray[v and 0x0F]
|
||||
mapped.streamData.subs?.map {sub ->
|
||||
subtitleCallback.invoke(
|
||||
SubtitleFile(
|
||||
sub.label.toString(),
|
||||
sub.file ?: return@map null,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun encodeId(id: String): String {
|
||||
val code = "${createHashTable()}||$id||${createHashTable()}||streamsb"
|
||||
return code.toCharArray().joinToString("") { char ->
|
||||
char.code.toString(16)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createHashTable(): String {
|
||||
return buildString {
|
||||
repeat(12) {
|
||||
append(alphabet[Random.nextInt(alphabet.length)])
|
||||
}
|
||||
}
|
||||
return String(hexChars)
|
||||
}
|
||||
|
||||
data class Subs (
|
||||
|
@ -126,42 +184,4 @@ open class StreamSB : ExtractorApi() {
|
|||
@JsonProperty("status_code") val statusCode: Int,
|
||||
)
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val regexID =
|
||||
Regex("(embed-[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+|/e/[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)")
|
||||
val id = regexID.findAll(url).map {
|
||||
it.value.replace(Regex("(embed-|/e/)"), "")
|
||||
}.first()
|
||||
// val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
|
||||
val master = "$mainUrl/sources16/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
|
||||
val headers = mapOf(
|
||||
"watchsb" to "sbstream",
|
||||
)
|
||||
val mapped = app.get(
|
||||
master.lowercase(),
|
||||
headers = headers,
|
||||
referer = url,
|
||||
).parsedSafe<Main>()
|
||||
// val urlmain = mapped.streamData.file.substringBefore("/hls/")
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
mapped?.streamData?.file ?: return,
|
||||
url,
|
||||
headers = headers
|
||||
).forEach(callback)
|
||||
|
||||
mapped.streamData.subs?.map {sub ->
|
||||
subtitleCallback.invoke(
|
||||
SubtitleFile(
|
||||
sub.label.toString(),
|
||||
sub.file ?: return@map null,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ open class Tantifilm : ExtractorApi() {
|
|||
val jsonvideodata = parseJson<TantifilmJsonData>(response)
|
||||
return jsonvideodata.data.map {
|
||||
ExtractorLink(
|
||||
it.file+".${it.type}",
|
||||
this.name,
|
||||
this.name,
|
||||
it.file+".${it.type}",
|
||||
mainUrl,
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
||||
open class Uservideo : ExtractorApi() {
|
||||
override val name: String = "Uservideo"
|
||||
override val mainUrl: String = "https://uservideo.xyz"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val script = app.get(url).document.selectFirst("script:containsData(hosts =)")?.data()
|
||||
val host = script?.substringAfter("hosts = [\"")?.substringBefore("\"];")
|
||||
val servers = script?.substringAfter("servers = \"")?.substringBefore("\";")
|
||||
|
||||
val sources = app.get("$host/s/$servers").text.substringAfter("\"sources\":[").substringBefore("],").let {
|
||||
AppUtils.tryParseJson<List<Sources>>("[$it]")
|
||||
}
|
||||
val quality = Regex("(\\d{3,4})[Pp]").find(url)?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||
|
||||
sources?.map { source ->
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
source.src ?: return@map null,
|
||||
url,
|
||||
quality ?: Qualities.Unknown.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class Sources(
|
||||
@JsonProperty("src") val src: String? = null,
|
||||
@JsonProperty("type") val type: String? = null,
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
)
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
|
||||
open class Vicloud : ExtractorApi() {
|
||||
override val name: String = "Vicloud"
|
||||
override val mainUrl: String = "https://vicloud.sbs"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val id = Regex("\"apiQuery\":\"(.*?)\"").find(app.get(url).text)?.groupValues?.getOrNull(1)
|
||||
app.get(
|
||||
"$mainUrl/api/?$id=&_=${System.currentTimeMillis()}",
|
||||
headers = mapOf(
|
||||
"X-Requested-With" to "XMLHttpRequest"
|
||||
),
|
||||
referer = url
|
||||
).parsedSafe<Responses>()?.sources?.map { source ->
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
source.file ?: return@map null,
|
||||
url,
|
||||
getQualityFromName(source.label),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private data class Sources(
|
||||
@JsonProperty("file") val file: String? = null,
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
)
|
||||
|
||||
private data class Responses(
|
||||
@JsonProperty("sources") val sources: List<Sources>? = arrayListOf(),
|
||||
)
|
||||
|
||||
}
|
|
@ -6,6 +6,10 @@ 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"
|
||||
}
|
||||
|
||||
open class Voe : ExtractorApi() {
|
||||
override val name = "Voe"
|
||||
override val mainUrl = "https://voe.sx"
|
||||
|
@ -18,8 +22,8 @@ open class Voe : ExtractorApi() {
|
|||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val res = app.get(url, referer = referer).document
|
||||
val link = res.select("script").find { it.data().contains("const sources") }?.data()
|
||||
?.substringAfter("\"hls\": \"")?.substringBefore("\",")
|
||||
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,
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
|
||||
open class VoeExtractor : ExtractorApi() {
|
||||
override val name: String = "Voe"
|
||||
override val mainUrl: String = "https://voe.sx"
|
||||
override val requiresReferer = false
|
||||
|
||||
private data class ResponseLinks(
|
||||
@JsonProperty("hls") val hls: String?,
|
||||
@JsonProperty("mp4") val mp4: String?,
|
||||
@JsonProperty("video_height") val label: Int?
|
||||
//val type: String // Mp4
|
||||
)
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val html = app.get(url).text
|
||||
if (html.isNotBlank()) {
|
||||
val src = html.substringAfter("const sources =").substringBefore(";")
|
||||
// Remove last comma, it is not proper json otherwise
|
||||
.replace("0,", "0")
|
||||
// Make json use the proper quotes
|
||||
.replace("'", "\"")
|
||||
|
||||
//Log.i(this.name, "Result => (src) ${src}")
|
||||
parseJson<ResponseLinks?>(src)?.let { voeLink ->
|
||||
//Log.i(this.name, "Result => (voeLink) ${voeLink}")
|
||||
|
||||
// Always defaults to the hls link, but returns the mp4 if null
|
||||
val linkUrl = voeLink.hls ?: voeLink.mp4
|
||||
val linkLabel = voeLink.label?.toString() ?: ""
|
||||
if (!linkUrl.isNullOrEmpty()) {
|
||||
return listOf(
|
||||
ExtractorLink(
|
||||
name = this.name,
|
||||
source = this.name,
|
||||
url = linkUrl,
|
||||
quality = getQualityFromName(linkLabel),
|
||||
referer = url,
|
||||
isM3u8 = voeLink.hls != null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
}
|
|
@ -8,6 +8,16 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
|||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
|
||||
class StreamM4u : XStreamCdn() {
|
||||
override val name: String = "StreamM4u"
|
||||
override val mainUrl: String = "https://streamm4u.club"
|
||||
}
|
||||
|
||||
class Fembed9hd : XStreamCdn() {
|
||||
override var mainUrl = "https://fembed9hd.com"
|
||||
override var name = "Fembed9hd"
|
||||
}
|
||||
|
||||
class Cdnplayer: XStreamCdn() {
|
||||
override val name: String = "Cdnplayer"
|
||||
override val mainUrl: String = "https://cdnplayer.online"
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
package com.lagradost.cloudstream3.extractors.helper
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.base64Decode
|
||||
import com.lagradost.cloudstream3.base64DecodeArray
|
||||
import com.lagradost.cloudstream3.base64Encode
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import org.jsoup.nodes.Document
|
||||
import java.net.URI
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object GogoHelper {
|
||||
|
||||
/**
|
||||
* @param id base64Decode(show_id) + IV
|
||||
* @return the encryption key
|
||||
* */
|
||||
private fun getKey(id: String): String? {
|
||||
return normalSafeApiCall {
|
||||
id.map {
|
||||
it.code.toString(16)
|
||||
}.joinToString("").substring(0, 32)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/saikou-app/saikou/blob/45d0a99b8a72665a29a1eadfb38c506b842a29d7/app/src/main/java/ani/saikou/parsers/anime/extractors/GogoCDN.kt#L97
|
||||
// No Licence on the function
|
||||
private fun cryptoHandler(
|
||||
string: String,
|
||||
iv: String,
|
||||
secretKeyString: String,
|
||||
encrypt: Boolean = true
|
||||
): String {
|
||||
//println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string")
|
||||
val ivParameterSpec = IvParameterSpec(iv.toByteArray())
|
||||
val secretKey = SecretKeySpec(secretKeyString.toByteArray(), "AES")
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
return if (!encrypt) {
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec)
|
||||
String(cipher.doFinal(base64DecodeArray(string)))
|
||||
} else {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
|
||||
base64Encode(cipher.doFinal(string.toByteArray()))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iframeUrl something like https://gogoplay4.com/streaming.php?id=XXXXXX
|
||||
* @param mainApiName used for ExtractorLink names and source
|
||||
* @param iv secret iv from site, required non-null if isUsingAdaptiveKeys is off
|
||||
* @param secretKey secret key for decryption from site, required non-null if isUsingAdaptiveKeys is off
|
||||
* @param secretDecryptKey secret key to decrypt the response json, required non-null if isUsingAdaptiveKeys is off
|
||||
* @param isUsingAdaptiveKeys generates keys from IV and ID, see getKey()
|
||||
* @param isUsingAdaptiveData generate encrypt-ajax data based on $("script[data-name='episode']")[0].dataset.value
|
||||
* */
|
||||
suspend fun extractVidstream(
|
||||
iframeUrl: String,
|
||||
mainApiName: String,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
iv: String?,
|
||||
secretKey: String?,
|
||||
secretDecryptKey: String?,
|
||||
// This could be removed, but i prefer it verbose
|
||||
isUsingAdaptiveKeys: Boolean,
|
||||
isUsingAdaptiveData: Boolean,
|
||||
// If you don't want to re-fetch the document
|
||||
iframeDocument: Document? = null
|
||||
) = safeApiCall {
|
||||
if ((iv == null || secretKey == null || secretDecryptKey == null) && !isUsingAdaptiveKeys)
|
||||
return@safeApiCall
|
||||
|
||||
val id = Regex("id=([^&]+)").find(iframeUrl)!!.value.removePrefix("id=")
|
||||
|
||||
var document: Document? = iframeDocument
|
||||
val foundIv =
|
||||
iv ?: (document ?: app.get(iframeUrl).document.also { document = it })
|
||||
.select("""div.wrapper[class*=container]""")
|
||||
.attr("class").split("-").lastOrNull() ?: return@safeApiCall
|
||||
val foundKey = secretKey ?: getKey(base64Decode(id) + foundIv) ?: return@safeApiCall
|
||||
val foundDecryptKey = secretDecryptKey ?: foundKey
|
||||
|
||||
val uri = URI(iframeUrl)
|
||||
val mainUrl = "https://" + uri.host
|
||||
|
||||
val encryptedId = cryptoHandler(id, foundIv, foundKey)
|
||||
val encryptRequestData = if (isUsingAdaptiveData) {
|
||||
// Only fetch the document if necessary
|
||||
val realDocument = document ?: app.get(iframeUrl).document
|
||||
val dataEncrypted =
|
||||
realDocument.select("script[data-name='episode']").attr("data-value")
|
||||
val headers = cryptoHandler(dataEncrypted, foundIv, foundKey, false)
|
||||
"id=$encryptedId&alias=$id&" + headers.substringAfter("&")
|
||||
} else {
|
||||
"id=$encryptedId&alias=$id"
|
||||
}
|
||||
|
||||
val jsonResponse =
|
||||
app.get(
|
||||
"$mainUrl/encrypt-ajax.php?$encryptRequestData",
|
||||
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
|
||||
)
|
||||
val dataencrypted =
|
||||
jsonResponse.text.substringAfter("{\"data\":\"").substringBefore("\"}")
|
||||
val datadecrypted = cryptoHandler(dataencrypted, foundIv, foundDecryptKey, false)
|
||||
val sources = AppUtils.parseJson<GogoSources>(datadecrypted)
|
||||
|
||||
suspend fun invokeGogoSource(
|
||||
source: GogoSource,
|
||||
sourceCallback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
if (source.file.contains(".m3u8")) {
|
||||
M3u8Helper.generateM3u8(
|
||||
mainApiName,
|
||||
source.file,
|
||||
mainUrl,
|
||||
headers = mapOf("Origin" to "https://plyr.link")
|
||||
).forEach(sourceCallback)
|
||||
} else {
|
||||
sourceCallback.invoke(
|
||||
ExtractorLink(
|
||||
mainApiName,
|
||||
mainApiName,
|
||||
source.file,
|
||||
mainUrl,
|
||||
getQualityFromName(source.label),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sources.source?.forEach {
|
||||
invokeGogoSource(it, callback)
|
||||
}
|
||||
sources.sourceBk?.forEach {
|
||||
invokeGogoSource(it, callback)
|
||||
}
|
||||
}
|
||||
|
||||
data class GogoSources(
|
||||
@JsonProperty("source") val source: List<GogoSource>?,
|
||||
@JsonProperty("sourceBk") val sourceBk: List<GogoSource>?,
|
||||
)
|
||||
|
||||
data class GogoSource(
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("label") val label: String?,
|
||||
@JsonProperty("type") val type: String?,
|
||||
@JsonProperty("default") val default: String? = null
|
||||
)
|
||||
}
|
|
@ -39,6 +39,7 @@ import com.lagradost.cloudstream3.CommonActivity.playerEventListener
|
|||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvidersIsActive
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
|
@ -108,8 +109,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
// get() = episodes.isNotEmpty()
|
||||
|
||||
// options for player
|
||||
protected var currentPrefQuality =
|
||||
Qualities.P2160.value // preferred maximum quality, used for ppl w bad internet or on cell
|
||||
|
||||
/**
|
||||
* Default profile 1
|
||||
* Decides how links should be sorted based on a priority system.
|
||||
* This will be set in runtime based on settings.
|
||||
**/
|
||||
protected var currentQualityProfile = 1
|
||||
// protected var currentPrefQuality =
|
||||
// Qualities.P2160.value // preferred maximum quality, used for ppl w bad internet or on cell
|
||||
protected var fastForwardTime = 10000L
|
||||
protected var androidTVInterfaceOffSeekTime = 10000L;
|
||||
protected var androidTVInterfaceOnSeekTime = 30000L;
|
||||
|
@ -1221,10 +1229,16 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
.toLong() * 1000L
|
||||
|
||||
androidTVInterfaceOffSeekTime =
|
||||
settingsManager.getInt(ctx.getString(R.string.android_tv_interface_off_seek_key), 10)
|
||||
settingsManager.getInt(
|
||||
ctx.getString(R.string.android_tv_interface_off_seek_key),
|
||||
10
|
||||
)
|
||||
.toLong() * 1000L
|
||||
androidTVInterfaceOnSeekTime =
|
||||
settingsManager.getInt(ctx.getString(R.string.android_tv_interface_on_seek_key), 10)
|
||||
settingsManager.getInt(
|
||||
ctx.getString(R.string.android_tv_interface_on_seek_key),
|
||||
10
|
||||
)
|
||||
.toLong() * 1000L
|
||||
|
||||
navigationBarHeight = ctx.getNavigationBarHeight()
|
||||
|
@ -1257,10 +1271,20 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
ctx.getString(R.string.double_tap_pause_enabled_key),
|
||||
false
|
||||
)
|
||||
currentPrefQuality = settingsManager.getInt(
|
||||
ctx.getString(if (ctx.isUsingMobileData()) R.string.quality_pref_mobile_data_key else R.string.quality_pref_key),
|
||||
currentPrefQuality
|
||||
)
|
||||
|
||||
val profiles = QualityDataHelper.getProfiles()
|
||||
val type = if (ctx.isUsingMobileData())
|
||||
QualityDataHelper.QualityProfileType.Data
|
||||
else QualityDataHelper.QualityProfileType.WiFi
|
||||
|
||||
currentQualityProfile =
|
||||
profiles.firstOrNull { it.type == type }?.id ?: profiles.firstOrNull()?.id
|
||||
?: currentQualityProfile
|
||||
|
||||
// currentPrefQuality = settingsManager.getInt(
|
||||
// ctx.getString(if (ctx.isUsingMobileData()) R.string.quality_pref_mobile_data_key else R.string.quality_pref_key),
|
||||
// currentPrefQuality
|
||||
// )
|
||||
// useSystemBrightness =
|
||||
// settingsManager.getBoolean(ctx.getString(R.string.use_system_brightness_key), false)
|
||||
}
|
||||
|
|
|
@ -32,6 +32,10 @@ import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
|
|||
import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage
|
||||
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.SourcePriority
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.SourcePriorityDialog
|
||||
import com.lagradost.cloudstream3.ui.result.*
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
|
||||
|
@ -57,6 +61,9 @@ import kotlinx.android.synthetic.main.player_select_source_and_subs.subtitles_cl
|
|||
import kotlinx.android.synthetic.main.player_select_tracks.*
|
||||
import kotlinx.coroutines.Job
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.math.abs
|
||||
|
||||
class GeneratorPlayer : FullScreenPlayer() {
|
||||
companion object {
|
||||
|
@ -188,17 +195,31 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
player.addTimeStamps(listOf()) // clear stamps
|
||||
}
|
||||
|
||||
private fun sortLinks(useQualitySettings: Boolean = true): List<Pair<ExtractorLink?, ExtractorUri?>> {
|
||||
return currentLinks.sortedBy {
|
||||
val (linkData, _) = it
|
||||
var quality = linkData?.quality ?: Qualities.Unknown.value
|
||||
private fun closestQuality(target: Int?): Qualities {
|
||||
if (target == null) return Qualities.Unknown
|
||||
return Qualities.values().minBy { abs(it.value - target) }
|
||||
}
|
||||
|
||||
// we set all qualities above current max as reverse
|
||||
if (useQualitySettings && quality > currentPrefQuality) {
|
||||
quality = currentPrefQuality - quality - 1
|
||||
}
|
||||
// negative because we want to sort highest quality first
|
||||
-(quality)
|
||||
private fun getLinkPriority(
|
||||
qualityProfile: Int,
|
||||
link: Pair<ExtractorLink?, ExtractorUri?>
|
||||
): Int {
|
||||
val (linkData, _) = link
|
||||
|
||||
val qualityPriority = QualityDataHelper.getQualityPriority(
|
||||
qualityProfile,
|
||||
closestQuality(linkData?.quality)
|
||||
)
|
||||
val sourcePriority =
|
||||
QualityDataHelper.getSourcePriority(qualityProfile, linkData?.name)
|
||||
|
||||
// negative because we want to sort highest quality first
|
||||
return qualityPriority + sourcePriority
|
||||
}
|
||||
|
||||
private fun sortLinks(qualityProfile: Int): List<Pair<ExtractorLink?, ExtractorUri?>> {
|
||||
return currentLinks.sortedBy {
|
||||
-getLinkPriority(qualityProfile, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -219,6 +240,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
meta.name = newMeta.headerName
|
||||
}
|
||||
|
||||
is ExtractorUri -> {
|
||||
if (newMeta.tvType?.isMovieType() == false) {
|
||||
meta.episode = newMeta.episode
|
||||
|
@ -584,33 +606,39 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
var sourceIndex = 0
|
||||
var startSource = 0
|
||||
var sortedUrls = emptyList<Pair<ExtractorLink?, ExtractorUri?>>()
|
||||
|
||||
val sortedUrls = sortLinks(useQualitySettings = false)
|
||||
if (sortedUrls.isEmpty()) {
|
||||
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.isGone = true
|
||||
} else {
|
||||
startSource = sortedUrls.indexOf(currentSelectedLink)
|
||||
sourceIndex = startSource
|
||||
fun refreshLinks(qualityProfile: Int) {
|
||||
sortedUrls = sortLinks(qualityProfile)
|
||||
if (sortedUrls.isEmpty()) {
|
||||
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.isGone =
|
||||
true
|
||||
} else {
|
||||
startSource = sortedUrls.indexOf(currentSelectedLink)
|
||||
sourceIndex = startSource
|
||||
|
||||
val sourcesArrayAdapter =
|
||||
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||
val sourcesArrayAdapter =
|
||||
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||
|
||||
sourcesArrayAdapter.addAll(sortedUrls.map { (link, uri) ->
|
||||
val name = link?.name ?: uri?.name ?: "NULL"
|
||||
"$name ${Qualities.getStringByInt(link?.quality)}"
|
||||
})
|
||||
sourcesArrayAdapter.addAll(sortedUrls.map { (link, uri) ->
|
||||
val name = link?.name ?: uri?.name ?: "NULL"
|
||||
"$name ${Qualities.getStringByInt(link?.quality)}"
|
||||
})
|
||||
|
||||
providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
providerList.adapter = sourcesArrayAdapter
|
||||
providerList.setSelection(sourceIndex)
|
||||
providerList.setItemChecked(sourceIndex, true)
|
||||
providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
providerList.adapter = sourcesArrayAdapter
|
||||
providerList.setSelection(sourceIndex)
|
||||
providerList.setItemChecked(sourceIndex, true)
|
||||
|
||||
providerList.setOnItemClickListener { _, _, which, _ ->
|
||||
sourceIndex = which
|
||||
providerList.setItemChecked(which, true)
|
||||
providerList.setOnItemClickListener { _, _, which, _ ->
|
||||
sourceIndex = which
|
||||
providerList.setItemChecked(which, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshLinks(currentQualityProfile)
|
||||
|
||||
sourceDialog.setOnDismissListener {
|
||||
if (shouldDismiss) dismiss()
|
||||
selectSourceDialog = null
|
||||
|
@ -650,6 +678,29 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
sourceDialog.dismissSafe(activity)
|
||||
}
|
||||
|
||||
fun setProfileName(profile: Int) {
|
||||
sourceDialog.source_settings_btt.setText(
|
||||
QualityDataHelper.getProfileName(
|
||||
profile
|
||||
)
|
||||
)
|
||||
}
|
||||
setProfileName(currentQualityProfile)
|
||||
|
||||
sourceDialog.profiles_click_settings.setOnClickListener {
|
||||
val activity = activity ?: return@setOnClickListener
|
||||
QualityProfileDialog(
|
||||
activity,
|
||||
R.style.AlertDialogCustomBlack,
|
||||
currentLinks.mapNotNull { it.first },
|
||||
currentQualityProfile
|
||||
) { profile ->
|
||||
currentQualityProfile = profile.id
|
||||
setProfileName(profile.id)
|
||||
refreshLinks(profile.id)
|
||||
}.show()
|
||||
}
|
||||
|
||||
sourceDialog.subtitles_encoding_format?.apply {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
|
||||
|
@ -848,7 +899,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
private fun startPlayer() {
|
||||
if (isActive) return // we don't want double load when you skip loading
|
||||
|
||||
val links = sortLinks()
|
||||
val links = sortLinks(currentQualityProfile)
|
||||
if (links.isEmpty()) {
|
||||
noLinksFound()
|
||||
return
|
||||
|
@ -869,12 +920,12 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
override fun hasNextMirror(): Boolean {
|
||||
val links = sortLinks()
|
||||
val links = sortLinks(currentQualityProfile)
|
||||
return links.isNotEmpty() && links.indexOf(currentSelectedLink) + 1 < links.size
|
||||
}
|
||||
|
||||
override fun nextMirror() {
|
||||
val links = sortLinks()
|
||||
val links = sortLinks(currentQualityProfile)
|
||||
if (links.isEmpty()) {
|
||||
noLinksFound()
|
||||
return
|
||||
|
@ -933,6 +984,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
is ResultEpisode -> {
|
||||
DataStoreHelper.removeLastWatched(newMeta.parentId)
|
||||
}
|
||||
|
||||
is ExtractorUri -> {
|
||||
DataStoreHelper.removeLastWatched(newMeta.parentId)
|
||||
}
|
||||
|
@ -949,6 +1001,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
isFromDownload = false
|
||||
)
|
||||
}
|
||||
|
||||
is ExtractorUri -> {
|
||||
DataStoreHelper.setLastWatched(
|
||||
resumeMeta.parentId,
|
||||
|
@ -1080,6 +1133,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
season = meta.season
|
||||
tvType = meta.tvType
|
||||
}
|
||||
|
||||
is ExtractorUri -> {
|
||||
headerName = meta.headerName
|
||||
subName = meta.name
|
||||
|
@ -1296,6 +1350,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
is Resource.Loading -> {
|
||||
startLoading()
|
||||
}
|
||||
|
||||
is Resource.Success -> {
|
||||
// provider returned false
|
||||
//if (it.value != true) {
|
||||
|
@ -1303,6 +1358,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
//}
|
||||
startPlayer()
|
||||
}
|
||||
|
||||
is Resource.Failure -> {
|
||||
showToast(activity, it.errorString, Toast.LENGTH_LONG)
|
||||
startPlayer()
|
||||
|
@ -1315,6 +1371,17 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val turnVisible = it.isNotEmpty()
|
||||
val wasGone = overlay_loading_skip_button?.isGone == true
|
||||
overlay_loading_skip_button?.isVisible = turnVisible
|
||||
|
||||
normalSafeApiCall {
|
||||
if (currentLinks.any { link ->
|
||||
getLinkPriority(currentQualityProfile, link) >=
|
||||
QualityDataHelper.AUTO_SKIP_PRIORITY
|
||||
}
|
||||
) {
|
||||
startPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
if (turnVisible && wasGone) {
|
||||
overlay_loading_skip_button?.requestFocus()
|
||||
}
|
||||
|
|
|
@ -156,18 +156,24 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
val currentSubs = mutableSetOf<SubtitleData>()
|
||||
|
||||
// clear old data
|
||||
_currentSubs.postValue(currentSubs)
|
||||
_currentLinks.postValue(currentLinks)
|
||||
_currentSubs.postValue(emptySet())
|
||||
_currentLinks.postValue(emptySet())
|
||||
|
||||
// load more data
|
||||
_loadingLinks.postValue(Resource.Loading())
|
||||
val loadingState = safeApiCall {
|
||||
generator?.generateLinks(clearCache = clearCache, isCasting = isCasting, {
|
||||
currentLinks.add(it)
|
||||
_currentLinks.postValue(currentLinks)
|
||||
// Clone to prevent ConcurrentModificationException
|
||||
normalSafeApiCall {
|
||||
// Extra normalSafeApiCall since .toSet() iterates.
|
||||
_currentLinks.postValue(currentLinks.toSet())
|
||||
}
|
||||
}, {
|
||||
currentSubs.add(it)
|
||||
// _currentSubs.postValue(currentSubs) // this causes ConcurrentModificationException, so fuck it
|
||||
normalSafeApiCall {
|
||||
_currentSubs.postValue(currentSubs.toSet())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import kotlinx.android.synthetic.main.player_prioritize_item.view.*
|
||||
|
||||
data class SourcePriority<T>(
|
||||
val data: T,
|
||||
val name: String,
|
||||
var priority: Int
|
||||
)
|
||||
|
||||
class PriorityAdapter<T>(override val items: MutableList<SourcePriority<T>>) :
|
||||
AppUtils.DiffAdapter<SourcePriority<T>>(items) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return PriorityViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is PriorityViewHolder -> holder.bind(items[position])
|
||||
}
|
||||
}
|
||||
|
||||
class PriorityViewHolder(
|
||||
itemView: View,
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
fun <T> bind(item: SourcePriority<T>) {
|
||||
val plusButton: ImageView = itemView.add_button
|
||||
val subtractButton: ImageView = itemView.subtract_button
|
||||
val priorityText: TextView = itemView.priority_text
|
||||
val priorityNumber: TextView = itemView.priority_number
|
||||
priorityText.text = item.name
|
||||
|
||||
fun updatePriority() {
|
||||
priorityNumber.text = item.priority.toString()
|
||||
}
|
||||
|
||||
updatePriority()
|
||||
plusButton.setOnClickListener {
|
||||
// If someone clicks til the integer limit then they deserve to crash.
|
||||
item.priority++
|
||||
updatePriority()
|
||||
}
|
||||
|
||||
subtractButton.setOnClickListener {
|
||||
item.priority--
|
||||
updatePriority()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Typeface
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.ui.result.UiImage
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_item.view.card_view
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_item.view.outline
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_item.view.profile_image_background
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_item.view.profile_text
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_item.view.text_is_mobile_data
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_item.view.text_is_wifi
|
||||
|
||||
class ProfilesAdapter(
|
||||
override val items: MutableList<QualityDataHelper.QualityProfile>,
|
||||
val usedProfile: Int,
|
||||
val clickCallback: (oldIndex: Int?, newIndex: Int) -> Unit,
|
||||
) :
|
||||
AppUtils.DiffAdapter<QualityDataHelper.QualityProfile>(
|
||||
items,
|
||||
comparison = { first: QualityDataHelper.QualityProfile, second: QualityDataHelper.QualityProfile ->
|
||||
first.id == second.id
|
||||
}) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return ProfilesViewHolder(
|
||||
LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.player_quality_profile_item, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is ProfilesViewHolder -> holder.bind(items[position], position)
|
||||
}
|
||||
}
|
||||
|
||||
private var currentItem: Pair<Int, QualityDataHelper.QualityProfile>? = null
|
||||
|
||||
fun getCurrentProfile(): QualityDataHelper.QualityProfile? {
|
||||
return currentItem?.second
|
||||
}
|
||||
|
||||
inner class ProfilesViewHolder(
|
||||
itemView: View,
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
private val art = listOf(
|
||||
R.drawable.profile_bg_teal,
|
||||
R.drawable.profile_bg_blue,
|
||||
R.drawable.profile_bg_dark_blue,
|
||||
R.drawable.profile_bg_purple,
|
||||
R.drawable.profile_bg_pink,
|
||||
R.drawable.profile_bg_red,
|
||||
R.drawable.profile_bg_orange,
|
||||
)
|
||||
|
||||
fun bind(item: QualityDataHelper.QualityProfile, index: Int) {
|
||||
val priorityText: TextView = itemView.profile_text
|
||||
val profileBg: ImageView = itemView.profile_image_background
|
||||
val wifiText: TextView = itemView.text_is_wifi
|
||||
val dataText: TextView = itemView.text_is_mobile_data
|
||||
val outline: View = itemView.outline
|
||||
val cardView: View = itemView.card_view
|
||||
|
||||
priorityText.text = item.name.asString(itemView.context)
|
||||
dataText.isVisible = item.type == QualityDataHelper.QualityProfileType.Data
|
||||
wifiText.isVisible = item.type == QualityDataHelper.QualityProfileType.WiFi
|
||||
|
||||
fun setCurrentItem() {
|
||||
val prevIndex = currentItem?.first
|
||||
// Prevent UI bug when re-selecting the item quickly
|
||||
if (prevIndex == index) {
|
||||
return
|
||||
}
|
||||
currentItem = index to item
|
||||
clickCallback.invoke(prevIndex, index)
|
||||
}
|
||||
|
||||
outline.isVisible = currentItem?.second?.id == item.id
|
||||
|
||||
profileBg.setImage(UiImage.Drawable(art[index % art.size]), null, false) { palette ->
|
||||
val color = palette.getDarkVibrantColor(
|
||||
ContextCompat.getColor(
|
||||
itemView.context,
|
||||
R.color.dubColorBg
|
||||
)
|
||||
)
|
||||
wifiText.backgroundTintList = ColorStateList.valueOf(color)
|
||||
dataText.backgroundTintList = ColorStateList.valueOf(color)
|
||||
}
|
||||
|
||||
val textStyle =
|
||||
if (item.id == usedProfile) {
|
||||
Typeface.BOLD
|
||||
} else {
|
||||
Typeface.NORMAL
|
||||
}
|
||||
|
||||
priorityText.setTypeface(null, textStyle)
|
||||
|
||||
cardView.setOnClickListener {
|
||||
setCurrentItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||
import com.lagradost.cloudstream3.ui.result.UiText
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
||||
object QualityDataHelper {
|
||||
private const val VIDEO_SOURCE_PRIORITY = "video_source_priority"
|
||||
private const val VIDEO_PROFILE_NAME = "video_profile_name"
|
||||
private const val VIDEO_QUALITY_PRIORITY = "video_quality_priority"
|
||||
private const val VIDEO_PROFILE_TYPE = "video_profile_type"
|
||||
private const val DEFAULT_SOURCE_PRIORITY = 1
|
||||
/**
|
||||
* Automatically skip loading links once this priority is reached
|
||||
**/
|
||||
const val AUTO_SKIP_PRIORITY = 10
|
||||
|
||||
/**
|
||||
* Must be higher than amount of QualityProfileTypes
|
||||
**/
|
||||
private const val PROFILE_COUNT = 7
|
||||
|
||||
/**
|
||||
* Unique guarantees that there will always be one of this type in the profile list.
|
||||
**/
|
||||
enum class QualityProfileType(@StringRes val stringRes: Int, val unique: Boolean) {
|
||||
None(R.string.none, false),
|
||||
WiFi(R.string.wifi, true),
|
||||
Data(R.string.mobile_data, true)
|
||||
}
|
||||
|
||||
data class QualityProfile(
|
||||
val name: UiText,
|
||||
val id: Int,
|
||||
val type: QualityProfileType
|
||||
)
|
||||
|
||||
fun getSourcePriority(profile: Int, name: String?): Int {
|
||||
if (name == null) return DEFAULT_SOURCE_PRIORITY
|
||||
return getKey(
|
||||
"$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile",
|
||||
name,
|
||||
DEFAULT_SOURCE_PRIORITY
|
||||
) ?: DEFAULT_SOURCE_PRIORITY
|
||||
}
|
||||
|
||||
fun setSourcePriority(profile: Int, name: String, priority: Int) {
|
||||
setKey("$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile", name, priority)
|
||||
}
|
||||
|
||||
fun setProfileName(profile: Int, name: String?) {
|
||||
val path = "$currentAccount/$VIDEO_PROFILE_NAME/$profile"
|
||||
if (name == null) {
|
||||
removeKey(path)
|
||||
} else {
|
||||
setKey(path, name.trim())
|
||||
}
|
||||
}
|
||||
|
||||
fun getProfileName(profile: Int): UiText {
|
||||
return getKey<String>("$currentAccount/$VIDEO_PROFILE_NAME/$profile")?.let { txt(it) }
|
||||
?: txt(R.string.profile_number, profile)
|
||||
}
|
||||
|
||||
fun getQualityPriority(profile: Int, quality: Qualities): Int {
|
||||
return getKey(
|
||||
"$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile",
|
||||
quality.value.toString(),
|
||||
quality.defaultPriority
|
||||
) ?: quality.defaultPriority
|
||||
}
|
||||
|
||||
fun setQualityPriority(profile: Int, quality: Qualities, priority: Int) {
|
||||
setKey(
|
||||
"$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile",
|
||||
quality.value.toString(),
|
||||
priority
|
||||
)
|
||||
}
|
||||
|
||||
fun getQualityProfileType(profile: Int): QualityProfileType {
|
||||
return getKey("$currentAccount/$VIDEO_PROFILE_TYPE/$profile") ?: QualityProfileType.None
|
||||
}
|
||||
|
||||
fun setQualityProfileType(profile: Int, type: QualityProfileType?) {
|
||||
val path = "$currentAccount/$VIDEO_PROFILE_TYPE/$profile"
|
||||
if (type == QualityProfileType.None) {
|
||||
removeKey(path)
|
||||
} else {
|
||||
setKey(path, type)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all quality profiles, always includes one profile with WiFi and Data
|
||||
* Must under all circumstances at least return one profile
|
||||
**/
|
||||
fun getProfiles(): List<QualityProfile> {
|
||||
val availableTypes = QualityProfileType.values().toMutableList()
|
||||
val profiles = (1..PROFILE_COUNT).map { profileNumber ->
|
||||
// Get the real type
|
||||
val type = getQualityProfileType(profileNumber)
|
||||
|
||||
// This makes it impossible to get more than one of each type
|
||||
// Duplicates will be turned to None
|
||||
val uniqueType = if (type.unique && !availableTypes.remove(type)) {
|
||||
QualityProfileType.None
|
||||
} else {
|
||||
type
|
||||
}
|
||||
|
||||
QualityProfile(
|
||||
getProfileName(profileNumber),
|
||||
profileNumber,
|
||||
uniqueType
|
||||
)
|
||||
}.toMutableList()
|
||||
|
||||
/**
|
||||
* If no profile of this type exists: insert it on the earliest profile with None type
|
||||
**/
|
||||
fun insertType(
|
||||
list: MutableList<QualityProfile>,
|
||||
type: QualityProfileType
|
||||
) {
|
||||
if (list.any { it.type == type }) return
|
||||
val index =
|
||||
list.indexOfFirst { it.type == QualityProfileType.None }
|
||||
list.getOrNull(index)?.copy(type = type)
|
||||
?.let { fixed ->
|
||||
list.set(index, fixed)
|
||||
}
|
||||
}
|
||||
|
||||
QualityProfileType.values().forEach {
|
||||
if (it.unique) insertType(profiles, it)
|
||||
}
|
||||
|
||||
debugAssert({
|
||||
!QualityProfileType.values().all { type ->
|
||||
!type.unique || profiles.any { it.type == type }
|
||||
}
|
||||
}, { "All unique quality types do not exist" })
|
||||
|
||||
debugAssert({
|
||||
profiles.isEmpty()
|
||||
}, { "No profiles!" })
|
||||
|
||||
return profiles
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||
|
||||
import android.app.Dialog
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfileName
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfiles
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_dialog.*
|
||||
|
||||
class QualityProfileDialog(
|
||||
val activity: FragmentActivity,
|
||||
@StyleRes val themeRes: Int,
|
||||
private val links: List<ExtractorLink>,
|
||||
private val usedProfile: Int,
|
||||
private val profileSelectionCallback: (QualityDataHelper.QualityProfile) -> Unit
|
||||
) : Dialog(activity, themeRes) {
|
||||
override fun show() {
|
||||
setContentView(R.layout.player_quality_profile_dialog)
|
||||
val profilesRecyclerView: RecyclerView = profiles_recyclerview
|
||||
val useBtt: View = use_btt
|
||||
val editBtt: View = edit_btt
|
||||
val cancelBtt: View = cancel_btt
|
||||
val defaultBtt: View = set_default_btt
|
||||
val currentProfileText: TextView = currently_selected_profile_text
|
||||
val selectedItemActionsHolder: View = selected_item_holder
|
||||
|
||||
fun getCurrentProfile(): QualityDataHelper.QualityProfile? {
|
||||
return (profilesRecyclerView.adapter as? ProfilesAdapter)?.getCurrentProfile()
|
||||
}
|
||||
|
||||
fun refreshProfiles() {
|
||||
currentProfileText.text = getProfileName(usedProfile).asString(context)
|
||||
(profilesRecyclerView.adapter as? ProfilesAdapter)?.updateList(getProfiles())
|
||||
}
|
||||
|
||||
profilesRecyclerView.adapter = ProfilesAdapter(
|
||||
mutableListOf(),
|
||||
usedProfile,
|
||||
) { oldIndex: Int?, newIndex: Int ->
|
||||
profilesRecyclerView.adapter?.notifyItemChanged(newIndex)
|
||||
selectedItemActionsHolder.alpha = 1f
|
||||
if (oldIndex != null) {
|
||||
profilesRecyclerView.adapter?.notifyItemChanged(oldIndex)
|
||||
}
|
||||
}
|
||||
|
||||
refreshProfiles()
|
||||
|
||||
editBtt.setOnClickListener {
|
||||
getCurrentProfile()?.let { profile ->
|
||||
SourcePriorityDialog(context, themeRes, links, profile) {
|
||||
refreshProfiles()
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
defaultBtt.setOnClickListener {
|
||||
val currentProfile = getCurrentProfile() ?: return@setOnClickListener
|
||||
val choices = QualityDataHelper.QualityProfileType.values()
|
||||
.filter { it != QualityDataHelper.QualityProfileType.None }
|
||||
val choiceNames = choices.map { txt(it.stringRes).asString(context) }
|
||||
|
||||
activity.showBottomDialog(
|
||||
choiceNames,
|
||||
choices.indexOf(currentProfile.type),
|
||||
txt(R.string.set_default).asString(context),
|
||||
false,
|
||||
{},
|
||||
{ index ->
|
||||
val pickedChoice = choices.getOrNull(index) ?: return@showBottomDialog
|
||||
// Remove previous picks
|
||||
if (pickedChoice.unique) {
|
||||
getProfiles().filter { it.type == pickedChoice }.forEach {
|
||||
QualityDataHelper.setQualityProfileType(it.id, null)
|
||||
}
|
||||
}
|
||||
|
||||
QualityDataHelper.setQualityProfileType(currentProfile.id, pickedChoice)
|
||||
refreshProfiles()
|
||||
})
|
||||
}
|
||||
|
||||
cancelBtt.setOnClickListener {
|
||||
this.dismissSafe()
|
||||
}
|
||||
|
||||
useBtt.setOnClickListener {
|
||||
getCurrentProfile()?.let {
|
||||
profileSelectionCallback.invoke(it)
|
||||
this.dismissSafe()
|
||||
}
|
||||
}
|
||||
|
||||
super.show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.work.impl.constraints.controllers.ConstraintController
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import kotlinx.android.synthetic.main.player_select_source_priority.*
|
||||
|
||||
class SourcePriorityDialog(
|
||||
ctx: Context,
|
||||
@StyleRes themeRes: Int,
|
||||
val links: List<ExtractorLink>,
|
||||
private val profile: QualityDataHelper.QualityProfile,
|
||||
/**
|
||||
* Notify that the profile overview should be updated, for example if the name has been updated
|
||||
* Should not be called excessively.
|
||||
**/
|
||||
private val updatedCallback: () -> Unit
|
||||
) : Dialog(ctx, themeRes) {
|
||||
override fun show() {
|
||||
setContentView(R.layout.player_select_source_priority)
|
||||
val sourcesRecyclerView: RecyclerView = sort_sources
|
||||
val qualitiesRecyclerView: RecyclerView = sort_qualities
|
||||
val profileText: EditText = profile_text_editable
|
||||
val saveBtt: View = save_btt
|
||||
val exitBtt: View = close_btt
|
||||
val helpBtt: View = help_btt
|
||||
|
||||
profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context))
|
||||
profileText.hint = txt(R.string.profile_number, profile.id).asString(context)
|
||||
|
||||
sourcesRecyclerView.adapter = PriorityAdapter(
|
||||
links.map { link ->
|
||||
SourcePriority(
|
||||
null,
|
||||
link.source,
|
||||
QualityDataHelper.getSourcePriority(profile.id, link.source)
|
||||
)
|
||||
}.distinctBy { it.name }.sortedBy { -it.priority }.toMutableList()
|
||||
)
|
||||
|
||||
qualitiesRecyclerView.adapter = PriorityAdapter(
|
||||
Qualities.values().mapNotNull {
|
||||
SourcePriority(
|
||||
it,
|
||||
Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null },
|
||||
QualityDataHelper.getQualityPriority(profile.id, it)
|
||||
)
|
||||
}.sortedBy { -it.priority }.toMutableList()
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST") // We know the types
|
||||
saveBtt.setOnClickListener {
|
||||
val qualityAdapter = qualitiesRecyclerView.adapter as? PriorityAdapter<Qualities>
|
||||
val sourcesAdapter = sourcesRecyclerView.adapter as? PriorityAdapter<Nothing>
|
||||
|
||||
val qualities = qualityAdapter?.items ?: emptyList()
|
||||
val sources = sourcesAdapter?.items ?: emptyList()
|
||||
|
||||
qualities.forEach {
|
||||
val data = it.data as? Qualities ?: return@forEach
|
||||
QualityDataHelper.setQualityPriority(profile.id, data, it.priority)
|
||||
}
|
||||
|
||||
sources.forEach {
|
||||
QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority)
|
||||
}
|
||||
|
||||
qualityAdapter?.updateList(qualities.sortedBy { -it.priority })
|
||||
sourcesAdapter?.updateList(sources.sortedBy { -it.priority })
|
||||
|
||||
val savedProfileName = profileText.text.toString()
|
||||
if (savedProfileName.isBlank()) {
|
||||
QualityDataHelper.setProfileName(profile.id, null)
|
||||
} else {
|
||||
QualityDataHelper.setProfileName(profile.id, savedProfileName)
|
||||
}
|
||||
updatedCallback.invoke()
|
||||
}
|
||||
|
||||
exitBtt.setOnClickListener {
|
||||
this.dismissSafe()
|
||||
}
|
||||
|
||||
helpBtt.setOnClickListener {
|
||||
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
|
||||
setMessage(R.string.quality_profile_help)
|
||||
}.show()
|
||||
}
|
||||
|
||||
super.show()
|
||||
}
|
||||
}
|
|
@ -72,7 +72,7 @@ sealed class UiImage {
|
|||
|
||||
fun ImageView?.setImage(value: UiImage?, fadeIn: Boolean = true) {
|
||||
when (value) {
|
||||
is UiImage.Image -> setImageImage(value,fadeIn)
|
||||
is UiImage.Image -> setImageImage(value, fadeIn)
|
||||
is UiImage.Drawable -> setImageDrawable(value)
|
||||
null -> {
|
||||
this?.isVisible = false
|
||||
|
@ -88,7 +88,7 @@ fun ImageView?.setImageImage(value: UiImage.Image, fadeIn: Boolean = true) {
|
|||
fun ImageView?.setImageDrawable(value: UiImage.Drawable) {
|
||||
if (this == null) return
|
||||
this.isVisible = true
|
||||
setImageResource(value.resId)
|
||||
this.setImage(UiImage.Drawable(value.resId))
|
||||
}
|
||||
|
||||
@JvmName("imgNull")
|
||||
|
|
|
@ -103,7 +103,7 @@ val appLanguages = arrayListOf(
|
|||
Triple("", "اردو", "ur"),
|
||||
Triple("", "Tiếng Việt", "vi"),
|
||||
Triple("", "中文", "zh"),
|
||||
Triple("\uD83C\uDDF9\uD83C\uDDFC", "文言", "zh-rTW"),
|
||||
Triple("\uD83C\uDDF9\uD83C\uDDFC", "正體中文(臺灣)", "zh-rTW"),
|
||||
/* end language list */
|
||||
).sortedBy { it.second.lowercase() } //ye, we go alphabetical, so ppl don't put their lang on top
|
||||
|
||||
|
|
|
@ -117,7 +117,7 @@ object DataStoreHelper {
|
|||
/**
|
||||
* A datastore wide account for future implementations of a multiple account system
|
||||
**/
|
||||
private var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION
|
||||
var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION
|
||||
|
||||
fun getAllWatchStateIds(): List<Int>? {
|
||||
val folder = "$currentAccount/$RESULT_WATCH_STATE"
|
||||
|
|
|
@ -114,16 +114,16 @@ data class ExtractorSubtitleLink(
|
|||
*/
|
||||
val schemaStripRegex = Regex("""^(https:|)//(www\.|)""")
|
||||
|
||||
enum class Qualities(var value: Int) {
|
||||
Unknown(400),
|
||||
P144(144), // 144p
|
||||
P240(240), // 240p
|
||||
P360(360), // 360p
|
||||
P480(480), // 480p
|
||||
P720(720), // 720p
|
||||
P1080(1080), // 1080p
|
||||
P1440(1440), // 1440p
|
||||
P2160(2160); // 4k or 2160p
|
||||
enum class Qualities(var value: Int, val defaultPriority: Int) {
|
||||
Unknown(400, 4),
|
||||
P144(144, 0), // 144p
|
||||
P240(240, 2), // 240p
|
||||
P360(360, 3), // 360p
|
||||
P480(480, 4), // 480p
|
||||
P720(720, 5), // 720p
|
||||
P1080(1080, 6), // 1080p
|
||||
P1440(1440, 7), // 1440p
|
||||
P2160(2160, 8); // 4k or 2160p
|
||||
|
||||
companion object {
|
||||
fun getStringByInt(qual: Int?): String {
|
||||
|
@ -135,6 +135,14 @@ enum class Qualities(var value: Int) {
|
|||
else -> "${qual}p"
|
||||
}
|
||||
}
|
||||
fun getStringByIntFull(quality: Int): String {
|
||||
return when (quality) {
|
||||
0 -> "Auto"
|
||||
Unknown.value -> "Unknown"
|
||||
P2160.value -> "4K"
|
||||
else -> "${quality}p"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,6 +244,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
XStreamCdn(),
|
||||
|
||||
StreamSB(),
|
||||
Vidgomunimesb(),
|
||||
StreamSB1(),
|
||||
StreamSB2(),
|
||||
StreamSB3(),
|
||||
|
@ -275,7 +284,6 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
Uqload2(),
|
||||
Evoload(),
|
||||
Evoload1(),
|
||||
VoeExtractor(),
|
||||
UpstreamExtractor(),
|
||||
|
||||
Tomatomatela(),
|
||||
|
@ -342,6 +350,24 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
DesuOdvip(),
|
||||
DesuDrive(),
|
||||
|
||||
Chillx(),
|
||||
Watchx(),
|
||||
Bestx(),
|
||||
Keephealth(),
|
||||
Sbnet(),
|
||||
Sbasian(),
|
||||
Sblongvu(),
|
||||
Fembed9hd(),
|
||||
StreamM4u(),
|
||||
Krakenfiles(),
|
||||
Gofile(),
|
||||
Vicloud(),
|
||||
Uservideo(),
|
||||
|
||||
Movhide(),
|
||||
StreamhideCom(),
|
||||
FileMoonIn(),
|
||||
Moviesm4u(),
|
||||
Filesim(),
|
||||
FileMoon(),
|
||||
FileMoonSx(),
|
||||
|
@ -357,6 +383,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
Vidmoly(),
|
||||
Vidmolyme(),
|
||||
Voe(),
|
||||
Tubeless(),
|
||||
Moviehab(),
|
||||
MoviehabNet(),
|
||||
Jeniusplay(),
|
||||
|
|
|
@ -250,17 +250,6 @@ object SingleSelectionHelper {
|
|||
)
|
||||
}
|
||||
|
||||
fun showBottomDialog(
|
||||
items: List<String>,
|
||||
selectedIndex: Int,
|
||||
name: String,
|
||||
showApply: Boolean,
|
||||
dismissCallback: () -> Unit,
|
||||
callback: (Int) -> Unit,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
/** Only for a low amount of items */
|
||||
fun Activity?.showBottomDialog(
|
||||
items: List<String>,
|
||||
|
|
|
@ -44,12 +44,13 @@ import com.bumptech.glide.load.engine.GlideException
|
|||
import com.bumptech.glide.load.model.GlideUrl
|
||||
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.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.result.UiImage
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.utils.GlideOptions.bitmapTransform
|
||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
@ -188,11 +189,30 @@ object UIHelper {
|
|||
fadeIn: Boolean = true,
|
||||
colorCallback: ((Palette) -> Unit)? = null
|
||||
): Boolean {
|
||||
if (this == null || url.isNullOrBlank()) return false
|
||||
if (url.isNullOrBlank()) return false
|
||||
this.setImage(UiImage.Image(url, headers, errorImageDrawable), errorImageDrawable, fadeIn, colorCallback)
|
||||
return true
|
||||
}
|
||||
|
||||
fun ImageView?.setImage(
|
||||
uiImage: UiImage?,
|
||||
@DrawableRes
|
||||
errorImageDrawable: Int? = null,
|
||||
fadeIn: Boolean = true,
|
||||
colorCallback: ((Palette) -> Unit)? = null
|
||||
): Boolean {
|
||||
if (this == null || uiImage == null) return false
|
||||
|
||||
val (glideImage, identifier) =
|
||||
(uiImage as? UiImage.Drawable)?.resId?.let {
|
||||
it to it.toString()
|
||||
} ?: (uiImage as? UiImage.Image)?.let { image ->
|
||||
GlideUrl(image.url) { image.headers ?: emptyMap() } to image.url
|
||||
} ?: return false
|
||||
|
||||
return try {
|
||||
val builder = GlideApp.with(this)
|
||||
.load(GlideUrl(url) { headers ?: emptyMap() })
|
||||
.load(glideImage)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL).let { req ->
|
||||
if (fadeIn)
|
||||
|
@ -211,7 +231,13 @@ object UIHelper {
|
|||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
resource?.toBitmapOrNull()
|
||||
?.let { bitmap -> createPaletteAsync(url, bitmap, colorCallback) }
|
||||
?.let { bitmap ->
|
||||
createPaletteAsync(
|
||||
identifier,
|
||||
bitmap,
|
||||
colorCallback
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
5
app/src/main/res/drawable/baseline_help_outline_24.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="?attr/white" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z"/>
|
||||
</vector>
|
|
@ -3,7 +3,7 @@
|
|||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
android:tint="?attr/white">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,13H5v-2h14v2z"/>
|
||||
|
|
BIN
app/src/main/res/drawable/profile_bg_blue.jpg
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
app/src/main/res/drawable/profile_bg_dark_blue.jpg
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
app/src/main/res/drawable/profile_bg_orange.jpg
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
app/src/main/res/drawable/profile_bg_pink.jpg
Normal file
After Width: | Height: | Size: 110 KiB |
BIN
app/src/main/res/drawable/profile_bg_purple.jpg
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
app/src/main/res/drawable/profile_bg_red.jpg
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
app/src/main/res/drawable/profile_bg_teal.jpg
Normal file
After Width: | Height: | Size: 118 KiB |
48
app/src/main/res/layout/player_prioritize_item.xml
Normal file
|
@ -0,0 +1,48 @@
|
|||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="15dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/priority_text"
|
||||
style="@style/NoCheckLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_toStartOf="@+id/subtract_button"
|
||||
tools:text="hello" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/subtract_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/priority_number"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/baseline_remove_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/priority_number"
|
||||
style="@style/NoCheckLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/add_button"
|
||||
android:gravity="center"
|
||||
android:minWidth="50dp"
|
||||
android:textColor="?attr/textColor"
|
||||
tools:text="1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/add_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/ic_baseline_add_24" />
|
||||
|
||||
</RelativeLayout>
|
105
app/src/main/res/layout/player_quality_profile_dialog.xml
Normal file
|
@ -0,0 +1,105 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/profile_text_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:paddingBottom="10dp"
|
||||
android:text="@string/profiles"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/currently_selected_profile_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="15sp"
|
||||
tools:text="Profile 1" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/profiles_recyclerview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/profile_button_bar"
|
||||
android:layout_below="@+id/profile_text_bar"
|
||||
android:orientation="horizontal"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:itemCount="4"
|
||||
tools:listitem="@layout/player_quality_profile_item" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/profile_button_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="10dp"
|
||||
android:animateLayoutChanges="true"
|
||||
android:gravity="end|bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/selected_item_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:alpha="0.5">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/edit_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="@string/edit" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/set_default_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="@string/set_default" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/use_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="@string/use" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel_btt"
|
||||
style="@style/BlackButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="@string/sort_cancel" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</RelativeLayout>
|
66
app/src/main/res/layout/player_quality_profile_item.xml
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:focusable="false">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/card_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintDimensionRatio="1"
|
||||
android:layout_marginStart="10dp"
|
||||
android:animateLayoutChanges="true"
|
||||
android:backgroundTint="?attr/primaryGrayBackground"
|
||||
android:foreground="?attr/selectableItemBackgroundBorderless"
|
||||
app:cardCornerRadius="@dimen/rounded_image_radius"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.4"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/profile_image_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0.4"
|
||||
android:contentDescription="@string/profile_background_des"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<View
|
||||
android:id="@+id/outline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/outline"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/profile_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:padding="10dp"
|
||||
android:textSize="16sp"
|
||||
tools:text="@string/mobile_data" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_is_wifi"
|
||||
style="@style/DubButton"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/wifi"
|
||||
android:textColor="@color/textColor" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_is_mobile_data"
|
||||
style="@style/DubButton"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/mobile_data"
|
||||
android:textColor="@color/textColor" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -20,19 +21,44 @@
|
|||
android:layout_weight="50"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
<LinearLayout
|
||||
android:id="@+id/profiles_click_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:paddingBottom="10dp"
|
||||
android:text="@string/pick_source"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
android:background="@drawable/outline_drawable_less"
|
||||
android:foreground="?attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:paddingBottom="10dp"
|
||||
android:text="@string/pick_source"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/source_settings_btt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:drawablePadding="10dp"
|
||||
android:gravity="center"
|
||||
android:minWidth="140dp"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="15sp"
|
||||
app:drawableEndCompat="@drawable/ic_outline_settings_24"
|
||||
tools:text="@string/profile_number" />
|
||||
</LinearLayout>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/sort_providers"
|
||||
|
|
179
app/src/main/res/layout/player_select_source_priority.xml
Normal file
|
@ -0,0 +1,179 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@null"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/sort_sources_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="50"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:paddingBottom="10dp"
|
||||
android:text="@string/pick_source"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/sort_sources"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:listSelector="@drawable/outline_drawable_less"
|
||||
android:nextFocusLeft="@id/sort_subtitles"
|
||||
android:nextFocusRight="@id/apply_btt"
|
||||
android:requiresFadingEdge="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:layout_height="100dp"
|
||||
tools:listitem="@layout/player_prioritize_item" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/sort_subtitles_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"
|
||||
-->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/subtitles_click_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:paddingBottom="10dp"
|
||||
android:text="@string/qualities"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/help_btt"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/baseline_help_outline_24"
|
||||
android:contentDescription="@string/help" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:contentDescription="@string/home_change_provider_img_des"
|
||||
android:src="@drawable/ic_outline_settings_24"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/sort_qualities"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:listSelector="@drawable/outline_drawable_less"
|
||||
android:nextFocusLeft="@id/sort_providers"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:requiresFadingEdge="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:layout_height="200dp"
|
||||
tools:listfooter="@layout/sort_bottom_footer_add_choice"
|
||||
tools:listitem="@layout/player_prioritize_item" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/apply_btt_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginTop="-60dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/profile_text_editable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:inputType="text"
|
||||
android:maxLength="32"
|
||||
android:layout_marginHorizontal="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="@string/profile_number"
|
||||
android:autofillHints="username" />
|
||||
<!-- <ImageView-->
|
||||
<!-- android:layout_width="50dp"-->
|
||||
<!-- android:layout_height="50dp"-->
|
||||
<!-- android:padding="10dp"-->
|
||||
<!-- android:layout_gravity="center"-->
|
||||
<!-- android:src="@drawable/outline_edit_24" />-->
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
|
||||
</Space>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/save_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/sort_save" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/close_btt"
|
||||
style="@style/BlackButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/sort_close" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
<string formatted="true" name="app_dub_sub_episode_text_format">%s еп. %d</string>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- KEYS DON'T TRANSLATE -->
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- KEYS DON'T TRANSLATE -->
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<string name="app_name">CloudStream</string>
|
||||
<string name="title_home">Αρχική</string>
|
||||
|
|
|
@ -33,4 +33,16 @@
|
|||
<string name="next_episode_time_hour_format" formatted="true">%dساعت %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="free_storage">آزاد</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="anime">انیمه</string>
|
||||
<string name="cartoons">کارتونها</string>
|
||||
<string name="used_storage">استفاده شده</string>
|
||||
<string name="app_storage">برنامه</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<string name="app_name">CloudStream</string>
|
||||
<string name="title_home">Accueil</string>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
|
||||
<string name="player_speed_text_format" formatted="true">रफ्तार (%.2fx)</string>
|
||||
|
@ -147,4 +146,13 @@
|
|||
<string name="next_episode_time_hour_format" formatted="true">%dh %dm</string>
|
||||
<string name="next_episode_time_min_format" formatted="true">%dm</string>
|
||||
<string name="result_poster_img_des">विज्ञापन</string>
|
||||
<string name="home_next_random_img_des">अगला रैंडम</string>
|
||||
<string name="go_back_img_des">वापस जाओ</string>
|
||||
<string name="search_poster_img_des">पोस्टर</string>
|
||||
<string name="preview_background_img_des">पृष्ठभूमि का पूर्वावलोकन करें</string>
|
||||
<string name="home_change_provider_img_des">प्रदाता बदलें</string>
|
||||
<string name="cast_format" formatted="true">Cast: %s</string>
|
||||
<string name="home_main_poster_img_des">मुख्य पोस्टर</string>
|
||||
<string name="episode_poster_img_des">एपिसोड का पोस्टर</string>
|
||||
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
<string name="extra_info_format" formatted="true" translatable="false">%d %s | %s</string>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>
|
||||
|
|
|
@ -198,4 +198,7 @@
|
|||
\nSky UK Limitedによる無脳なDMCAテイクダウンのため🤮、アプリ内でリポジトリサイトをリンクすることができません。
|
||||
\n
|
||||
\n私たちのDiscordに参加するか、オンラインで検索してください。</string>
|
||||
<string name="preview_background_img_des">バックグラウンドをプレビュー</string>
|
||||
<string name="play_livestream_button">ライブストリームの再生</string>
|
||||
<string name="home_change_provider_img_des">プロバイダーの変更</string>
|
||||
</resources>
|
||||
|
|
|
@ -125,4 +125,11 @@
|
|||
<string name="download_started">ಡೌನ್ಲೋಡ್ ಪ್ರಾರಂಭವಾಗಿದೆ</string>
|
||||
<string name="download_canceled">ಡೌನ್ಲೋಡ್ ರದ್ದುಗೊಳಿಸಲಾಗಿದೆ</string>
|
||||
<string name="home_next_random_img_des">ಮುಂದಿನ ರಾಂಡಮ್</string>
|
||||
<string name="swipe_to_seek_settings">ಮುಂದಕ್ಕೆ ಹೋಗಲು ಸ್ವೈಪ್ ಮಾಡಿ</string>
|
||||
<string name="swipe_to_seek_settings_des">ವೀಡಿಯೊದಲ್ಲಿ ನಿಮ್ಮ ಸ್ಥಾನವನ್ನು ನಿಯಂತ್ರಿಸಲು ಅಕ್ಕಪಕ್ಕಕ್ಕೆ ಸ್ವೈಪ್ ಮಾಡಿ</string>
|
||||
<string name="autoplay_next_settings">ಮುಂದಿನ ಸಂಚಿಕೆಯನ್ನು ಆಟೋ ಪ್ಲೇ ಮಾಡಿ</string>
|
||||
<string name="double_tap_to_seek_settings">ಮುಂದೂಡಲು ಅಥವಾ ಇಂದೂಡಲು ಎರಡು ಬಾರಿ ಟ್ಯಾಪ್ ಮಾಡಿ</string>
|
||||
<string name="swipe_to_change_settings_des">Brightness ಅಥವಾ volume ಬದಲಾಯಿಸಲು ಎಡ ಅಥವಾ ಬಲಭಾಗದಲ್ಲಿ ಮೇಲಕ್ಕೆ ಅಥವಾ ಕೆಳಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ</string>
|
||||
<string name="autoplay_next_settings_des">ಈಗಿನ ಎಪಿಸೋಡ್ ಮುಗಿದಾಗ ಮುಂದಿನ ಎಪಿಸೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ</string>
|
||||
<string name="swipe_to_change_settings">ಸೆಟ್ಟಿಂಗ್ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸ್ವೈಪ್ ಮಾಡಿ</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
|
||||
<string name="player_speed_text_format" formatted="true">Брзина (%.2fx)</string>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
|
||||
<string name="player_speed_text_format" formatted="true">വേഗം (%.2fx)</string>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>
|
||||
|
|
|
@ -1,2 +1,149 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources/>
|
||||
<resources>
|
||||
<string name="episode_more_options_des">ଅଧିକ ଵିକଳ୍ପ</string>
|
||||
<string name="type_watching">ଦେଖୁଛନ୍ତି</string>
|
||||
<string name="next_episode_time_day_format" formatted="true">%dଦି %dଘ %dମି</string>
|
||||
<string name="next_episode_time_hour_format" formatted="true">%dଘ %dମି</string>
|
||||
<string name="next_episode_time_min_format" formatted="true">%dମି</string>
|
||||
<string name="type_re_watching">ପୁନଃଦେଖୁଛନ୍ତି</string>
|
||||
<string name="home_expanded_hide">ଲୁଚାଅ</string>
|
||||
<string name="home_play">ଚଲାଅ</string>
|
||||
<string name="home_info">ସୂଚନା</string>
|
||||
<string name="title_home">ଗୃହ</string>
|
||||
<string name="title_search">ସନ୍ଧାନ</string>
|
||||
<string name="result_tags">ଧରଣ</string>
|
||||
<string name="type_on_hold">ସ୍ଥଗିତ</string>
|
||||
<string name="type_completed">ସାରିଛନ୍ତି</string>
|
||||
<string name="title_settings">ସେଟିଂ</string>
|
||||
<string name="duration_format" formatted="true">%d ମିନିଟ୍</string>
|
||||
<string name="player_speed_text_format" formatted="true">ଵେଗ (%.2fଗୁଣ)</string>
|
||||
<string name="type_dropped">ତ୍ୟାଗିଛନ୍ତି</string>
|
||||
<string name="type_plan_to_watch">ଦେଖିବା ପାଇଁ ଇଚ୍ଛୁକ</string>
|
||||
<string name="type_none">କିଛି ନାହିଁ</string>
|
||||
<string name="home_more_info">ଅଧିକ ସୂଚନା</string>
|
||||
<string name="cast_format" formatted="true">ପାତ୍ର: %s</string>
|
||||
<string name="result_poster_img_des">ପୋଷ୍ଟର୍</string>
|
||||
<string name="search_poster_img_des">ପୋଷ୍ଟର୍</string>
|
||||
<string name="play_episode_toast">ଅଧ୍ୟାୟ ଚଲାଅ</string>
|
||||
<string name="no_episodes_found">କୌଣସି ଅଧ୍ୟାୟ ମିଳିଲା ନାହିଁ</string>
|
||||
<string name="episodes">ଟି ଅଧ୍ୟାୟ</string>
|
||||
<string name="episode">ଟିଏ ଅଧ୍ୟାୟ</string>
|
||||
<string name="episode_action_play_in_format">%sରେ ଚଲାଅ</string>
|
||||
<string name="episode_action_play_in_browser">ବ୍ରାଉଜର୍ରେ ଚଲାଅ</string>
|
||||
<string name="episode_action_download_subtitle">ଉପଶୀର୍ଷକ ଡାଉନଲୋଡ୍ କରିବା</string>
|
||||
<string name="sync_total_episodes_some" formatted="true">/%d</string>
|
||||
<string name="sync_total_episodes_none">/\?\?</string>
|
||||
<string name="subscription_episode_released">ଅଧ୍ୟାୟ %d ମୁକ୍ତିଲାଭ କଲା!</string>
|
||||
<string name="episode_action_auto_download">ସ୍ୱତଃ ଡାଉନଲୋଡ୍</string>
|
||||
<string name="episode_action_reload_links">ଲିଙ୍କ୍ଗୁଡ଼ିକୁ ପୁନଃଲୋଡ୍ କରିବା</string>
|
||||
<string name="episode_action_copy_link">ଲିଙ୍କ୍ କପି କରିନେବା</string>
|
||||
<string name="episode_action_play_in_app">ଆପ୍ରେ ଚଲାଅ</string>
|
||||
<string name="episode_action_chromecast_episode">Chromecast ଅଧ୍ୟାୟ</string>
|
||||
<string name="episode_short">ଅ</string>
|
||||
<string name="episode_poster_img_des">ଅଧ୍ୟାୟର ପୋଷ୍ଟର୍</string>
|
||||
<string name="home_main_poster_img_des">ମୁଖ୍ୟ ପୋଷ୍ଟର୍</string>
|
||||
<string name="default_subtitles">ଡିଫଲ୍ଟ</string>
|
||||
<string name="extension_language">ଭାଷା</string>
|
||||
<string name="no">ନାହିଁ</string>
|
||||
<string name="extension_description">ଵର୍ଣ୍ଣନା</string>
|
||||
<string name="yes">ହଁ</string>
|
||||
<string name="library">ଲାଇବ୍ରେରୀ</string>
|
||||
<string name="history">ଇତିଵୃତ୍ତି</string>
|
||||
<string name="extension_authors">ଲେଖକ</string>
|
||||
<string name="skip_type_format" formatted="true">%s ବାଦ୍ ଦିଅ</string>
|
||||
<string name="subs_subtitle_languages">ଉପଶୀର୍ଷକ ଭାଷା</string>
|
||||
<string name="single_plugin_disabled" formatted="true">%s (ଅକ୍ଷମ)</string>
|
||||
<string name="extension_status">ସ୍ଥିତି</string>
|
||||
<string name="extension_size">ଆକାର</string>
|
||||
<string name="extension_types">ସମର୍ଥିତ</string>
|
||||
<string name="hls_playlist">HLS ଚାଳନାତାଲିକା</string>
|
||||
<string name="player_settings_play_in_app">ଅନ୍ତଃ-ଚାଳକ</string>
|
||||
<string name="skip_type_op">ଆଦ୍ୟ</string>
|
||||
<string name="skip_type_ed">ପ୍ରାନ୍ତ</string>
|
||||
<string name="app_not_found_error">ଆପ୍ ମିଳିଲା ନାହିଁ</string>
|
||||
<string name="all_languages_preference">ସବୁ ଭାଷା</string>
|
||||
<string name="player_settings_play_in_vlc">VLC</string>
|
||||
<string name="player_settings_play_in_mpv">MPV</string>
|
||||
<string name="skip_type_mixed_ed">ମିଶ୍ରିତ ପ୍ରାନ୍ତ</string>
|
||||
<string name="skip_type_mixed_op">ମିଶ୍ରିତ ଆଦ୍ୟ</string>
|
||||
<string name="skip_type_creddits">ଶ୍ରେୟ</string>
|
||||
<string name="skip_type_intro">ଉପକ୍ରମ</string>
|
||||
<string name="provider_languages_tip">ଏହି ଭାଷାଗୁଡ଼ିକରେ ଵିଡ଼ିଓ ଦେଖନ୍ତୁ</string>
|
||||
<string name="extension_version">ସଂସ୍କରଣ</string>
|
||||
<string name="app_language">ଆପ୍ ଭାଷା</string>
|
||||
<string name="play_episode">ଅଧ୍ୟାୟ ଚଲାଅ</string>
|
||||
<string name="season_short">ଋ</string>
|
||||
<string name="status_ongoing">ଚଳିତ</string>
|
||||
<string name="copy_link_toast">ଲିଙ୍କ୍ କ୍ଲିପ୍ବୋର୍ଡରେ କପି କରିନିଆଗଲା</string>
|
||||
<string name="movies">ଚଳଚ୍ଚିତ୍ର</string>
|
||||
<string name="livestreams">ସିଧାପ୍ରସାରଣ</string>
|
||||
<string name="video_source">ଉତ୍ସ</string>
|
||||
<string name="no_update_found">କୌଣସି ଅଦ୍ୟତନ ମିଳିଲା ନାହିଁ</string>
|
||||
<string name="category_general">ସାଧାରଣ</string>
|
||||
<string name="dont_show_again">ପୁନଃ ଦେଖାଅନି</string>
|
||||
<string name="automatic">ସ୍ୱତଃ</string>
|
||||
<string name="error">ତ୍ରୁଟି</string>
|
||||
<string name="restore_settings">ବ୍ୟାକଅପ୍ରୁ ତଥ୍ୟ ପୁନରୁଦ୍ଧାର କରିବା</string>
|
||||
<string name="backup_failed">ଷ୍ଟୋରେଜ୍ ଅନୁମତି ଦିଆଯାଇ ନାହିଁ। ଦୟାକରି ପୁଣିଥରେ ଚେଷ୍ଟା କରନ୍ତୁ।</string>
|
||||
<string name="category_updates">ଅଦ୍ୟତନ ଏଵଂ ବ୍ୟାକଅପ୍</string>
|
||||
<string name="pref_category_backup">ବ୍ୟାକଅପ୍</string>
|
||||
<string name="pref_category_android_tv">ଆଣ୍ଡ୍ରଏଡ୍ ଟିଵି</string>
|
||||
<string name="pref_category_gestures">ଅଙ୍ଗଭଙ୍ଗୀ</string>
|
||||
<string name="new_update_format" formatted="true">ନୂଆ ଅଦ୍ୟତନ ମିଳିଲା!
|
||||
\n%s -> %s</string>
|
||||
<string name="duration">ଅଵଧି</string>
|
||||
<string name="app_storage">ଆପ୍</string>
|
||||
<string name="restore_success">ବ୍ୟାକଅପ୍ ଫାଇଲ୍ ଧାରଣ ହେଲା</string>
|
||||
<string name="backup_success">ତଥ୍ୟ ଗଚ୍ଛିତ ହୋଇଛି</string>
|
||||
<string name="backup_failed_error_format">%s ବ୍ୟାକଅପ୍ ନେବାରେ ତ୍ରୁଟି ଘଟିଲା</string>
|
||||
<string name="season">ଋତୁ</string>
|
||||
<string name="no_season">କୌଣସି ଋତୁ ନାହିଁ</string>
|
||||
<string name="delete_file">ଫାଇଲ୍ ଵିଲୋପ କରିବେ</string>
|
||||
<string name="test_passed">ପାରିତ ହେଲା</string>
|
||||
<string name="go_back_30">-୩୦</string>
|
||||
<string name="status">ସ୍ଥିତି</string>
|
||||
<string name="used_storage">ଵ୍ୟଵହୃତ</string>
|
||||
<string name="tv_series">ଟିଵି ଧାରାଵାହିକ</string>
|
||||
<string name="asian_drama">ଏସୀୟ ନାଟକ</string>
|
||||
<string name="others">ଅନ୍ୟାନ୍ୟ</string>
|
||||
<string name="other_singular">ଵିଡ଼ିଓ</string>
|
||||
<string name="source_error">ଉତ୍ସ ତ୍ରୁଟି</string>
|
||||
<string name="unexpected_error">ଅପ୍ରତ୍ୟାଶିତ ଚାଳକ ତ୍ରୁଟି</string>
|
||||
<string name="show_title">ଆଖ୍ୟା</string>
|
||||
<string name="check_for_update">ଅଦ୍ୟତନ ପାଇଁ ଯାଞ୍ଚ କରିବା</string>
|
||||
<string name="video_lock">ତାଲା</string>
|
||||
<string name="video_aspect_ratio_resize">ଆକାର ଠିକ୍ କରିବା</string>
|
||||
<string name="skip_update">ଏହି ଅଦ୍ୟତନଟିକୁ ବାଦ୍ ଦିଅ</string>
|
||||
<string name="pref_category_actions">କୃତ୍ୟ</string>
|
||||
<string name="pref_category_subtitles">ଉପଶୀର୍ଷକ</string>
|
||||
<string name="pref_category_ui_features">ଵୈଶିଷ୍ଟ୍ୟସବୁ</string>
|
||||
<string name="pref_category_looks">ଵେଶ</string>
|
||||
<string name="pref_category_defaults">ଡିଫଲ୍ଟଗୁଡ଼ା</string>
|
||||
<string name="primary_color_settings">ପ୍ରାଥମିକ ରଙ୍ଗ</string>
|
||||
<string name="added_sync_format" formatted="true">%s ଯୋଡ଼ାଗଲା</string>
|
||||
<string name="title">ଆଖ୍ୟା</string>
|
||||
<string name="setup_done">ହେଲା</string>
|
||||
<string name="update_notification_downloading">ଆପ୍ ଅଦ୍ୟତନ ଡାଉନଲୋଡ୍ ଚାଲିଛି…</string>
|
||||
<string name="update_notification_installing">ଆପ୍ ଅଦ୍ୟତନ ଅଧିସ୍ଥାପନ ଚାଲିଛି…</string>
|
||||
<string name="update_notification_failed">ଆପ୍ର ନୂଆ ସଂସ୍କରଣ ଅଧିସ୍ଥାପନ କରିହେଲା ନାହିଁ</string>
|
||||
<string name="test_failed">ଵିଫଳ ହେଲା</string>
|
||||
<string name="category_player">ଚାଳକ</string>
|
||||
<string name="backup_settings">ତଥ୍ୟର ବ୍ୟାକଅପ୍ ନେବା</string>
|
||||
<string name="delete">ଵିଲୋପ କର</string>
|
||||
<string name="documentaries">ଵୃତ୍ତଚିତ୍ର</string>
|
||||
<string name="anime_singular">ଅନିମେ</string>
|
||||
<string name="tv_series_singular">ଧାରାଵାହିକ</string>
|
||||
<string name="movies_singular">ଚଳଚ୍ଚିତ୍ର</string>
|
||||
<string name="documentaries_singular">ଵୃତ୍ତଚିତ୍ର</string>
|
||||
<string name="asian_drama_singular">ଏସୀୟ ନାଟକ</string>
|
||||
<string name="live_singular">ସିଧାପ୍ରସାରଣ</string>
|
||||
<string name="show_hd">ଗୁଣଵତ୍ତା ଲେବଲ୍</string>
|
||||
<string name="update">ଅଦ୍ୟତନ କରିବା</string>
|
||||
<string name="pref_category_player_features">ଚାଳକ ଵୈଶିଷ୍ଟ୍ୟସବୁ</string>
|
||||
<string name="app_theme_settings">ଆପ୍ ଥିମ୍</string>
|
||||
<string name="subs_auto_select_language">ଭାଷା ସ୍ୱତଃ-ଚୟନ</string>
|
||||
<string name="anime">ଅନିମେ</string>
|
||||
<string name="player_subtitles_settings">ଉପଶୀର୍ଷକ</string>
|
||||
<string name="go_forward_30">+୩୦</string>
|
||||
<string name="year">ଵର୍ଷ</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<string name="player_speed_text_format" formatted="true">Prędkość (%.2fx)</string>
|
||||
<string name="rated_format" formatted="true">Ocena: %.1f</string>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<string name="rated_format">Betygsatt: %.1f</string>
|
||||
<string name="player_speed_text_format">Hastighet (%.2fx)</string>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- KEYS DON'T TRANSLATE -->
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
<string name="extra_info_format" formatted="true" translatable="false">%d %s | %s</string>
|
||||
|
|
|
@ -453,22 +453,22 @@
|
|||
<string name="player_settings_play_in_mpv">MPV</string>
|
||||
<string name="player_settings_play_in_web">Відтворення веб-відео</string>
|
||||
<string name="player_settings_play_in_browser">Веб-браузер</string>
|
||||
<string name="skip_type_ed">Кінець</string>
|
||||
<string name="skip_type_ed">Ендінґ</string>
|
||||
<string name="skip_type_recap">Коротке повторення</string>
|
||||
<string name="skip_type_format" formatted="true">Пропустити %s</string>
|
||||
<string name="skip_type_mixed_ed">Змішаний кінець</string>
|
||||
<string name="skip_type_mixed_ed">Змішаний ендінґ</string>
|
||||
<string name="skip_type_creddits">Подяки</string>
|
||||
<string name="skip_type_op">Опенінг</string>
|
||||
<string name="skip_type_op">Опенінґ</string>
|
||||
<string name="skip_type_intro">Вступ</string>
|
||||
<string name="clear_history">Очистити історію</string>
|
||||
<string name="history">Історія</string>
|
||||
<string name="enable_skip_op_from_database_des">Показувати спливаючі вікна для опенінгу/кінця</string>
|
||||
<string name="enable_skip_op_from_database_des">Показувати спливаючі вікна для опенінґу/кінця</string>
|
||||
<string name="clipboard_too_large">Забагато тексту. Не вдалося зберегти в буфер обміну.</string>
|
||||
<string name="action_mark_as_watched">Позначити як переглянуте</string>
|
||||
<string name="confirm_exit_dialog">Ви впевнені що хочете вийти\?</string>
|
||||
<string name="yes">Так</string>
|
||||
<string name="no">Ні</string>
|
||||
<string name="update_notification_installing">Установлення оновлення програми…</string>
|
||||
<string name="update_notification_installing">Встановлення оновлення програми…</string>
|
||||
<string name="update_notification_failed">Не вдалося встановити нову версію програми</string>
|
||||
<string name="apk_installer_legacy">Старий</string>
|
||||
<string name="apk_installer_package_installer">Інсталятор пакетів</string>
|
||||
|
@ -487,7 +487,7 @@
|
|||
<string name="update_notification_downloading">Завантаження оновлення програми…</string>
|
||||
<string name="safe_mode_description">Усі розширення були вимкнені через збій, щоб допомогти вам знайти те, що стало причиною проблеми.</string>
|
||||
<string name="app_not_found_error">Програму не знайдено</string>
|
||||
<string name="skip_type_mixed_op">Змішаний опенінг</string>
|
||||
<string name="skip_type_mixed_op">Змішаний опенінґ</string>
|
||||
<string name="action_remove_from_watched">Видалити з переглянутого</string>
|
||||
<string name="sort_updated_old">За оновленням (від старого до нового)</string>
|
||||
<string name="sort_updated_new">За оновленням (від нового до старого)</string>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- KEYS DON'T TRANSLATE -->
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
<string name="extra_info_format" formatted="true" translatable="false">%d %s | %s</string>
|
||||
|
@ -143,14 +142,14 @@
|
|||
<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_seek_settings_des">從一側滑動到另一側以控制影片中的位置</string>
|
||||
<string name="swipe_to_change_settings">滑動更改設定</string>
|
||||
<string name="swipe_to_change_settings_des">上下滑動更改亮度或音量</string>
|
||||
<string name="autoplay_next_settings">自動播放下一集</string>
|
||||
<string name="autoplay_next_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>
|
||||
<string name="double_tap_to_seek_amount_settings">輕按兩下以控制進度時間(秒)</string>
|
||||
<string name="double_tap_to_seek_settings_des">在右側或左側輕按兩次以向前或向後快轉</string>
|
||||
<string name="double_tap_to_pause_settings_des">輕按兩下中間以暫停</string>
|
||||
<string name="use_system_brightness_settings">使用系統亮度</string>
|
||||
|
@ -178,7 +177,7 @@
|
|||
<string name="pref_filter_search_quality">在搜尋結果中隱藏選中的影片畫質</string>
|
||||
<string name="automatic_plugin_updates">自動更新外掛程式</string>
|
||||
<string name="updates_settings">顯示應用更新</string>
|
||||
<string name="updates_settings_des">啟動時自動搜尋更新</string>
|
||||
<string name="updates_settings_des">啟動應用程式後自動搜尋更新。</string>
|
||||
<string name="uprereleases_settings">更新至預覽版</string>
|
||||
<string name="uprereleases_settings_des">搜尋預覽版更新而不是僅搜尋正式版</string>
|
||||
<string name="github">Github</string>
|
||||
|
@ -245,8 +244,8 @@
|
|||
<string name="movies_singular">電影</string>
|
||||
<string name="tv_series_singular">電視劇</string>
|
||||
<string name="cartoons_singular">卡通</string>
|
||||
<string name="anime_singular">@string/anime</string>
|
||||
<string name="ova_singular">@string/ova</string>
|
||||
<string name="anime_singular">動畫</string>
|
||||
<string name="ova_singular">OVA</string>
|
||||
<string name="torrent_singular">種子</string>
|
||||
<string name="documentaries_singular">紀錄片</string>
|
||||
<string name="asian_drama_singular">亞洲劇</string>
|
||||
|
@ -286,7 +285,7 @@
|
|||
<string name="dont_show_again">不再顯示</string>
|
||||
<string name="skip_update">跳過此更新</string>
|
||||
<string name="update">更新</string>
|
||||
<string name="watch_quality_pref">偏好播放畫質</string>
|
||||
<string name="watch_quality_pref">偏好播放畫質 (WiFi)</string>
|
||||
<string name="limit_title">影片播放器標題最大字數</string>
|
||||
<string name="limit_title_rez">影片播放器標題</string>
|
||||
<string name="video_buffer_size_settings">影片緩衝大小</string>
|
||||
|
@ -535,4 +534,47 @@
|
|||
<string name="pref_category_looks">外觀</string>
|
||||
<string name="pref_category_ui_features">功能</string>
|
||||
<string name="browser">瀏覽器</string>
|
||||
<string name="subscription_episode_released">第 %d 集已發行!</string>
|
||||
<string name="library">媒體庫</string>
|
||||
<string name="start">開始</string>
|
||||
<string name="android_tv_interface_on_seek_settings">播放器顯示 - 快轉快退秒數</string>
|
||||
<string name="open_with">開啟方式</string>
|
||||
<string name="delayed_update_notice">應用程式將在關閉時更新</string>
|
||||
<string name="sort_rating_asc">評分(從低到高)</string>
|
||||
<string name="update_started">更新開始</string>
|
||||
<string name="plugin_downloaded">外掛程式已下載</string>
|
||||
<string name="action_remove_from_watched">從觀看中刪除</string>
|
||||
<string name="sort_by">排序方式</string>
|
||||
<string name="sort">排序</string>
|
||||
<string name="sort_rating_desc">評分(從高到低)</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">播放器可見時使用的快轉快退秒數</string>
|
||||
<string name="android_tv_interface_off_seek_settings">播放器隱藏 - 快轉快退秒數</string>
|
||||
<string name="sort_updated_new">更新(從新到舊)</string>
|
||||
<string name="sort_updated_old">更新(從舊到新)</string>
|
||||
<string name="sort_alphabetical_a">按字母順序(A 到 Z)</string>
|
||||
<string name="sort_alphabetical_z">按字母順序(Z 到 A)</string>
|
||||
<string name="select_library">選擇媒體庫</string>
|
||||
<string name="safe_mode_file">找到安全模式檔案!
|
||||
\n在刪除檔案之前不在啟動時載入任何擴充功能。</string>
|
||||
<string name="test_log">日誌</string>
|
||||
<string name="test_failed">失敗</string>
|
||||
<string name="test_passed">通過</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">播放器隱藏時使用的快轉快退秒數</string>
|
||||
<string name="pref_category_android_tv">Android TV</string>
|
||||
<string name="category_provider_test">片源測試</string>
|
||||
<string name="restart">重新啟動</string>
|
||||
<string name="stop">停止</string>
|
||||
<string name="subscription_list_name">訂閱</string>
|
||||
<string name="subscription_new">已訂閱 %s</string>
|
||||
<string name="subscription_deleted">已取消訂閱 %s</string>
|
||||
<string name="watch_quality_pref_data">偏好播放畫質 (行動數據)</string>
|
||||
<string name="jsdelivr_proxy">raw.githubusercontent.com Proxy</string>
|
||||
<string name="pref_category_bypass">繞過 ISP</string>
|
||||
<string name="revert">還原</string>
|
||||
<string name="jsdelivr_enabled">無法訪問 GitHub。 正在開啟 jsDelivr proxy…</string>
|
||||
<string name="jsdelivr_proxy_summary">使用 jsDelivr 繞過 GitHub 的阻擋。 可能導致更新延遲幾天。</string>
|
||||
<string name="empty_library_no_accounts_message">您的媒體庫是空的:(
|
||||
\n登入媒體庫帳戶或將節目添加到您本機的媒體庫。</string>
|
||||
<string name="empty_library_logged_in_message">此列表是空的。 嘗試切換到另一個。</string>
|
||||
<string name="subscription_in_progress_notification">正在更新訂閱節目</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
|
||||
<resources>
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
<string name="extra_info_format" formatted="true" translatable="false">%d %s | %s</string>
|
||||
|
|
|
@ -664,8 +664,27 @@
|
|||
<string name="subscription_new">Subscribed to %s</string>
|
||||
<string name="subscription_deleted">Unsubscribed from %s</string>
|
||||
<string name="subscription_episode_released">Episode %d released!</string>
|
||||
<string name="profile_number">Profile %d</string>
|
||||
<string name="wifi">Wi-Fi</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="set_default">Set default</string>
|
||||
<string name="use">Use</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="profiles">Profiles</string>
|
||||
<string name="help">Help</string>
|
||||
<string name="quality_profile_help">
|
||||
Here you can change how the sources are ordered. If a video has a higher priority it will appear higher in the source selection.
|
||||
The sum of the source priority and the quality priority is the video priority.
|
||||
\n\nSource A: 3
|
||||
\nQuality B: 7
|
||||
\nWill have a combined video priority of 10.
|
||||
|
||||
\n\nNOTE: If the sum is 10 or more the player will automatically skip loading when that link is loaded!
|
||||
</string>
|
||||
<string name="qualities">Qualities</string>
|
||||
<string name="profile_background_des">Profile background</string>
|
||||
<string name="example_login_file_name_full">Sync file name (optional)</string>
|
||||
<string name="example_login_redirect_url_full">Oauth redirect url (optional)</string>
|
||||
<string name="example_redirect_url" translatable="false">https://recloudstream.github.io/cloudstream-sync/google-drive</string>
|
||||
<string name="info_button">Info</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -11,14 +11,14 @@
|
|||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_category_defaults">
|
||||
<Preference
|
||||
android:icon="@drawable/ic_baseline_hd_24"
|
||||
android:key="@string/quality_pref_key"
|
||||
android:title="@string/watch_quality_pref" />
|
||||
<Preference
|
||||
android:icon="@drawable/ic_baseline_hd_24"
|
||||
android:key="@string/quality_pref_mobile_data_key"
|
||||
android:title="@string/watch_quality_pref_data" />
|
||||
<!-- <Preference-->
|
||||
<!-- android:icon="@drawable/ic_baseline_hd_24"-->
|
||||
<!-- android:key="@string/quality_pref_key"-->
|
||||
<!-- android:title="@string/watch_quality_pref" />-->
|
||||
<!-- <Preference-->
|
||||
<!-- android:icon="@drawable/ic_baseline_hd_24"-->
|
||||
<!-- android:key="@string/quality_pref_mobile_data_key"-->
|
||||
<!-- android:title="@string/watch_quality_pref_data" />-->
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/netflix_play"
|
||||
|
|