Merge branch 'master' into arabicSubs100%

This commit is contained in:
Cloudburst 2022-09-22 11:43:14 +02:00 committed by GitHub
commit 603b30d357
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 713 additions and 389 deletions

View file

@ -39,9 +39,8 @@ jobs:
- name: Clean old builds - name: Clean old builds
run: | run: |
shopt -s extglob
cd $GITHUB_WORKSPACE/dokka/ cd $GITHUB_WORKSPACE/dokka/
rm -rf !(.git) rm -rf "./-cloudstream"
- name: Setup JDK 11 - name: Setup JDK 11
uses: actions/setup-java@v1 uses: actions/setup-java@v1

View file

@ -1,63 +1,63 @@
name: Issue automatic actions name: Issue automatic actions
on: on:
issues: issues:
types: [opened, edited] types: [opened, edited]
jobs: jobs:
issue-moderator: issue-moderator:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Generate access token - name: Generate access token
id: generate_token id: generate_token
uses: tibdex/github-app-token@v1 uses: tibdex/github-app-token@v1
with: with:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
- name: Similarity analysis - name: Similarity analysis
uses: actions-cool/issues-similarity-analysis@v1 uses: actions-cool/issues-similarity-analysis@v1
with: with:
token: ${{ steps.generate_token.outputs.token }} token: ${{ steps.generate_token.outputs.token }}
filter-threshold: 0.5 filter-threshold: 0.5
title-excludes: '' title-excludes: ''
comment-title: | comment-title: |
### Your issue looks similar to these issues: ### Your issue looks similar to these issues:
Please close if duplicate. Please close if duplicate.
comment-body: '${index}. ${similarity} #${number}' comment-body: '${index}. ${similarity} #${number}'
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Automatically close issues that dont follow the issue template - name: Automatically close issues that dont follow the issue template
uses: lucasbento/auto-close-issues@v1.0.2 uses: lucasbento/auto-close-issues@v1.0.2
with: with:
github-token: ${{ steps.generate_token.outputs.token }} github-token: ${{ steps.generate_token.outputs.token }}
issue-close-message: | issue-close-message: |
@${issue.user.login}: hello! :wave: @${issue.user.login}: hello! :wave:
This issue is being automatically closed because it does not follow the issue template." This issue is being automatically closed because it does not follow the issue template."
closed-issues-label: "invalid" closed-issues-label: "invalid"
- name: Check if issue mentions a provider - name: Check if issue mentions a provider
id: provider_check id: provider_check
env: env:
GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}" GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}"
run: | run: |
wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py" wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py"
pip3 install httpx pip3 install httpx
RES="$(python3 ./check_issue.py)" RES="$(python3 ./check_issue.py)"
echo "::set-output name=name::${RES}" echo "::set-output name=name::${RES}"
- name: Comment if issue mentions a provider - name: Comment if issue mentions a provider
if: steps.provider_check.outputs.name != 'none' if: steps.provider_check.outputs.name != 'none'
uses: actions-cool/issues-helper@v3 uses: actions-cool/issues-helper@v3
with: with:
actions: 'create-comment' actions: 'create-comment'
token: ${{ steps.generate_token.outputs.token }} token: ${{ steps.generate_token.outputs.token }}
body: | body: |
Hello ${{ github.event.issue.user.login }}. Hello ${{ github.event.issue.user.login }}.
Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM). Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM).
Found provider name: `${{ steps.provider_check.outputs.name }}` Found provider name: `${{ steps.provider_check.outputs.name }}`
- name: Add eyes reaction to all issues - name: Add eyes reaction to all issues
uses: actions-cool/emoji-helper@v1.0.0 uses: actions-cool/emoji-helper@v1.0.0
with: with:
type: 'issue' type: 'issue'
token: ${{ steps.generate_token.outputs.token }} token: ${{ steps.generate_token.outputs.token }}
emoji: 'eyes' emoji: 'eyes'

View file

@ -43,9 +43,7 @@ jobs:
echo "::set-output name=key_pwd::$KEY_PWD" echo "::set-output name=key_pwd::$KEY_PWD"
- name: Run Gradle - name: Run Gradle
run: | run: |
./gradlew assemblePrerelease ./gradlew assemblePrerelease makeJar androidSourcesJar
./gradlew androidSourcesJar
./gradlew makeJar
env: env:
SIGNING_KEY_ALIAS: "key0" SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}

View file

@ -170,7 +170,7 @@ dependencies {
// Networking // Networking
// implementation "com.squareup.okhttp3:okhttp:4.9.2" // implementation "com.squareup.okhttp3:okhttp:4.9.2"
// implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1" // implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
implementation 'com.github.Blatzar:NiceHttp:0.3.2' implementation 'com.github.Blatzar:NiceHttp:0.3.3'
// Util to skip the URI file fuckery 🙏 // Util to skip the URI file fuckery 🙏
implementation "com.github.tachiyomiorg:unifile:17bec43" implementation "com.github.tachiyomiorg:unifile:17bec43"
@ -206,10 +206,28 @@ task androidSourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs//full sources from android.sourceSets.main.java.srcDirs//full sources
} }
// this is used by the gradlew plugin
task makeJar(type: Copy) { task makeJar(type: Copy) {
// after modifying here, you can export. Jar
from('build/intermediates/compile_app_classes_jar/debug') from('build/intermediates/compile_app_classes_jar/debug')
into('build') // output location into('build')
include('classes.jar') // the classes file of the imported rack package include('classes.jar')
dependsOn build dependsOn('build')
} }
dokkaHtml {
moduleName.set("Cloudstream")
dokkaSourceSets {
main {
sourceLink {
// Unix based directory relative path to the root of the project (where you execute gradle respectively).
localDirectory.set(file("src/main/java"))
// URL showing where the source code can be accessed through the web browser
remoteUrl.set(new URL(
"https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
// Suffix which is used to append the line number to the URL. Use #L for GitHub
remoteLineSuffix.set("#L")
}
}
}
}

View file

@ -7,10 +7,12 @@ import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.google.auto.service.AutoService import com.google.auto.service.AutoService
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKey
@ -74,19 +76,28 @@ class CustomSenderFactory : ReportSenderFactory {
} }
} }
class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.UncaughtExceptionHandler { class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread, error: Throwable) { override fun uncaughtException(thread: Thread, error: Throwable) {
ACRA.errorReporter.handleException(error) ACRA.errorReporter.handleException(error)
try { try {
PrintStream(errorFile).use { ps -> PrintStream(errorFile).use { ps ->
ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")) ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}"))
ps.println(String.format("Fatal exception on thread %s (%d)", thread.name, thread.id)) ps.println(
String.format(
"Fatal exception on thread %s (%d)",
thread.name,
thread.id
)
)
error.printStackTrace(ps) error.printStackTrace(ps)
} }
} catch (ignored: FileNotFoundException) { } } catch (ignored: FileNotFoundException) {
}
try { try {
onError.invoke() onError.invoke()
} catch (ignored: Exception) { } } catch (ignored: Exception) {
}
exitProcess(1) exitProcess(1)
} }
@ -95,7 +106,7 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.U
class AcraApplication : Application() { class AcraApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")){ Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) {
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
startActivity(Intent.makeRestartActivityTask(intent!!.component)) startActivity(Intent.makeRestartActivityTask(intent!!.component))
}) })
@ -183,5 +194,15 @@ class AcraApplication : Application() {
fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) { fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) {
context?.openBrowser(url, fallbackWebview, fragment) context?.openBrowser(url, fallbackWebview, fragment)
} }
/** Will fallback to webview if in TV layout */
fun openBrowser(url: String, activity: FragmentActivity?) {
openBrowser(
url,
isTvSettings(),
activity?.supportFragmentManager?.fragments?.lastOrNull()
)
}
} }
} }

View file

@ -40,6 +40,7 @@ object APIHolder {
private const val defProvider = 0 private const val defProvider = 0
// ConcurrentModificationException is possible!!!
val allProviders: MutableList<MainAPI> = arrayListOf() val allProviders: MutableList<MainAPI> = arrayListOf()
fun initAll() { fun initAll() {
@ -1118,6 +1119,11 @@ data class NextAiring(
val unixTime: Long, val unixTime: Long,
) )
/**
* @param season To be mapped with episode season, not shown in UI if displaySeason is defined
* @param name To be shown next to the season like "Season $displaySeason $name" but if displaySeason is null then "$name"
* @param displaySeason What to be displayed next to the season name, if null then the name is the only thing shown.
* */
data class SeasonData( data class SeasonData(
val season: Int, val season: Int,
val name: String? = null, val name: String? = null,
@ -1198,9 +1204,12 @@ data class AnimeLoadResponse(
override var backgroundPosterUrl: String? = null, override var backgroundPosterUrl: String? = null,
) : LoadResponse, EpisodeResponse ) : LoadResponse, EpisodeResponse
/**
* If episodes already exist appends the list.
* */
fun AnimeLoadResponse.addEpisodes(status: DubStatus, episodes: List<Episode>?) { fun AnimeLoadResponse.addEpisodes(status: DubStatus, episodes: List<Episode>?) {
if (episodes.isNullOrEmpty()) return if (episodes.isNullOrEmpty()) return
this.episodes[status] = episodes this.episodes[status] = (this.episodes[status] ?: emptyList()) + episodes
} }
suspend fun MainAPI.newAnimeLoadResponse( suspend fun MainAPI.newAnimeLoadResponse(

View file

@ -15,6 +15,7 @@ import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
@ -144,6 +145,68 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val mainPluginsLoadedEvent = val mainPluginsLoadedEvent =
Event<Boolean>() // homepage api, used to speed up time to load for homepage Event<Boolean>() // homepage api, used to speed up time to load for homepage
val afterRepositoryLoadedEvent = Event<Boolean>() val afterRepositoryLoadedEvent = Event<Boolean>()
/**
* @return true if the str has launched an app task (be it successful or not)
* @param isWebview does not handle providers and opening download page if true. Can still add repos and login.
* */
fun handleAppIntentUrl(activity: FragmentActivity?, str: String?, isWebview: Boolean): Boolean =
with(activity) {
if (str != null && this != null) {
if (str.startsWith("https://cs.repo")) {
val realUrl = "https://" + str.substringAfter("?")
println("Repository url: $realUrl")
loadRepository(realUrl)
return true
} else if (str.contains(appString)) {
for (api in OAuth2Apis) {
if (str.contains("/${api.redirectUrl}")) {
ioSafe {
Log.i(TAG, "handleAppIntent $str")
val isSuccessful = api.handleRedirect(str)
if (isSuccessful) {
Log.i(TAG, "authenticated ${api.name}")
} else {
Log.i(TAG, "failed to authenticate ${api.name}")
}
this@with.runOnUiThread {
try {
showToast(
this@with,
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
api.name
)
)
} catch (e: Exception) {
logError(e) // format might fail
}
}
}
return true
}
}
} else if (URI(str).scheme == appStringRepo) {
val url = str.replaceFirst(appStringRepo, "https")
loadRepository(url)
return true
} else if (!isWebview){
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
this.navigate(R.id.navigation_downloads)
return true
} else {
for (api in apis) {
if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name)
return true
}
}
}
}
}
return false
}
} }
override fun onColorSelected(dialogId: Int, color: Int) { override fun onColorSelected(dialogId: Int, color: Int) {
@ -348,56 +411,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (intent == null) return if (intent == null) return
val str = intent.dataString val str = intent.dataString
loadCache() loadCache()
if (str != null) { handleAppIntentUrl(this, str, false)
if (str.startsWith("https://cs.repo")) {
val realUrl = "https://" + str.substringAfter("?")
println("Repository url: $realUrl")
loadRepository(realUrl)
} else if (str.contains(appString)) {
for (api in OAuth2Apis) {
if (str.contains("/${api.redirectUrl}")) {
val activity = this
ioSafe {
Log.i(TAG, "handleAppIntent $str")
val isSuccessful = api.handleRedirect(str)
if (isSuccessful) {
Log.i(TAG, "authenticated ${api.name}")
} else {
Log.i(TAG, "failed to authenticate ${api.name}")
}
activity.runOnUiThread {
try {
showToast(
activity,
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
api.name
)
)
} catch (e: Exception) {
logError(e) // format might fail
}
}
}
}
}
} else if (URI(str).scheme == appStringRepo) {
val url = str.replaceFirst(appStringRepo, "https")
loadRepository(url)
} else {
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
this.navigate(R.id.navigation_downloads)
} else {
for (api in apis) {
if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name)
break
}
}
}
}
}
} }
private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean = private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
@ -445,7 +459,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
} }
// it.hashCode() is not enough to make sure they are distinct // it.hashCode() is not enough to make sure they are distinct
apis = allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name } apis =
allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
APIHolder.apiMap = null APIHolder.apiMap = null
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
@ -465,9 +480,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
lastError = errorFile.readText(Charset.defaultCharset()) lastError = errorFile.readText(Charset.defaultCharset())
errorFile.delete() errorFile.delete()
} }
val settingsForProvider = SettingsJson() val settingsForProvider = SettingsJson()
settingsForProvider.enableAdult = settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false) settingsForProvider.enableAdult =
settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false)
MainAPI.settingsForProvider = settingsForProvider MainAPI.settingsForProvider = settingsForProvider
@ -501,7 +517,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
ioSafe { ioSafe {
if (settingsManager.getBoolean(getString(R.string.auto_update_plugins_key), true)) { if (settingsManager.getBoolean(
getString(R.string.auto_update_plugins_key),
true
)
) {
PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity) PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
} else { } else {
PluginManager.loadAllOnlinePlugins(this@MainActivity) PluginManager.loadAllOnlinePlugins(this@MainActivity)

View file

@ -7,6 +7,10 @@ import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
class DoodWfExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.wf"
}
class DoodCxExtractor : DoodLaExtractor() { class DoodCxExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.cx" override var mainUrl = "https://dood.cx"
} }

View file

@ -31,7 +31,7 @@ class GMPlayer : ExtractorApi() {
this.name, this.name,
this.name, this.name,
m3u8, m3u8,
"", ref,
Qualities.Unknown.value, Qualities.Unknown.value,
headers = mapOf("accept" to "*/*"), headers = mapOf("accept" to "*/*"),
isM3u8 = true isM3u8 = true

View file

@ -0,0 +1,182 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.*
import org.jsoup.nodes.Element
import java.security.DigestException
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class DatabaseGdrive : Gdriveplayer() {
override var mainUrl = "https://series.databasegdriveplayer.co"
}
class Gdriveplayerapi: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayerapi.com"
}
class Gdriveplayerapp: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.app"
}
class Gdriveplayerfun: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.fun"
}
class Gdriveplayerio: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.io"
}
class Gdriveplayerme: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.me"
}
class Gdriveplayerbiz: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.biz"
}
class Gdriveplayerorg: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.org"
}
class Gdriveplayerus: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.us"
}
class Gdriveplayerco: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.co"
}
open class Gdriveplayer : ExtractorApi() {
override val name = "Gdrive"
override val mainUrl = "https://gdriveplayer.to"
override val requiresReferer = false
private fun unpackJs(script: Element): String? {
return script.select("script").find { it.data().contains("eval(function(p,a,c,k,e,d)") }
?.data()?.let { getAndUnpack(it) }
}
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
// https://stackoverflow.com/a/41434590/8166854
private fun GenerateKeyAndIv(
password: ByteArray,
salt: ByteArray,
hashAlgorithm: String = "MD5",
keyLength: Int = 32,
ivLength: Int = 16,
iterations: Int = 1
): List<ByteArray>? {
val md = MessageDigest.getInstance(hashAlgorithm)
val digestLength = md.digestLength
val targetKeySize = keyLength + ivLength
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
val generatedData = ByteArray(requiredLength)
var generatedLength = 0
try {
md.reset()
while (generatedLength < targetKeySize) {
if (generatedLength > 0)
md.update(
generatedData,
generatedLength - digestLength,
digestLength
)
md.update(password)
md.update(salt, 0, 8)
md.digest(generatedData, generatedLength, digestLength)
for (i in 1 until iterations) {
md.update(generatedData, generatedLength, digestLength)
md.digest(generatedData, generatedLength, digestLength)
}
generatedLength += digestLength
}
return listOf(
generatedData.copyOfRange(0, keyLength),
generatedData.copyOfRange(keyLength, targetKeySize)
)
} catch (e: DigestException) {
return null
}
}
private fun cryptoAESHandler(
data: AesData,
pass: ByteArray,
encrypt: Boolean = true
): String? {
val (key, iv) = GenerateKeyAndIv(pass, data.s.decodeHex()) ?: return null
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
String(cipher.doFinal(base64DecodeArray(data.ct)))
} else {
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
base64Encode(cipher.doFinal(data.ct.toByteArray()))
}
}
private fun Regex.first(str: String): String? {
return find(str)?.groupValues?.getOrNull(1)
}
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val document = app.get(url).document
val eval = unpackJs(document)?.replace("\\", "") ?: return
val data = AppUtils.tryParseJson<AesData>(Regex("data='(\\S+?)'").first(eval)) ?: return
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
?.split(Regex("\\D+"))
?.joinToString("") {
Char(it.toInt()).toString()
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
?: throw ErrorLoadingException("can't find password")
val decryptedData =
cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "")
?.substringAfter("sources:[")?.substringBefore("],")
Regex("\"file\":\"(\\S+?)\".*?res=(\\d+)").findAll(decryptedData ?: return).map {
it.groupValues[1] to it.groupValues[2]
}.toList().distinctBy { it.second }.map { (link, quality) ->
callback.invoke(
ExtractorLink(
source = this.name,
name = this.name,
url = "${httpsify(link)}&res=$quality",
referer = mainUrl,
quality = quality.toIntOrNull() ?: Qualities.Unknown.value,
headers = mapOf("Range" to "bytes=0-")
)
)
}
}
data class AesData(
@JsonProperty("ct") val ct: String,
@JsonProperty("iv") val iv: String,
@JsonProperty("s") val s: String
)
}

View file

@ -1,12 +1,21 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
class Streamsss : StreamSB() {
override var mainUrl = "https://streamsss.net"
}
class Sbflix : StreamSB() {
override var mainUrl = "https://sbflix.xyz"
override var name = "Sbflix"
}
class Vidgomunime : StreamSB() { class Vidgomunime : StreamSB() {
override var mainUrl = "https://vidgomunime.xyz" override var mainUrl = "https://vidgomunime.xyz"
} }
@ -104,31 +113,33 @@ open class StreamSB : ExtractorApi() {
@JsonProperty("status_code") val statusCode: Int, @JsonProperty("status_code") val statusCode: Int,
) )
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? { override suspend fun getUrl(
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_-]+)") 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 { val id = regexID.findAll(url).map {
it.value.replace(Regex("(embed-|\\/e\\/)"),"") it.value.replace(Regex("(embed-|/e/)"), "")
}.first() }.first()
val bytes = id.toByteArray() // val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
val bytesToHex = bytesToHex(bytes) val master = "$mainUrl/sources48/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
val master = "$mainUrl/sources43/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
val headers = mapOf( val headers = mapOf(
"watchsb" to "streamsb", "watchsb" to "sbstream",
) )
val urltext = app.get(master, val mapped = app.get(
master.lowercase(),
headers = headers, headers = headers,
allowRedirects = false referer = url,
).text ).parsedSafe<Main>()
val mapped = urltext.let { parseJson<Main>(it) }
val testurl = app.get(mapped.streamData.file, headers = headers).text
// val urlmain = mapped.streamData.file.substringBefore("/hls/") // val urlmain = mapped.streamData.file.substringBefore("/hls/")
if (urltext.contains("m3u8") && testurl.contains("EXTM3U")) M3u8Helper.generateM3u8(
return M3u8Helper.generateM3u8( name,
name, mapped?.streamData?.file ?: return,
mapped.streamData.file, url,
url, headers = headers
headers = headers ).forEach(callback)
)
return null
} }
} }

View file

@ -7,6 +7,7 @@ import com.bumptech.glide.load.HttpException
import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.ErrorLoadingException
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.InterruptedIOException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.net.ssl.SSLHandshakeException import javax.net.ssl.SSLHandshakeException
@ -157,7 +158,7 @@ suspend fun <T> safeApiCall(
} }
safeFail(throwable) safeFail(throwable)
} }
is SocketTimeoutException -> { is SocketTimeoutException, is InterruptedIOException -> {
Resource.Failure( Resource.Failure(
true, true,
null, null,
@ -192,7 +193,7 @@ suspend fun <T> safeApiCall(
true, true,
null, null,
null, null,
(throwable.message ?: "SSLHandshakeException") + "\nTry again later." (throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS."
) )
} }
else -> safeFail(throwable) else -> safeFail(throwable)

View file

@ -5,15 +5,12 @@ import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.getCookies
import com.lagradost.nicehttp.ignoreAllSSLErrors import com.lagradost.nicehttp.ignoreAllSSLErrors
import okhttp3.Cache import okhttp3.Cache
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders import okhttp3.Headers.Companion.toHeaders
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit
fun Requests.initClient(context: Context): OkHttpClient { fun Requests.initClient(context: Context): OkHttpClient {

View file

@ -10,6 +10,9 @@ import java.security.MessageDigest
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
object VotingApi { // please do not cheat the votes lol object VotingApi { // please do not cheat the votes lol
private const val LOGKEY = "VotingApi" private const val LOGKEY = "VotingApi"
@ -23,10 +26,10 @@ object VotingApi { // please do not cheat the votes lol
private val apiDomain = "https://api.countapi.xyz" private val apiDomain = "https://api.countapi.xyz"
private fun transformUrl(url: String): String = // dont touch or all votes get reset private fun transformUrl(url: String): String = // dont touch or all votes get reset
MessageDigest MessageDigest
.getInstance("SHA-256") .getInstance("SHA-256")
.digest("${url}#funny-salt".toByteArray()) .digest("${url}#funny-salt".toByteArray())
.fold("") { str, it -> str + "%02x".format(it) } .fold("") { str, it -> str + "%02x".format(it) }
suspend fun SitePlugin.getVotes(): Int { suspend fun SitePlugin.getVotes(): Int {
return getVotes(url) return getVotes(url)
@ -53,9 +56,9 @@ object VotingApi { // please do not cheat the votes lol
return votesCache[pluginUrl] ?: app.get(url).parsedSafe<Result>()?.value?.also { return votesCache[pluginUrl] ?: app.get(url).parsedSafe<Result>()?.value?.also {
votesCache[pluginUrl] = it votesCache[pluginUrl] = it
} ?: (0.also { } ?: (0.also {
ioSafe { ioSafe {
createBucket(pluginUrl) createBucket(pluginUrl)
} }
}) })
} }
@ -64,7 +67,8 @@ object VotingApi { // please do not cheat the votes lol
} }
private suspend fun createBucket(pluginUrl: String) { private suspend fun createBucket(pluginUrl: String) {
val url = "${apiDomain}/create?namespace=cs3-votes&key=${transformUrl(pluginUrl)}&value=0&update_lowerbound=-2&update_upperbound=2&enable_reset=0" val url =
"${apiDomain}/create?namespace=cs3-votes&key=${transformUrl(pluginUrl)}&value=0&update_lowerbound=-2&update_upperbound=2&enable_reset=0"
Log.d(LOGKEY, "Requesting: $url") Log.d(LOGKEY, "Requesting: $url")
app.get(url) app.get(url)
} }
@ -74,32 +78,46 @@ object VotingApi { // please do not cheat the votes lol
return true return true
} }
private val voteLock = Mutex()
suspend fun vote(pluginUrl: String, requestType: VoteType): Int { suspend fun vote(pluginUrl: String, requestType: VoteType): Int {
if (!canVote(pluginUrl)) { // Prevent multiple requests at the same time.
main { voteLock.withLock {
Toast.makeText(context, R.string.extension_install_first, Toast.LENGTH_SHORT).show() if (!canVote(pluginUrl)) {
main {
Toast.makeText(context, R.string.extension_install_first, Toast.LENGTH_SHORT)
.show()
}
return getVotes(pluginUrl)
} }
return getVotes(pluginUrl)
} val savedType: VoteType =
val savedType: VoteType = getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE
var newType: VoteType = requestType
var changeValue = 0 val newType = if (requestType == savedType) VoteType.NONE else requestType
if (requestType == savedType) { val changeValue = if (requestType == savedType) {
newType = VoteType.NONE -requestType.value
changeValue = -requestType.value } else if (savedType == VoteType.NONE) {
} else if (savedType == VoteType.NONE) { requestType.value
changeValue = requestType.value } else if (savedType != requestType) {
} else if (savedType != requestType) { -savedType.value + requestType.value
changeValue = -savedType.value + requestType.value } else 0
}
val url = "${apiDomain}/update/cs3-votes/${transformUrl(pluginUrl)}?amount=${changeValue}" // Pre-emptively set vote key
Log.d(LOGKEY, "Requesting: $url")
val res = app.get(url).parsedSafe<Result>()?.value
if (res != null) {
setKey("cs3-votes/${transformUrl(pluginUrl)}", newType) setKey("cs3-votes/${transformUrl(pluginUrl)}", newType)
votesCache[pluginUrl] = res
val url =
"${apiDomain}/update/cs3-votes/${transformUrl(pluginUrl)}?amount=${changeValue}"
Log.d(LOGKEY, "Requesting: $url")
val res = app.get(url).parsedSafe<Result>()?.value
if (res == null) {
// "Refund" key if the response is invalid
setKey("cs3-votes/${transformUrl(pluginUrl)}", savedType)
} else {
votesCache[pluginUrl] = res
}
return res ?: 0
} }
return res ?: 0
} }
private data class Result( private data class Result(

View file

@ -1,9 +1,11 @@
package com.lagradost.cloudstream3.syncproviders package com.lagradost.cloudstream3.syncproviders
import androidx.fragment.app.FragmentActivity
interface OAuth2API : AuthAPI { interface OAuth2API : AuthAPI {
val key: String val key: String
val redirectUrl: String val redirectUrl: String
suspend fun handleRedirect(url: String) : Boolean suspend fun handleRedirect(url: String) : Boolean
fun authenticate() fun authenticate(activity: FragmentActivity?)
} }

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.json.JsonMapper
@ -48,9 +49,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
removeAccountKeys() removeAccountKeys()
} }
override fun authenticate() { override fun authenticate(activity: FragmentActivity?) {
val request = "https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token" val request = "https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token"
openBrowser(request) openBrowser(request, activity)
} }
override suspend fun handleRedirect(url: String): Boolean { override suspend fun handleRedirect(url: String): Boolean {

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.syncproviders.OAuth2API
@ -15,7 +16,7 @@ class Dropbox : OAuth2API {
override val icon: Int override val icon: Int
get() = TODO("Not yet implemented") get() = TODO("Not yet implemented")
override fun authenticate() { override fun authenticate(activity: FragmentActivity?) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Base64 import android.util.Base64
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
@ -281,7 +282,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return false return false
} }
override fun authenticate() { override fun authenticate(activity: FragmentActivity?) {
// It is recommended to use a URL-safe string as code_verifier. // It is recommended to use a URL-safe string as code_verifier.
// See section 4 of RFC 7636 for more details. // See section 4 of RFC 7636 for more details.
@ -294,7 +295,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val codeChallenge = codeVerifier val codeChallenge = codeVerifier
val request = val request =
"$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId" "$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId"
openBrowser(request) openBrowser(request, activity)
} }
private var requestId = 0 private var requestId = 0

View file

@ -11,12 +11,12 @@ import android.webkit.WebViewClient
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.network.WebViewResolver
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
import kotlinx.android.synthetic.main.fragment_webview.* import kotlinx.android.synthetic.main.fragment_webview.*
import java.net.URI
class WebviewFragment : Fragment() { class WebviewFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -31,16 +31,8 @@ class WebviewFragment : Fragment() {
request: WebResourceRequest? request: WebResourceRequest?
): Boolean { ): Boolean {
val requestUrl = request?.url.toString() val requestUrl = request?.url.toString()
val repoUrl = if (requestUrl.startsWith("https://cs.repo")) { val performedAction = MainActivity.handleAppIntentUrl(activity, requestUrl, true)
"https://" + requestUrl.substringAfter("?") if (performedAction) {
} else if (URI(requestUrl).scheme == appStringRepo) {
requestUrl.replaceFirst(appStringRepo, "https")
} else {
null
}
if (repoUrl != null) {
activity?.loadRepository(repoUrl)
findNavController().popBackStack() findNavController().popBackStack()
return true return true
} }
@ -50,6 +42,7 @@ class WebviewFragment : Fragment() {
} }
web_view.addJavascriptInterface(RepoApi(activity), "RepoApi") web_view.addJavascriptInterface(RepoApi(activity), "RepoApi")
web_view.settings.javaScriptEnabled = true web_view.settings.javaScriptEnabled = true
web_view.settings.userAgentString = USER_AGENT
web_view.settings.domStorageEnabled = true web_view.settings.domStorageEnabled = true
WebViewResolver.webViewUserAgent = web_view.settings.userAgentString WebViewResolver.webViewUserAgent = web_view.settings.userAgentString

View file

@ -272,12 +272,13 @@ class HomeViewModel : ViewModel() {
if (!forceReload && api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true) { if (!forceReload && api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true) {
return@launchSafe return@launchSafe
} }
// If the plugin isn't loaded yet. (Does not set the key)
if (api == null) { if (preferredApiName == noneApi.name) {
loadAndCancel(noneApi)
} else if (preferredApiName == noneApi.name) {
setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name)
loadAndCancel(noneApi) loadAndCancel(noneApi)
// If the plugin isn't loaded yet. (Does not set the key)
} else if (api == null) {
loadAndCancel(noneApi)
} else if (preferredApiName == randomApi.name) { } else if (preferredApiName == randomApi.name) {
val validAPIs = context?.filterProviderByPreferredMedia() val validAPIs = context?.filterProviderByPreferredMedia()
if (validAPIs.isNullOrEmpty()) { if (validAPIs.isNullOrEmpty()) {

View file

@ -611,6 +611,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player_lock?.isGone = !isShowing player_lock?.isGone = !isShowing
//player_media_route_button?.isClickable = !isGone //player_media_route_button?.isClickable = !isGone
player_go_back_holder?.isGone = isGone player_go_back_holder?.isGone = isGone
player_sources_btt?.isGone = isGone
} }
private fun updateLockUI() { private fun updateLockUI() {

View file

@ -1483,15 +1483,20 @@ class ResultViewModel2 : ViewModel() {
0 -> txt(R.string.no_season) 0 -> txt(R.string.no_season)
else -> { else -> {
val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames
val seasonData = val seasonData = seasonNames.getSeason(indexer.season)
seasonNames.getSeason(indexer.season)
val suffix = seasonData?.name?.let { " $it" } ?: "" // If displaySeason is null then only show the name!
txt( if (seasonData?.name != null && seasonData.displaySeason == null) {
R.string.season_format, txt(seasonData.name)
txt(R.string.season), } else {
seasonData?.displaySeason ?: indexer.season, val suffix = seasonData?.name?.let { " $it" } ?: ""
suffix txt(
) R.string.season_format,
txt(R.string.season),
seasonData?.displaySeason ?: indexer.season,
suffix
)
}
} }
} }
) )
@ -1587,8 +1592,8 @@ class ResultViewModel2 : ViewModel() {
val idIndex = ep.key.id val idIndex = ep.key.id
for ((index, i) in ep.value.withIndex()) { for ((index, i) in ep.value.withIndex()) {
val episode = i.episode ?: (index + 1) val episode = i.episode ?: (index + 1)
val id = mainId + episode + idIndex * 1000000 val id = mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000) ?: 0)
if (!existingEpisodes.contains(episode)) { if (!existingEpisodes.contains(id)) {
existingEpisodes.add(id) existingEpisodes.add(id)
val seasonData = loadResponse.seasonNames.getSeason(i.season) val seasonData = loadResponse.seasonNames.getSeason(i.season)
val eps = val eps =
@ -1597,8 +1602,8 @@ class ResultViewModel2 : ViewModel() {
filterName(i.name), filterName(i.name),
i.posterUrl, i.posterUrl,
episode, episode,
null, seasonData?.season ?: i.season,
seasonData?.displaySeason ?: i.season, if (seasonData != null) seasonData.displaySeason else i.season,
i.data, i.data,
loadResponse.apiName, loadResponse.apiName,
id, id,
@ -1610,7 +1615,7 @@ class ResultViewModel2 : ViewModel() {
mainId mainId
) )
val season = eps.season ?: 0 val season = eps.seasonIndex ?: 0
val indexer = EpisodeIndexer(ep.key, season) val indexer = EpisodeIndexer(ep.key, season)
episodes[indexer]?.add(eps) ?: run { episodes[indexer]?.add(eps) ?: run {
episodes[indexer] = mutableListOf(eps) episodes[indexer] = mutableListOf(eps)
@ -1625,15 +1630,14 @@ class ResultViewModel2 : ViewModel() {
mutableMapOf() mutableMapOf()
val existingEpisodes = HashSet<Int>() val existingEpisodes = HashSet<Int>()
for ((index, episode) in loadResponse.episodes.sortedBy { for ((index, episode) in loadResponse.episodes.sortedBy {
(it.season?.times(10000) ?: 0) + (it.episode ?: 0) (it.season?.times(10_000) ?: 0) + (it.episode ?: 0)
}.withIndex()) { }.withIndex()) {
val episodeIndex = episode.episode ?: (index + 1) val episodeIndex = episode.episode ?: (index + 1)
val id = val id =
mainId + (episode.season?.times(100000) ?: 0) + episodeIndex + 1 mainId + (episode.season?.times(100_000) ?: 0) + episodeIndex + 1
if (!existingEpisodes.contains(id)) { if (!existingEpisodes.contains(id)) {
existingEpisodes.add(id) existingEpisodes.add(id)
val seasonIndex = episode.season?.minus(1) val seasonData =
val currentSeason =
loadResponse.seasonNames.getSeason(episode.season) loadResponse.seasonNames.getSeason(episode.season)
val ep = val ep =
@ -1642,8 +1646,8 @@ class ResultViewModel2 : ViewModel() {
filterName(episode.name), filterName(episode.name),
episode.posterUrl, episode.posterUrl,
episodeIndex, episodeIndex,
seasonIndex, seasonData?.season ?: episode.season,
currentSeason?.displaySeason ?: episode.season, if (seasonData != null) seasonData.displaySeason else episode.season,
episode.data, episode.data,
loadResponse.apiName, loadResponse.apiName,
id, id,
@ -1655,7 +1659,7 @@ class ResultViewModel2 : ViewModel() {
mainId mainId
) )
val season = episode.season ?: 0 val season = ep.seasonIndex ?: 0
val indexer = EpisodeIndexer(DubStatus.None, season) val indexer = EpisodeIndexer(DubStatus.None, season)
episodes[indexer]?.add(ep) ?: kotlin.run { episodes[indexer]?.add(ep) ?: kotlin.run {
@ -1747,16 +1751,17 @@ class ResultViewModel2 : ViewModel() {
val seasonData = loadResponse.seasonNames.getSeason(seasonNumber) val seasonData = loadResponse.seasonNames.getSeason(seasonNumber)
val fixedSeasonNumber = seasonData?.displaySeason ?: seasonNumber val fixedSeasonNumber = seasonData?.displaySeason ?: seasonNumber
val suffix = seasonData?.name?.let { " $it" } ?: "" val suffix = seasonData?.name?.let { " $it" } ?: ""
// If displaySeason is null then only show the name!
val name = val name = if (seasonData?.name != null && seasonData.displaySeason == null) {
/*loadResponse.seasonNames?.firstOrNull { it.season == seasonNumber }?.name?.let { seasonData -> txt(seasonData.name)
txt(seasonData) } else {
} ?:*/txt( txt(
R.string.season_format, R.string.season_format,
txt(R.string.season), txt(R.string.season),
fixedSeasonNumber, fixedSeasonNumber,
suffix suffix
) )
}
name to seasonNumber name to seasonNumber
}) })
} }
@ -1812,7 +1817,12 @@ class ResultViewModel2 : ViewModel() {
} }
private fun loadTrailers(loadResponse: LoadResponse) = ioSafe { private fun loadTrailers(loadResponse: LoadResponse) = ioSafe {
_trailers.postValue(getTrailers(loadResponse, 3)) // we dont want to fetch too many trailers _trailers.postValue(
getTrailers(
loadResponse,
3
)
) // we dont want to fetch too many trailers
} }
private suspend fun getTrailers( private suspend fun getTrailers(

View file

@ -1,8 +1,5 @@
package com.lagradost.cloudstream3.ui.settings package com.lagradost.cloudstream3.ui.settings
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.View.* import android.view.View.*
@ -12,8 +9,10 @@ import androidx.annotation.UiThread
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -39,7 +38,11 @@ import kotlinx.android.synthetic.main.add_account_input.*
class SettingsAccount : PreferenceFragmentCompat() { class SettingsAccount : PreferenceFragmentCompat() {
companion object { companion object {
/** Used by nginx plugin too */ /** Used by nginx plugin too */
fun showLoginInfo(activity: Activity?, api: AccountManager, info: AuthAPI.LoginInfo) { fun showLoginInfo(
activity: FragmentActivity?,
api: AccountManager,
info: AuthAPI.LoginInfo
) {
val builder = val builder =
AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom) AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom)
.setView(R.layout.account_managment) .setView(R.layout.account_managment)
@ -62,9 +65,13 @@ class SettingsAccount : PreferenceFragmentCompat() {
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
showAccountSwitch(activity, api) showAccountSwitch(activity, api)
} }
if (isTvSettings()) {
dialog.account_switch_account?.requestFocus()
}
} }
fun showAccountSwitch(activity: Activity, api: AccountManager) { fun showAccountSwitch(activity: FragmentActivity, api: AccountManager) {
val accounts = api.getAccounts() ?: return val accounts = api.getAccounts() ?: return
val builder = val builder =
@ -98,11 +105,11 @@ class SettingsAccount : PreferenceFragmentCompat() {
} }
@UiThread @UiThread
fun addAccount(activity: Activity?, api: AccountManager) { fun addAccount(activity: FragmentActivity?, api: AccountManager) {
try { try {
when (api) { when (api) {
is OAuth2API -> { is OAuth2API -> {
api.authenticate() api.authenticate(activity)
} }
is InAppAuthAPI -> { is InAppAuthAPI -> {
val builder = val builder =
@ -144,13 +151,11 @@ class SettingsAccount : PreferenceFragmentCompat() {
dialog.login_username_input?.isVisible = api.requiresUsername dialog.login_username_input?.isVisible = api.requiresUsername
dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank() dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank()
dialog.create_account?.setOnClickListener { dialog.create_account?.setOnClickListener {
val i = Intent(Intent.ACTION_VIEW) openBrowser(
i.data = Uri.parse(api.createAccountUrl) api.createAccountUrl ?: return@setOnClickListener,
try { activity
activity.startActivity(i) )
} catch (e: Exception) { dialog.dismissSafe()
logError(e)
}
} }
dialog.text1?.text = api.name dialog.text1?.text = api.name
@ -181,9 +186,10 @@ class SettingsAccount : PreferenceFragmentCompat() {
try { try {
showToast( showToast(
activity, activity,
activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format( activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail)
api.name .format(
) api.name
)
) )
} catch (e: Exception) { } catch (e: Exception) {
logError(e) // format might fail logError(e) // format might fail

View file

@ -76,7 +76,7 @@ class RepoAdapter(
imageClickCallback(repositoryData) imageClickCallback(repositoryData)
} }
itemView.setOnClickListener { itemView.repository_item_root?.setOnClickListener {
clickCallback(repositoryData) clickCallback(repositoryData)
} }
itemView.main_text?.text = repositoryData.name itemView.main_text?.text = repositoryData.name

View file

@ -31,6 +31,7 @@ import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.text.toSpanned import androidx.core.text.toSpanned
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -415,7 +416,7 @@ object AppUtils {
} }
} }
fun AppCompatActivity.loadResult( fun FragmentActivity.loadResult(
url: String, url: String,
apiName: String, apiName: String,
startAction: Int = 0, startAction: Int = 0,

View file

@ -236,6 +236,8 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Ssbstream(), Ssbstream(),
Sbthe(), Sbthe(),
Vidgomunime(), Vidgomunime(),
Sbflix(),
Streamsss(),
Fastream(), Fastream(),
@ -269,6 +271,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
DoodWsExtractor(), DoodWsExtractor(),
DoodShExtractor(), DoodShExtractor(),
DoodWatchExtractor(), DoodWatchExtractor(),
DoodWfExtractor(),
AsianLoad(), AsianLoad(),
@ -321,6 +324,18 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Mvidoo(), Mvidoo(),
Streamplay(), Streamplay(),
Gdriveplayerapi(),
Gdriveplayerapp(),
Gdriveplayerfun(),
Gdriveplayerio(),
Gdriveplayerme(),
Gdriveplayerbiz(),
Gdriveplayerorg(),
Gdriveplayerus(),
Gdriveplayerco(),
Gdriveplayer(),
DatabaseGdrive(),
YoutubeExtractor(), YoutubeExtractor(),
YoutubeShortLinkExtractor(), YoutubeShortLinkExtractor(),
YoutubeMobileExtractor(), YoutubeMobileExtractor(),

View file

@ -200,16 +200,23 @@ class InAppUpdater {
private suspend fun Activity.downloadUpdate(url: String): Boolean { private suspend fun Activity.downloadUpdate(url: String): Boolean {
try { try {
Log.d(LOG_TAG, "Downloading update: $url") Log.d(LOG_TAG, "Downloading update: $url")
val appUpdateName = "CloudStream"
val appUpdateSuffix = "apk"
val localContext = this // Delete all old updates
this.cacheDir.listFiles()?.filter {
it.name.startsWith(appUpdateName) && it.extension == appUpdateSuffix
}?.forEach {
it.deleteOnExit()
}
val downloadedFile = File.createTempFile("CloudStream", ".apk") val downloadedFile = File.createTempFile(appUpdateName, ".$appUpdateSuffix")
val sink: BufferedSink = downloadedFile.sink().buffer() val sink: BufferedSink = downloadedFile.sink().buffer()
updateLock.withLock { updateLock.withLock {
sink.writeAll(app.get(url).body.source()) sink.writeAll(app.get(url).body.source())
sink.close() sink.close()
openApk(localContext, Uri.fromFile(downloadedFile)) openApk(this, Uri.fromFile(downloadedFile))
} }
return true return true
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -1,131 +1,134 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="match_parent"
android:layout_height="match_parent"> android:orientation="vertical">
<FrameLayout
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_rowWeight="1"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
tools:text="Test" />
<com.google.android.material.button.MaterialButton
style="@style/WhiteButton"
android:layout_gravity="center_vertical|end"
app:icon="@drawable/ic_baseline_add_24"
android:text="@string/create_account"
android:id="@+id/create_account"
android:layout_width="wrap_content" />
</FrameLayout>
<LinearLayout <LinearLayout
android:orientation="vertical" android:layout_width="match_parent"
android:layout_marginBottom="60dp" android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp" android:layout_marginTop="20dp"
android:layout_width="match_parent" android:layout_marginBottom="10dp"
android:layout_height="wrap_content"> android:orientation="horizontal">
<EditText <TextView
android:textColorHint="?attr/grayTextColor" android:id="@+id/text1"
android:hint="@string/example_username" android:layout_weight="1"
android:autofillHints="username" android:layout_width="0dp"
android:id="@+id/login_username_input" android:layout_height="wrap_content"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusDown="@id/login_email_input"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
tools:ignore="LabelFor" />
<EditText android:layout_rowWeight="1"
android:textColorHint="?attr/grayTextColor" android:layout_gravity="center_vertical"
android:autofillHints="emailAddress"
android:hint="@string/example_email"
android:id="@+id/login_email_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusUp="@id/login_username_input"
android:nextFocusDown="@id/login_server_input"
android:requiresFadingEdge="vertical" android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:layout_width="match_parent" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:layout_height="wrap_content" android:textColor="?attr/textColor"
android:inputType="textEmailAddress" android:textSize="20sp"
tools:ignore="LabelFor" /> android:textStyle="bold"
tools:text="Test" />
<EditText <com.google.android.material.button.MaterialButton
android:textColorHint="?attr/grayTextColor" android:id="@+id/create_account"
android:hint="@string/example_ip" style="@style/WhiteButton"
android:id="@+id/login_server_input" android:layout_width="wrap_content"
android:nextFocusRight="@id/cancel_btt" android:layout_gravity="center_vertical|end"
android:nextFocusLeft="@id/apply_btt" android:text="@string/create_account"
android:nextFocusUp="@id/login_email_input" app:icon="@drawable/ic_baseline_add_24" />
android:nextFocusDown="@id/login_password_input"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
tools:ignore="LabelFor" />
<EditText
android:textColorHint="?attr/grayTextColor"
android:hint="@string/example_password"
android:id="@+id/login_password_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusUp="@id/login_server_input"
android:nextFocusDown="@id/apply_btt"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textVisiblePassword"
tools:ignore="LabelFor"
android:autofillHints="password" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/apply_btt_holder" android:layout_width="match_parent"
android:orientation="horizontal" android:layout_height="wrap_content"
android:layout_gravity="bottom" android:layout_marginHorizontal="10dp"
android:gravity="bottom|end" android:layout_marginBottom="60dp"
android:layout_marginTop="-60dp" android:orientation="vertical">
<EditText
android:id="@+id/login_username_input"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="60dp"> android:layout_height="wrap_content"
android:autofillHints="username"
android:hint="@string/example_username"
android:inputType="text"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusDown="@id/login_email_input"
android:requiresFadingEdge="vertical"
android:textColorHint="?attr/grayTextColor"
tools:ignore="LabelFor" />
<EditText
android:id="@+id/login_email_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="emailAddress"
android:hint="@string/example_email"
android:inputType="textEmailAddress"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusUp="@id/login_username_input"
android:nextFocusDown="@id/login_server_input"
android:requiresFadingEdge="vertical"
android:textColorHint="?attr/grayTextColor"
tools:ignore="LabelFor" />
<EditText
android:id="@+id/login_server_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/example_ip"
android:inputType="textUri"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusUp="@id/login_email_input"
android:nextFocusDown="@id/login_password_input"
android:requiresFadingEdge="vertical"
android:textColorHint="?attr/grayTextColor"
tools:ignore="LabelFor" />
<EditText
android:id="@+id/login_password_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="password"
android:hint="@string/example_password"
android:inputType="textVisiblePassword"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusUp="@id/login_server_input"
android:nextFocusDown="@id/apply_btt"
android:requiresFadingEdge="vertical"
android:textColorHint="?attr/grayTextColor"
tools:ignore="LabelFor" />
</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:gravity="bottom|end"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
style="@style/WhiteButton" android:id="@+id/apply_btt"
android:layout_gravity="center_vertical|end" style="@style/WhiteButton"
android:text="@string/login" android:layout_width="wrap_content"
android:id="@+id/apply_btt" android:layout_gravity="center_vertical|end"
android:layout_width="wrap_content" /> android:text="@string/login" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
style="@style/BlackButton" android:id="@+id/cancel_btt"
android:layout_gravity="center_vertical|end" style="@style/BlackButton"
android:text="@string/sort_cancel" android:layout_width="wrap_content"
android:id="@+id/cancel_btt" android:layout_gravity="center_vertical|end"
android:layout_width="wrap_content" /> android:text="@string/sort_cancel" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View file

@ -102,18 +102,21 @@
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/home_select_cartoons" android:nextFocusLeft="@id/home_select_cartoons"
android:nextFocusRight="@id/home_select_livestreams"
android:text="@string/documentaries" /> android:text="@string/documentaries" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/home_select_livestreams" android:id="@+id/home_select_livestreams"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/home_select_documentaries" android:nextFocusLeft="@id/home_select_documentaries"
android:nextFocusRight="@id/home_select_nsfw"
android:text="@string/livestreams" /> android:text="@string/livestreams" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/home_select_nsfw" android:id="@+id/home_select_nsfw"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/home_select_livestreams" android:nextFocusLeft="@id/home_select_livestreams"
android:nextFocusRight="@id/home_select_nsfw"
android:text="@string/nsfw" /> android:text="@string/nsfw" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton

View file

@ -136,21 +136,23 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/search_select_documentaries" android:id="@+id/search_select_documentaries"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/search_select_cartoons" android:nextFocusLeft="@id/search_select_cartoons"
android:nextFocusRight="@id/search_select_livestreams"
android:text="@string/documentaries" /> android:text="@string/documentaries" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/search_select_livestreams" android:id="@+id/search_select_livestreams"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/search_select_documentaries" android:nextFocusLeft="@id/search_select_documentaries"
android:nextFocusRight="@id/search_select_nsfw"
android:text="@string/livestreams" /> android:text="@string/livestreams" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/search_select_nsfw" android:id="@+id/search_select_nsfw"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/search_select_livestreams" android:nextFocusLeft="@id/search_select_livestreams"
android:nextFocusRight="@id/search_select_others"
android:text="@string/nsfw" /> android:text="@string/nsfw" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton

View file

@ -31,7 +31,7 @@
android:paddingEnd="10dp" android:paddingEnd="10dp"
android:requiresFadingEdge="horizontal"> android:requiresFadingEdge="horizontal">
<!-- Man what the fuck we need a recyclerview --> <!-- Man what the fuck we need a recyclerview -->
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -93,21 +93,23 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/home_select_documentaries" android:id="@+id/home_select_documentaries"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/home_select_cartoons" android:nextFocusLeft="@id/home_select_cartoons"
android:nextFocusRight="@id/home_select_livestreams"
android:text="@string/documentaries" /> android:text="@string/documentaries" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/home_select_livestreams" android:id="@+id/home_select_livestreams"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/home_select_documentaries" android:nextFocusLeft="@id/home_select_documentaries"
android:nextFocusRight="@id/home_select_nsfw"
android:text="@string/livestreams" /> android:text="@string/livestreams" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/home_select_nsfw" android:id="@+id/home_select_nsfw"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/home_select_livestreams" android:nextFocusLeft="@id/home_select_livestreams"
android:nextFocusRight="@id/home_select_others"
android:text="@string/nsfw" /> android:text="@string/nsfw" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton

View file

@ -101,7 +101,7 @@
<TextView <TextView
android:id="@+id/settings_extensions" android:id="@+id/settings_extensions"
style="@style/SettingsItem" style="@style/SettingsItem"
android:nextFocusUp="@id/settings_updates" android:nextFocusUp="@id/settings_credits"
android:text="@string/extensions" /> android:text="@string/extensions" />
<LinearLayout <LinearLayout

View file

@ -8,6 +8,8 @@
android:background="@drawable/outline_drawable" android:background="@drawable/outline_drawable"
android:nextFocusRight="@id/action_button" android:nextFocusRight="@id/action_button"
android:orientation="horizontal" android:orientation="horizontal"
android:clickable="true"
android:focusable="true"
android:padding="12dp"> android:padding="12dp">
<ImageView <ImageView

View file

@ -20,7 +20,7 @@
<!-- TRANSLATE, BUT DON'T FORGET FORMAT --> <!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
<string name="player_speed_text_format" formatted="true">Snelheid (%.2fx)</string> <string name="player_speed_text_format" formatted="true">Snelheid (%.2fx)</string>
<string name="rated_format" formatted="true">Beoordeeld: %.Als</string> <string name="rated_format" formatted="true">Beoordeeld: %.1fAls</string>
<string name="new_update_format" formatted="true">Nieuwe update gevonden!\n%s -> %s</string> <string name="new_update_format" formatted="true">Nieuwe update gevonden!\n%s -> %s</string>
<string name="filler" formatted="true">Filler</string> <string name="filler" formatted="true">Filler</string>
<string name="duration_format" formatted="true">%d min</string> <string name="duration_format" formatted="true">%d min</string>

View file

@ -1,5 +0,0 @@
files:
- source: /app/src/main/res/values/strings.xml
translation: /app/src/main/res/values-%android_code%/strings.xml
- source: /app/src/main/res/values/array.xml
translation: /app/src/main/res/values-%android_code%/array.xml