Fixes and merge with main
This commit is contained in:
commit
da94ffa7b9
|
@ -184,8 +184,8 @@ dependencies {
|
|||
//implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
|
||||
|
||||
// Downloading
|
||||
implementation("androidx.work:work-runtime:2.7.1")
|
||||
implementation("androidx.work:work-runtime-ktx:2.7.1")
|
||||
implementation("androidx.work:work-runtime:2.8.0")
|
||||
implementation("androidx.work:work-runtime-ktx:2.8.0")
|
||||
|
||||
// Networking
|
||||
// implementation("com.squareup.okhttp3:okhttp:4.9.2")
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package com.lagradost.cloudstream3
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||
import com.lagradost.cloudstream3.utils.TestingUtils
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
@ -16,142 +15,11 @@ import org.junit.runner.RunWith
|
|||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
//@Test
|
||||
//fun useAppContext() {
|
||||
// // Context of the app under test.
|
||||
// val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
// assertEquals("com.lagradost.cloudstream3", appContext.packageName)
|
||||
//}
|
||||
|
||||
private fun getAllProviders(): List<MainAPI> {
|
||||
println("Providers: ${APIHolder.allProviders.size}")
|
||||
return APIHolder.allProviders //.filter { !it.usesWebView }
|
||||
}
|
||||
|
||||
private suspend fun loadLinks(api: MainAPI, url: String?): Boolean {
|
||||
Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
|
||||
if (url == null) return true
|
||||
var linksLoaded = 0
|
||||
try {
|
||||
val success = api.loadLinks(url, false, {}) { link ->
|
||||
Assert.assertTrue(
|
||||
"Api ${api.name} returns link with invalid Quality",
|
||||
Qualities.values().map { it.value }.contains(link.quality)
|
||||
)
|
||||
Assert.assertTrue(
|
||||
"Api ${api.name} returns link with invalid url ${link.url}",
|
||||
link.url.length > 4
|
||||
)
|
||||
linksLoaded++
|
||||
}
|
||||
if (success) {
|
||||
return linksLoaded > 0
|
||||
}
|
||||
Assert.assertTrue("Api ${api.name} has returns false on .loadLinks", success)
|
||||
} catch (e: Exception) {
|
||||
if (e.cause is NotImplementedError) {
|
||||
Assert.fail("Provider has not implemented .loadLinks")
|
||||
}
|
||||
logError(e)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun testSingleProviderApi(api: MainAPI): Boolean {
|
||||
val searchQueries = listOf("over", "iron", "guy")
|
||||
var correctResponses = 0
|
||||
var searchResult: List<SearchResponse>? = null
|
||||
for (query in searchQueries) {
|
||||
val response = try {
|
||||
api.search(query)
|
||||
} catch (e: Exception) {
|
||||
if (e.cause is NotImplementedError) {
|
||||
Assert.fail("Provider has not implemented .search")
|
||||
}
|
||||
logError(e)
|
||||
null
|
||||
}
|
||||
if (!response.isNullOrEmpty()) {
|
||||
correctResponses++
|
||||
if (searchResult == null) {
|
||||
searchResult = response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (correctResponses == 0 || searchResult == null) {
|
||||
System.err.println("Api ${api.name} did not return any valid search responses")
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
var validResults = false
|
||||
for (result in searchResult) {
|
||||
Assert.assertEquals(
|
||||
"Invalid apiName on response on ${api.name}",
|
||||
result.apiName,
|
||||
api.name
|
||||
)
|
||||
val load = api.load(result.url) ?: continue
|
||||
Assert.assertEquals(
|
||||
"Invalid apiName on load on ${api.name}",
|
||||
load.apiName,
|
||||
result.apiName
|
||||
)
|
||||
Assert.assertTrue(
|
||||
"Api ${api.name} on load does not contain any of the supportedTypes",
|
||||
api.supportedTypes.contains(load.type)
|
||||
)
|
||||
when (load) {
|
||||
is AnimeLoadResponse -> {
|
||||
val gotNoEpisodes =
|
||||
load.episodes.keys.isEmpty() || load.episodes.keys.any { load.episodes[it].isNullOrEmpty() }
|
||||
|
||||
if (gotNoEpisodes) {
|
||||
println("Api ${api.name} got no episodes on ${load.url}")
|
||||
continue
|
||||
}
|
||||
|
||||
val url = (load.episodes[load.episodes.keys.first()])?.first()?.data
|
||||
validResults = loadLinks(api, url)
|
||||
if (!validResults) continue
|
||||
}
|
||||
is MovieLoadResponse -> {
|
||||
val gotNoEpisodes = load.dataUrl.isBlank()
|
||||
if (gotNoEpisodes) {
|
||||
println("Api ${api.name} got no movie on ${load.url}")
|
||||
continue
|
||||
}
|
||||
|
||||
validResults = loadLinks(api, load.dataUrl)
|
||||
if (!validResults) continue
|
||||
}
|
||||
is TvSeriesLoadResponse -> {
|
||||
val gotNoEpisodes = load.episodes.isEmpty()
|
||||
if (gotNoEpisodes) {
|
||||
println("Api ${api.name} got no episodes on ${load.url}")
|
||||
continue
|
||||
}
|
||||
|
||||
validResults = loadLinks(api, load.episodes.first().data)
|
||||
if (!validResults) continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
if (!validResults) {
|
||||
System.err.println("Api ${api.name} did not load on any")
|
||||
}
|
||||
|
||||
return validResults
|
||||
} catch (e: Exception) {
|
||||
if (e.cause is NotImplementedError) {
|
||||
Assert.fail("Provider has not implemented .load")
|
||||
}
|
||||
logError(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun providersExist() {
|
||||
Assert.assertTrue(getAllProviders().isNotEmpty())
|
||||
|
@ -159,6 +27,7 @@ class ExampleInstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@Throws(AssertionError::class)
|
||||
fun providerCorrectData() {
|
||||
val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 }
|
||||
Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty())
|
||||
|
@ -181,67 +50,20 @@ class ExampleInstrumentedTest {
|
|||
fun providerCorrectHomepage() {
|
||||
runBlocking {
|
||||
getAllProviders().amap { api ->
|
||||
if (api.hasMainPage) {
|
||||
try {
|
||||
val f = api.mainPage.first()
|
||||
val homepage =
|
||||
api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages))
|
||||
when {
|
||||
homepage == null -> {
|
||||
System.err.println("Homepage provider ${api.name} did not correctly load homepage!")
|
||||
}
|
||||
homepage.items.isEmpty() -> {
|
||||
System.err.println("Homepage provider ${api.name} does not contain any items!")
|
||||
}
|
||||
homepage.items.any { it.list.isEmpty() } -> {
|
||||
System.err.println("Homepage provider ${api.name} does not have any items on result!")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e.cause is NotImplementedError) {
|
||||
Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented")
|
||||
}
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
TestingUtils.testHomepage(api, ::println)
|
||||
}
|
||||
}
|
||||
println("Done providerCorrectHomepage")
|
||||
}
|
||||
|
||||
// @Test
|
||||
// fun testSingleProvider() {
|
||||
// testSingleProviderApi(ThenosProvider())
|
||||
// }
|
||||
|
||||
@Test
|
||||
fun providerCorrect() {
|
||||
fun testAllProvidersCorrect() {
|
||||
runBlocking {
|
||||
val invalidProvider = ArrayList<Pair<MainAPI, Exception?>>()
|
||||
val providers = getAllProviders()
|
||||
providers.amap { api ->
|
||||
try {
|
||||
println("Trying $api")
|
||||
if (testSingleProviderApi(api)) {
|
||||
println("Success $api")
|
||||
} else {
|
||||
System.err.println("Error $api")
|
||||
invalidProvider.add(Pair(api, null))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
invalidProvider.add(Pair(api, e))
|
||||
}
|
||||
}
|
||||
if (invalidProvider.isEmpty()) {
|
||||
println("No Invalid providers! :D")
|
||||
} else {
|
||||
println("Invalid providers are: ")
|
||||
for (provider in invalidProvider) {
|
||||
println("${provider.first}")
|
||||
}
|
||||
}
|
||||
TestingUtils.getDeferredProviderTests(
|
||||
this,
|
||||
getAllProviders(),
|
||||
::println
|
||||
) { _, _ -> }
|
||||
}
|
||||
println("Done providerCorrect")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,10 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
|||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||
import okhttp3.Interceptor
|
||||
import org.mozilla.javascript.Scriptable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.absoluteValue
|
||||
|
@ -734,6 +736,19 @@ fun fixTitle(str: String): String {
|
|||
.replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it }
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get rhino context in a safe way as it needs to be initialized on the main thread.
|
||||
* Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects()
|
||||
* Use like the following: rhino.evaluateString(scope, js, "JavaScript", 1, null)
|
||||
**/
|
||||
suspend fun getRhinoContext(): org.mozilla.javascript.Context {
|
||||
return Coroutines.mainWork {
|
||||
val rhino = org.mozilla.javascript.Context.enter()
|
||||
rhino.initSafeStandardObjects()
|
||||
rhino.optimizationLevel = -1
|
||||
rhino
|
||||
}
|
||||
}
|
||||
|
||||
/** https://www.imdb.com/title/tt2861424/ -> tt2861424 */
|
||||
fun imdbUrlToId(url: String): String? {
|
||||
|
@ -1312,7 +1327,7 @@ fun LoadResponse?.isAnimeBased(): Boolean {
|
|||
|
||||
fun TvType?.isEpisodeBased(): Boolean {
|
||||
if (this == null) return false
|
||||
return (this == TvType.TvSeries || this == TvType.Anime)
|
||||
return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama)
|
||||
}
|
||||
|
||||
|
||||
|
@ -1336,6 +1351,7 @@ interface EpisodeResponse {
|
|||
var showStatus: ShowStatus?
|
||||
var nextAiring: NextAiring?
|
||||
var seasonNames: List<SeasonData>?
|
||||
fun getLatestEpisodes(): Map<DubStatus, Int?>
|
||||
}
|
||||
|
||||
@JvmName("addSeasonNamesString")
|
||||
|
@ -1404,7 +1420,18 @@ data class AnimeLoadResponse(
|
|||
override var nextAiring: NextAiring? = null,
|
||||
override var seasonNames: List<SeasonData>? = null,
|
||||
override var backgroundPosterUrl: String? = null,
|
||||
) : LoadResponse, EpisodeResponse
|
||||
) : LoadResponse, EpisodeResponse {
|
||||
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
|
||||
return episodes.map { (status, episodes) ->
|
||||
val maxSeason = episodes.maxOfOrNull { it.season ?: Int.MIN_VALUE }
|
||||
.takeUnless { it == Int.MIN_VALUE }
|
||||
status to episodes
|
||||
.filter { it.season == maxSeason }
|
||||
.maxOfOrNull { it.episode ?: Int.MIN_VALUE }
|
||||
.takeUnless { it == Int.MIN_VALUE }
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If episodes already exist appends the list.
|
||||
|
@ -1602,7 +1629,17 @@ data class TvSeriesLoadResponse(
|
|||
override var nextAiring: NextAiring? = null,
|
||||
override var seasonNames: List<SeasonData>? = null,
|
||||
override var backgroundPosterUrl: String? = null,
|
||||
) : LoadResponse, EpisodeResponse
|
||||
) : LoadResponse, EpisodeResponse {
|
||||
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
|
||||
val maxSeason =
|
||||
episodes.maxOfOrNull { it.season ?: Int.MIN_VALUE }.takeUnless { it == Int.MIN_VALUE }
|
||||
val max = episodes
|
||||
.filter { it.season == maxSeason }
|
||||
.maxOfOrNull { it.episode ?: Int.MIN_VALUE }
|
||||
.takeUnless { it == Int.MIN_VALUE }
|
||||
return mapOf(DubStatus.None to max)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun MainAPI.newTvSeriesLoadResponse(
|
||||
name: String,
|
||||
|
|
|
@ -32,7 +32,9 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|||
import com.google.android.gms.cast.framework.*
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.navigationrail.NavigationRailView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.APIHolder.allProviders
|
||||
import com.lagradost.cloudstream3.APIHolder.apis
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||
|
@ -79,6 +81,7 @@ import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
|
|||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.html
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||
|
@ -86,6 +89,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
|||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
|
||||
|
@ -402,6 +406,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
R.id.navigation_settings_general,
|
||||
R.id.navigation_settings_extensions,
|
||||
R.id.navigation_settings_plugins,
|
||||
R.id.navigation_test_providers,
|
||||
).contains(destination.id)
|
||||
|
||||
|
||||
|
@ -716,6 +721,28 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
|
||||
changeStatusBarState(isEmulatorSettings())
|
||||
|
||||
// Automatically enable jsdelivr if cant connect to raw.githubusercontent.com
|
||||
if (this.getKey<Boolean>(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) {
|
||||
main {
|
||||
if (checkGithubConnectivity()) {
|
||||
this.setKey(getString(R.string.jsdelivr_proxy_key), false)
|
||||
} else {
|
||||
this.setKey(getString(R.string.jsdelivr_proxy_key), true)
|
||||
val parentView: View = findViewById(android.R.id.content)
|
||||
Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG).let { snackbar ->
|
||||
snackbar.setAction(R.string.revert) {
|
||||
setKey(getString(R.string.jsdelivr_proxy_key), false)
|
||||
}
|
||||
snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground))
|
||||
snackbar.setTextColor(colorFromAttribute(R.attr.textColor))
|
||||
snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary))
|
||||
snackbar.show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (PluginManager.checkSafeModeFile()) {
|
||||
normalSafeApiCall {
|
||||
|
@ -1089,4 +1116,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
// }
|
||||
|
||||
}
|
||||
|
||||
suspend fun checkGithubConnectivity(): Boolean {
|
||||
return try {
|
||||
app.get("https://raw.githubusercontent.com/recloudstream/.github/master/connectivitycheck", timeout = 5).text.trim() == "ok"
|
||||
} catch (t: Throwable) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,7 +130,7 @@ open class StreamSB : ExtractorApi() {
|
|||
it.value.replace(Regex("(embed-|/e/)"), "")
|
||||
}.first()
|
||||
// val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
|
||||
val master = "$mainUrl/sources50/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
|
||||
val master = "$mainUrl/sources51/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
|
||||
val headers = mapOf(
|
||||
"watchsb" to "sbstream",
|
||||
)
|
||||
|
@ -156,4 +156,4 @@ open class StreamSB : ExtractorApi() {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) {
|
|||
}
|
||||
}
|
||||
|
||||
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||
liveData.observe(this) { it?.let { t -> action(t) } }
|
||||
}
|
||||
|
||||
|
@ -121,13 +121,21 @@ suspend fun <T> suspendSafeApiCall(apiCall: suspend () -> T): T? {
|
|||
}
|
||||
}
|
||||
|
||||
fun Throwable.getAllMessages(): String {
|
||||
return (this.localizedMessage ?: "") + (this.cause?.getAllMessages()?.let { "\n$it" } ?: "")
|
||||
}
|
||||
|
||||
fun Throwable.getStackTracePretty(showMessage: Boolean = true): String {
|
||||
val prefix = if (showMessage) this.localizedMessage?.let { "\n$it" } ?: "" else ""
|
||||
return prefix + this.stackTrace.joinToString(
|
||||
separator = "\n"
|
||||
) {
|
||||
"${it.fileName} ${it.lineNumber}"
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> safeFail(throwable: Throwable): Resource<T> {
|
||||
val stackTraceMsg =
|
||||
(throwable.localizedMessage ?: "") + "\n\n" + throwable.stackTrace.joinToString(
|
||||
separator = "\n"
|
||||
) {
|
||||
"${it.fileName} ${it.lineNumber}"
|
||||
}
|
||||
val stackTraceMsg = throwable.getStackTracePretty()
|
||||
return Resource.Failure(false, null, null, stackTraceMsg)
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.google.gson.Gson
|
|||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
|
@ -165,11 +166,11 @@ object PluginManager {
|
|||
private var loadedLocalPlugins = false
|
||||
private val gson = Gson()
|
||||
|
||||
private suspend fun maybeLoadPlugin(activity: Activity, file: File) {
|
||||
private suspend fun maybeLoadPlugin(context: Context, file: File) {
|
||||
val name = file.name
|
||||
if (file.extension == "zip" || file.extension == "cs3") {
|
||||
loadPlugin(
|
||||
activity,
|
||||
context,
|
||||
file,
|
||||
PluginData(name, null, false, file.absolutePath, PLUGIN_VERSION_NOT_SET)
|
||||
)
|
||||
|
@ -199,7 +200,7 @@ object PluginManager {
|
|||
|
||||
// var allCurrentOutDatedPlugins: Set<OnlinePluginData> = emptySet()
|
||||
|
||||
suspend fun loadSinglePlugin(activity: Activity, apiName: String): Boolean {
|
||||
suspend fun loadSinglePlugin(context: Context, apiName: String): Boolean {
|
||||
return (getPluginsOnline().firstOrNull {
|
||||
// Most of the time the provider ends with Provider which isn't part of the api name
|
||||
it.internalName.replace("provider", "", ignoreCase = true) == apiName
|
||||
|
@ -209,7 +210,7 @@ object PluginManager {
|
|||
})?.let { savedData ->
|
||||
// OnlinePluginData(savedData, onlineData)
|
||||
loadPlugin(
|
||||
activity,
|
||||
context,
|
||||
File(savedData.filePath),
|
||||
savedData
|
||||
)
|
||||
|
@ -371,11 +372,11 @@ object PluginManager {
|
|||
/**
|
||||
* Use updateAllOnlinePluginsAndLoadThem
|
||||
* */
|
||||
fun loadAllOnlinePlugins(activity: Activity) {
|
||||
fun loadAllOnlinePlugins(context: Context) {
|
||||
// Load all plugins as fast as possible!
|
||||
(getPluginsOnline()).toList().apmap { pluginData ->
|
||||
loadPlugin(
|
||||
activity,
|
||||
context,
|
||||
File(pluginData.filePath),
|
||||
pluginData
|
||||
)
|
||||
|
@ -398,7 +399,7 @@ object PluginManager {
|
|||
* @param forceReload see afterPluginsLoadedEvent, basically a way to load all local plugins
|
||||
* and reload all pages even if they are previously valid
|
||||
**/
|
||||
fun loadAllLocalPlugins(activity: Activity, forceReload: Boolean) {
|
||||
fun loadAllLocalPlugins(context: Context, forceReload: Boolean) {
|
||||
val dir = File(LOCAL_PLUGINS_PATH)
|
||||
removeKey(PLUGINS_KEY_LOCAL)
|
||||
|
||||
|
@ -416,7 +417,7 @@ object PluginManager {
|
|||
Log.d(TAG, "Files in '${LOCAL_PLUGINS_PATH}' folder: $sortedPlugins")
|
||||
|
||||
sortedPlugins?.sortedBy { it.name }?.apmap { file ->
|
||||
maybeLoadPlugin(activity, file)
|
||||
maybeLoadPlugin(context, file)
|
||||
}
|
||||
|
||||
loadedLocalPlugins = true
|
||||
|
@ -441,14 +442,14 @@ object PluginManager {
|
|||
/**
|
||||
* @return True if successful, false if not
|
||||
* */
|
||||
private suspend fun loadPlugin(activity: Activity, file: File, data: PluginData): Boolean {
|
||||
private suspend fun loadPlugin(context: Context, file: File, data: PluginData): Boolean {
|
||||
val fileName = file.nameWithoutExtension
|
||||
val filePath = file.absolutePath
|
||||
currentlyLoading = fileName
|
||||
Log.i(TAG, "Loading plugin: $data")
|
||||
|
||||
return try {
|
||||
val loader = PathClassLoader(filePath, activity.classLoader)
|
||||
val loader = PathClassLoader(filePath, context.classLoader)
|
||||
var manifest: Plugin.Manifest
|
||||
loader.getResourceAsStream("manifest.json").use { stream ->
|
||||
if (stream == null) {
|
||||
|
@ -492,22 +493,22 @@ object PluginManager {
|
|||
addAssetPath.invoke(assets, file.absolutePath)
|
||||
pluginInstance.resources = Resources(
|
||||
assets,
|
||||
activity.resources.displayMetrics,
|
||||
activity.resources.configuration
|
||||
context.resources.displayMetrics,
|
||||
context.resources.configuration
|
||||
)
|
||||
}
|
||||
plugins[filePath] = pluginInstance
|
||||
classLoaders[loader] = pluginInstance
|
||||
urlPlugins[data.url ?: filePath] = pluginInstance
|
||||
pluginInstance.load(activity)
|
||||
pluginInstance.load(context)
|
||||
Log.i(TAG, "Loaded plugin ${data.internalName} successfully")
|
||||
currentlyLoading = null
|
||||
true
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to load $file: ${Log.getStackTraceString(e)}")
|
||||
showToast(
|
||||
activity,
|
||||
activity.getString(R.string.plugin_load_fail).format(fileName),
|
||||
context.getActivity(),
|
||||
context.getString(R.string.plugin_load_fail).format(fileName),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
currentlyLoading = null
|
||||
|
|
|
@ -2,8 +2,10 @@ package com.lagradost.cloudstream3.plugins
|
|||
|
||||
import android.content.Context
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.amap
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
|
@ -71,6 +73,15 @@ object RepositoryManager {
|
|||
val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy {
|
||||
getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
|
||||
}
|
||||
val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
|
||||
|
||||
/* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */
|
||||
fun convertRawGitUrl(url: String): String {
|
||||
if (getKey<Boolean>(context!!.getString(R.string.jsdelivr_proxy_key)) != true) return url
|
||||
val match = GH_REGEX.find(url) ?: return url
|
||||
val (user, repo, rest) = match.destructured
|
||||
return "https://cdn.jsdelivr.net/gh/$user/$repo@$rest"
|
||||
}
|
||||
|
||||
suspend fun parseRepoUrl(url: String): String? {
|
||||
val fixedUrl = url.trim()
|
||||
|
@ -84,10 +95,15 @@ object RepositoryManager {
|
|||
}
|
||||
} else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) {
|
||||
suspendSafeApiCall {
|
||||
app.get("https://l.cloudstream.cf/${fixedUrl}").let {
|
||||
return@let if (it.isSuccessful && !it.url.startsWith("https://cutt.ly/branded-domains")) it.url
|
||||
else app.get("https://cutt.ly/${fixedUrl}").let let2@{ it2 ->
|
||||
return@let2 if (it2.isSuccessful) it2.url else null
|
||||
app.get("https://l.cloudstream.cf/${fixedUrl}", allowRedirects = false).let {
|
||||
it.headers["Location"]?.let { url ->
|
||||
return@suspendSafeApiCall if (!url.startsWith("https://cutt.ly/branded-domains")) url
|
||||
else null
|
||||
}
|
||||
app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false).let { it2 ->
|
||||
it2.headers["Location"]?.let { url ->
|
||||
return@suspendSafeApiCall if (url.startsWith("https://cutt.ly/404")) url else null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,14 +113,14 @@ object RepositoryManager {
|
|||
suspend fun parseRepository(url: String): Repository? {
|
||||
return suspendSafeApiCall {
|
||||
// Take manifestVersion and such into account later
|
||||
app.get(url).parsedSafe()
|
||||
app.get(convertRawGitUrl(url)).parsedSafe()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parsePlugins(pluginUrls: String): List<SitePlugin> {
|
||||
// Take manifestVersion and such into account later
|
||||
return try {
|
||||
val response = app.get(pluginUrls)
|
||||
val response = app.get(convertRawGitUrl(pluginUrls))
|
||||
// Normal parsed function not working?
|
||||
// return response.parsedSafe()
|
||||
tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList()
|
||||
|
@ -139,7 +155,7 @@ object RepositoryManager {
|
|||
}
|
||||
file.createNewFile()
|
||||
|
||||
val body = app.get(pluginUrl).okhttpResponse.body
|
||||
val body = app.get(convertRawGitUrl(pluginUrl)).okhttpResponse.body
|
||||
write(body.byteStream(), file.outputStream())
|
||||
file
|
||||
}
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
package com.lagradost.cloudstream3.services
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.work.*
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getImageBitmapFromUrl
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
const val SUBSCRIPTION_CHANNEL_ID = "cloudstream3.subscriptions"
|
||||
const val SUBSCRIPTION_WORK_NAME = "work_subscription"
|
||||
const val SUBSCRIPTION_CHANNEL_NAME = "Subscriptions"
|
||||
const val SUBSCRIPTION_CHANNEL_DESCRIPTION = "Notifications for new episodes on subscribed shows"
|
||||
const val SUBSCRIPTION_NOTIFICATION_ID = 938712897 // Random unique
|
||||
|
||||
class SubscriptionWorkManager(val context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
companion object {
|
||||
fun enqueuePeriodicWork(context: Context?) {
|
||||
if (context == null) return
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
val periodicSyncDataWork =
|
||||
PeriodicWorkRequest.Builder(SubscriptionWorkManager::class.java, 6, TimeUnit.HOURS)
|
||||
.addTag(SUBSCRIPTION_WORK_NAME)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
SUBSCRIPTION_WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
periodicSyncDataWork
|
||||
)
|
||||
|
||||
// Uncomment below for testing
|
||||
|
||||
// val oneTimeSyncDataWork =
|
||||
// OneTimeWorkRequest.Builder(SubscriptionWorkManager::class.java)
|
||||
// .addTag(SUBSCRIPTION_WORK_NAME)
|
||||
// .setConstraints(constraints)
|
||||
// .build()
|
||||
//
|
||||
// WorkManager.getInstance(context).enqueue(oneTimeSyncDataWork)
|
||||
}
|
||||
}
|
||||
|
||||
private val progressNotificationBuilder =
|
||||
NotificationCompat.Builder(context, SUBSCRIPTION_CHANNEL_ID)
|
||||
.setAutoCancel(false)
|
||||
.setColorized(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setSilent(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
|
||||
.setContentTitle(context.getString(R.string.subscription_in_progress_notification))
|
||||
.setSmallIcon(R.drawable.quantum_ic_refresh_white_24)
|
||||
.setProgress(0, 0, true)
|
||||
|
||||
private val updateNotificationBuilder =
|
||||
NotificationCompat.Builder(context, SUBSCRIPTION_CHANNEL_ID)
|
||||
.setColorized(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setAutoCancel(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
|
||||
.setSmallIcon(R.drawable.ic_cloudstream_monochrome_big)
|
||||
|
||||
private val notificationManager: NotificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
private fun updateProgress(max: Int, progress: Int, indeterminate: Boolean) {
|
||||
notificationManager.notify(
|
||||
SUBSCRIPTION_NOTIFICATION_ID, progressNotificationBuilder
|
||||
.setProgress(max, progress, indeterminate)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
// println("Update subscriptions!")
|
||||
context.createNotificationChannel(
|
||||
SUBSCRIPTION_CHANNEL_ID,
|
||||
SUBSCRIPTION_CHANNEL_NAME,
|
||||
SUBSCRIPTION_CHANNEL_DESCRIPTION
|
||||
)
|
||||
|
||||
safeApiCall {
|
||||
setForeground(
|
||||
ForegroundInfo(
|
||||
SUBSCRIPTION_NOTIFICATION_ID,
|
||||
progressNotificationBuilder.build()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val subscriptions = getAllSubscriptions()
|
||||
|
||||
if (subscriptions.isEmpty()) {
|
||||
WorkManager.getInstance(context).cancelWorkById(this.id)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val max = subscriptions.size
|
||||
var progress = 0
|
||||
|
||||
updateProgress(max, progress, true)
|
||||
|
||||
// We need all plugins loaded.
|
||||
PluginManager.loadAllOnlinePlugins(context)
|
||||
PluginManager.loadAllLocalPlugins(context, false)
|
||||
|
||||
subscriptions.apmap { savedData ->
|
||||
try {
|
||||
val id = savedData.id ?: return@apmap null
|
||||
val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null
|
||||
|
||||
// Reasonable timeout to prevent having this worker run forever.
|
||||
val response = withTimeoutOrNull(60_000) {
|
||||
api.load(savedData.url) as? EpisodeResponse
|
||||
} ?: return@apmap null
|
||||
|
||||
val dubPreference =
|
||||
getDub(id) ?: if (
|
||||
context.getApiDubstatusSettings().contains(DubStatus.Dubbed)
|
||||
) {
|
||||
DubStatus.Dubbed
|
||||
} else {
|
||||
DubStatus.Subbed
|
||||
}
|
||||
|
||||
val latestEpisodes = response.getLatestEpisodes()
|
||||
val latestPreferredEpisode = latestEpisodes[dubPreference]
|
||||
|
||||
val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) {
|
||||
val latestSeenEpisode =
|
||||
savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE
|
||||
val shouldUpdate = latestPreferredEpisode > latestSeenEpisode
|
||||
shouldUpdate to latestPreferredEpisode
|
||||
} else {
|
||||
val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE
|
||||
val latestSeenEpisode =
|
||||
savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE
|
||||
val shouldUpdate = latestEpisode > latestSeenEpisode
|
||||
shouldUpdate to latestEpisode
|
||||
}
|
||||
|
||||
DataStoreHelper.updateSubscribedData(
|
||||
id,
|
||||
savedData,
|
||||
response
|
||||
)
|
||||
|
||||
if (shouldUpdate) {
|
||||
val updateHeader = savedData.name
|
||||
val updateDescription = txt(
|
||||
R.string.subscription_episode_released,
|
||||
latestEpisode,
|
||||
savedData.name
|
||||
).asString(context)
|
||||
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
data = savedData.url.toUri()
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
val pendingIntent =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(context, 0, intent, 0)
|
||||
}
|
||||
|
||||
val poster = ioWork { savedData.posterUrl?.let { url -> context.getImageBitmapFromUrl(url, savedData.posterHeaders) } }
|
||||
val updateNotification =
|
||||
updateNotificationBuilder.setContentTitle(updateHeader)
|
||||
.setContentText(updateDescription)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setLargeIcon(poster)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(id, updateNotification)
|
||||
}
|
||||
|
||||
// You can probably get some issues here since this is async but it does not matter much.
|
||||
updateProgress(max, ++progress, false)
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
}
|
|
@ -1,11 +1,22 @@
|
|||
package com.lagradost.cloudstream3.services
|
||||
|
||||
import android.app.IntentService
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class VideoDownloadService : IntentService("VideoDownloadService") {
|
||||
override fun onHandleIntent(intent: Intent?) {
|
||||
class VideoDownloadService : Service() {
|
||||
|
||||
private val downloadScope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent != null) {
|
||||
val id = intent.getIntExtra("id", -1)
|
||||
val type = intent.getStringExtra("type")
|
||||
|
@ -14,10 +25,36 @@ class VideoDownloadService : IntentService("VideoDownloadService") {
|
|||
"resume" -> VideoDownloadManager.DownloadActionType.Resume
|
||||
"pause" -> VideoDownloadManager.DownloadActionType.Pause
|
||||
"stop" -> VideoDownloadManager.DownloadActionType.Stop
|
||||
else -> return
|
||||
else -> return START_NOT_STICKY
|
||||
}
|
||||
|
||||
downloadScope.launch {
|
||||
VideoDownloadManager.downloadEvent.invoke(Pair(id, state))
|
||||
}
|
||||
VideoDownloadManager.downloadEvent.invoke(Pair(id, state))
|
||||
}
|
||||
}
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
downloadScope.coroutineContext.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
// override fun onHandleIntent(intent: Intent?) {
|
||||
// if (intent != null) {
|
||||
// val id = intent.getIntExtra("id", -1)
|
||||
// val type = intent.getStringExtra("type")
|
||||
// if (id != -1 && type != null) {
|
||||
// val state = when (type) {
|
||||
// "resume" -> VideoDownloadManager.DownloadActionType.Resume
|
||||
// "pause" -> VideoDownloadManager.DownloadActionType.Pause
|
||||
// "stop" -> VideoDownloadManager.DownloadActionType.Stop
|
||||
// else -> return
|
||||
// }
|
||||
// VideoDownloadManager.downloadEvent.invoke(Pair(id, state))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -759,6 +759,11 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
return data != ""
|
||||
}
|
||||
|
||||
/** Used to query a saved MediaItem on the list to get the id for removal */
|
||||
data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null)
|
||||
data class MediaListItem(@JsonProperty("MediaList") val MediaList: MediaListId? = null)
|
||||
data class MediaListId(@JsonProperty("id") val id: Long? = null)
|
||||
|
||||
private suspend fun postDataAboutId(
|
||||
id: Int,
|
||||
type: AniListStatusType,
|
||||
|
@ -766,19 +771,43 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
progress: Int?
|
||||
): Boolean {
|
||||
val q =
|
||||
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${
|
||||
aniListStatusString[maxOf(
|
||||
0,
|
||||
type.value
|
||||
)]
|
||||
}, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) {
|
||||
SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) {
|
||||
id
|
||||
status
|
||||
progress
|
||||
score
|
||||
}
|
||||
// Delete item if status type is None
|
||||
if (type == AniListStatusType.None) {
|
||||
val userID = getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.id ?: return false
|
||||
// Get list ID for deletion
|
||||
val idQuery = """
|
||||
query MediaList(${'$'}userId: Int = $userID, ${'$'}mediaId: Int = $id) {
|
||||
MediaList(userId: ${'$'}userId, mediaId: ${'$'}mediaId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
"""
|
||||
val response = postApi(idQuery)
|
||||
val listId =
|
||||
tryParseJson<MediaListItemRoot>(response)?.data?.MediaList?.id ?: return false
|
||||
"""
|
||||
mutation(${'$'}id: Int = $listId) {
|
||||
DeleteMediaListEntry(id: ${'$'}id) {
|
||||
deleted
|
||||
}
|
||||
}
|
||||
"""
|
||||
} else {
|
||||
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${
|
||||
aniListStatusString[maxOf(
|
||||
0,
|
||||
type.value
|
||||
)]
|
||||
}, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) {
|
||||
SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) {
|
||||
id
|
||||
status
|
||||
progress
|
||||
score
|
||||
}
|
||||
}"""
|
||||
}
|
||||
|
||||
val data = postApi(q)
|
||||
return data != ""
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import com.lagradost.cloudstream3.ui.WatchType
|
|||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
|
||||
|
@ -74,13 +75,16 @@ class LocalList : SyncAPI {
|
|||
group.value.mapNotNull {
|
||||
getBookmarkedData(it.first)?.toLibraryItem(it.first.toString())
|
||||
}
|
||||
}
|
||||
} + mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
|
||||
it.toLibraryItem()
|
||||
})
|
||||
}
|
||||
|
||||
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
|
||||
// None is not something to display
|
||||
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||
}
|
||||
} + mapOf(R.string.subscription_list_name to emptyList())
|
||||
|
||||
return SyncAPI.LibraryMetadata(
|
||||
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
|
||||
setOf(
|
||||
|
|
|
@ -15,6 +15,7 @@ import android.view.ViewGroup
|
|||
import android.widget.AbsListView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
|
@ -27,12 +28,14 @@ import com.google.android.material.chip.ChipDrawable
|
|||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.DubStatus
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.mvvm.*
|
||||
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
|
||||
|
@ -850,7 +853,7 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
}
|
||||
|
||||
observe(viewModel.page) { data ->
|
||||
if(data == null) return@observe
|
||||
if (data == null) return@observe
|
||||
when (data) {
|
||||
is Resource.Success -> {
|
||||
val d = data.value
|
||||
|
@ -904,6 +907,36 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
updateList(d.actors ?: emptyList())
|
||||
}
|
||||
|
||||
observeNullable(viewModel.subscribeStatus) { isSubscribed ->
|
||||
result_subscribe?.isVisible = isSubscribed != null
|
||||
if (isSubscribed == null) return@observeNullable
|
||||
|
||||
val drawable = if (isSubscribed) {
|
||||
R.drawable.ic_baseline_notifications_active_24
|
||||
} else {
|
||||
R.drawable.baseline_notifications_none_24
|
||||
}
|
||||
|
||||
result_subscribe?.setImageResource(drawable)
|
||||
}
|
||||
|
||||
result_subscribe?.setOnClickListener {
|
||||
val isSubscribed =
|
||||
viewModel.toggleSubscriptionStatus() ?: return@setOnClickListener
|
||||
|
||||
val message = if (isSubscribed) {
|
||||
// Kinda icky to have this here, but it works.
|
||||
SubscriptionWorkManager.enqueuePeriodicWork(context)
|
||||
R.string.subscription_new
|
||||
} else {
|
||||
R.string.subscription_deleted
|
||||
}
|
||||
|
||||
val name = (viewModel.page.value as? Resource.Success)?.value?.title
|
||||
?: txt(R.string.no_data).asStringNull(context) ?: ""
|
||||
showToast(activity, txt(message, name), Toast.LENGTH_SHORT)
|
||||
}
|
||||
|
||||
result_open_in_browser?.isVisible = d.url.startsWith("http")
|
||||
result_open_in_browser?.setOnClickListener {
|
||||
val i = Intent(ACTION_VIEW)
|
||||
|
|
|
@ -322,9 +322,7 @@ class ResultFragmentPhone : ResultFragment() {
|
|||
// it?.dismiss()
|
||||
//}
|
||||
builder.setCanceledOnTouchOutside(true)
|
||||
|
||||
builder.show()
|
||||
|
||||
builder
|
||||
}
|
||||
}
|
||||
|
|
|
@ -176,8 +176,7 @@ class ResultFragmentTv : ResultFragment() {
|
|||
loadingDialog = null
|
||||
}
|
||||
loadingDialog = loadingDialog ?: context?.let { ctx ->
|
||||
val builder =
|
||||
BottomSheetDialog(ctx)
|
||||
val builder = BottomSheetDialog(ctx)
|
||||
builder.setContentView(R.layout.bottom_loading)
|
||||
builder.setOnDismissListener {
|
||||
loadingDialog = null
|
||||
|
@ -187,9 +186,7 @@ class ResultFragmentTv : ResultFragment() {
|
|||
// it?.dismiss()
|
||||
//}
|
||||
builder.setCanceledOnTouchOutside(true)
|
||||
|
||||
builder.show()
|
||||
|
||||
builder
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.lagradost.cloudstream3.*
|
|||
import com.lagradost.cloudstream3.APIHolder.apis
|
||||
import com.lagradost.cloudstream3.APIHolder.getId
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.getCastSession
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
|
@ -414,6 +415,9 @@ class ResultViewModel2 : ViewModel() {
|
|||
private val _episodeSynopsis: MutableLiveData<String?> = MutableLiveData(null)
|
||||
val episodeSynopsis: LiveData<String?> = _episodeSynopsis
|
||||
|
||||
private val _subscribeStatus: MutableLiveData<Boolean?> = MutableLiveData(null)
|
||||
val subscribeStatus: LiveData<Boolean?> = _subscribeStatus
|
||||
|
||||
companion object {
|
||||
const val TAG = "RVM2"
|
||||
private const val EPISODE_RANGE_SIZE = 20
|
||||
|
@ -815,6 +819,42 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the new status is Subscribed, false if not. Null if not possible to subscribe.
|
||||
**/
|
||||
fun toggleSubscriptionStatus(): Boolean? {
|
||||
val isSubscribed = _subscribeStatus.value ?: return null
|
||||
val response = currentResponse ?: return null
|
||||
if (response !is EpisodeResponse) return null
|
||||
|
||||
val currentId = response.getId()
|
||||
|
||||
if (isSubscribed) {
|
||||
DataStoreHelper.removeSubscribedData(currentId)
|
||||
} else {
|
||||
val current = DataStoreHelper.getSubscribedData(currentId)
|
||||
|
||||
DataStoreHelper.setSubscribedData(
|
||||
currentId,
|
||||
DataStoreHelper.SubscribedData(
|
||||
currentId,
|
||||
current?.bookmarkedTime ?: unixTimeMS,
|
||||
unixTimeMS,
|
||||
response.getLatestEpisodes(),
|
||||
response.name,
|
||||
response.url,
|
||||
response.apiName,
|
||||
response.type,
|
||||
response.posterUrl,
|
||||
response.year
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
_subscribeStatus.postValue(!isSubscribed)
|
||||
return !isSubscribed
|
||||
}
|
||||
|
||||
private fun startChromecast(
|
||||
activity: Activity?,
|
||||
result: ResultEpisode,
|
||||
|
@ -1473,7 +1513,8 @@ class ResultViewModel2 : ViewModel() {
|
|||
this.engName,
|
||||
this.name,
|
||||
this.japName
|
||||
).filter { it.length > 2 }.distinct(), // the reason why we filter is due to not wanting smth like " " or "?"
|
||||
).filter { it.length > 2 }
|
||||
.distinct(), // the reason why we filter is due to not wanting smth like " " or "?"
|
||||
TrackerType.getTypes(this.type),
|
||||
this.year
|
||||
)
|
||||
|
@ -1670,6 +1711,16 @@ class ResultViewModel2 : ViewModel() {
|
|||
postResume()
|
||||
}
|
||||
|
||||
private fun postSubscription(loadResponse: LoadResponse) {
|
||||
if (loadResponse.isEpisodeBased()) {
|
||||
val id = loadResponse.getId()
|
||||
val data = DataStoreHelper.getSubscribedData(id)
|
||||
DataStoreHelper.updateSubscribedData(id, data, loadResponse as? EpisodeResponse)
|
||||
val isSubscribed = data != null
|
||||
_subscribeStatus.postValue(isSubscribed)
|
||||
}
|
||||
}
|
||||
|
||||
private fun postEpisodeRange(indexer: EpisodeIndexer?, range: EpisodeRange?) {
|
||||
if (range == null || indexer == null) {
|
||||
return
|
||||
|
@ -1806,6 +1857,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
) {
|
||||
currentResponse = loadResponse
|
||||
postPage(loadResponse, apiRepository)
|
||||
postSubscription(loadResponse)
|
||||
if (updateEpisodes)
|
||||
postEpisodes(loadResponse, updateFillers)
|
||||
}
|
||||
|
|
|
@ -68,12 +68,13 @@ val appLanguages = arrayListOf(
|
|||
Triple("", "español", "es"),
|
||||
Triple("", "فارسی", "fa"),
|
||||
Triple("", "français", "fr"),
|
||||
Triple("\uD83C\uDDEE\uD83C\uDDF1", "עברית", "iw"),
|
||||
Triple("", "हिन्दी", "hi"),
|
||||
Triple("", "hrvatski", "hr"),
|
||||
Triple("", "magyar", "hu"),
|
||||
Triple("\uD83C\uDDEE\uD83C\uDDE9", "Bahasa Indonesia", "in"),
|
||||
Triple("", "italiano", "it"),
|
||||
Triple("\uD83C\uDDEE\uD83C\uDDF1", "עברית", "iw"),
|
||||
Triple("", "日本語 (にほんご)", "ja"),
|
||||
Triple("", "ಕನ್ನಡ", "kn"),
|
||||
Triple("", "македонски", "mk"),
|
||||
Triple("", "മലയാളം", "ml"),
|
||||
|
@ -82,7 +83,7 @@ val appLanguages = arrayListOf(
|
|||
Triple("", "norsk bokmål", "no"),
|
||||
Triple("", "polski", "pl"),
|
||||
Triple("\uD83C\uDDF5\uD83C\uDDF9", "português", "pt"),
|
||||
Triple("🦍", "mmmm... monke", "qt"),
|
||||
Triple("\uD83E\uDD8D", "mmmm... monke", "qt"),
|
||||
Triple("", "română", "ro"),
|
||||
Triple("", "русский", "ru"),
|
||||
Triple("", "slovenčina", "sk"),
|
||||
|
@ -97,7 +98,7 @@ val appLanguages = arrayListOf(
|
|||
Triple("", "中文", "zh"),
|
||||
Triple("\uD83C\uDDF9\uD83C\uDDFC", "文言", "zh-rTW"),
|
||||
/* end language list */
|
||||
).sortedBy { it.second?.toLowerCase() } //ye, we go alphabetical, so ppl don't put their lang on top
|
||||
).sortedBy { it.second.lowercase() } //ye, we go alphabetical, so ppl don't put their lang on top
|
||||
|
||||
class SettingsGeneral : PreferenceFragmentCompat() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -157,9 +158,6 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
|
||||
getPref(R.string.locale_key)?.setOnPreferenceClickListener { pref ->
|
||||
val tempLangs = appLanguages.toMutableList()
|
||||
//if (beneneCount > 100) {
|
||||
// tempLangs.add(Triple("\uD83E\uDD8D", "mmmm... monke", "mo"))
|
||||
//}
|
||||
val current = getCurrentLocale(pref.context)
|
||||
val languageCodes = tempLangs.map { (_, _, iso) -> iso }
|
||||
val languageNames = tempLangs.map { (emoji, name, iso) ->
|
||||
|
@ -316,6 +314,12 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
} ?: emptyList()
|
||||
}
|
||||
|
||||
settingsManager.edit().putBoolean(getString(R.string.jsdelivr_proxy_key), getKey(getString(R.string.jsdelivr_proxy_key), false) ?: false).apply()
|
||||
getPref(R.string.jsdelivr_proxy_key)?.setOnPreferenceChangeListener { _, newValue ->
|
||||
setKey(getString(R.string.jsdelivr_proxy_key), newValue)
|
||||
return@setOnPreferenceChangeListener true
|
||||
}
|
||||
|
||||
getPref(R.string.download_path_key)?.setOnPreferenceClickListener {
|
||||
val dirs = getDownloadDirs()
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ package com.lagradost.cloudstream3.ui.settings
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.*
|
||||
|
@ -16,6 +18,7 @@ import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
|||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
|
||||
class SettingsProviders : PreferenceFragmentCompat() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -56,6 +59,20 @@ class SettingsProviders : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
getPref(R.string.test_providers_key)?.setOnPreferenceClickListener {
|
||||
// Somehow animations do not work without this.
|
||||
val options = NavOptions.Builder()
|
||||
.setEnterAnim(R.anim.enter_anim)
|
||||
.setExitAnim(R.anim.exit_anim)
|
||||
.setPopEnterAnim(R.anim.pop_enter)
|
||||
.setPopExitAnim(R.anim.pop_exit)
|
||||
.build()
|
||||
|
||||
this@SettingsProviders.findNavController()
|
||||
.navigate(R.id.navigation_test_providers, null, options)
|
||||
true
|
||||
}
|
||||
|
||||
getPref(R.string.prefer_media_type_key)?.setOnPreferenceClickListener {
|
||||
val names = enumValues<TvType>().sorted().map { it.name }
|
||||
val default =
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
package com.lagradost.cloudstream3.ui.settings.testing
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.mvvm.observeNullable
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
import kotlinx.android.synthetic.main.fragment_testing.*
|
||||
import kotlinx.android.synthetic.main.view_test.*
|
||||
|
||||
|
||||
class TestFragment : Fragment() {
|
||||
|
||||
private val testViewModel: TestViewModel by activityViewModels()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
setUpToolbar(R.string.category_provider_test)
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
provider_test_recycler_view?.adapter = TestResultAdapter(
|
||||
mutableListOf()
|
||||
)
|
||||
|
||||
testViewModel.init()
|
||||
if (testViewModel.isRunningTest) {
|
||||
provider_test?.setState(TestView.TestState.Running)
|
||||
}
|
||||
|
||||
observe(testViewModel.providerProgress) { (passed, failed, total) ->
|
||||
provider_test?.setProgress(passed, failed, total)
|
||||
}
|
||||
|
||||
observeNullable(testViewModel.providerResults) {
|
||||
normalSafeApiCall {
|
||||
val newItems = it.sortedBy { api -> api.first.name }
|
||||
(provider_test_recycler_view?.adapter as? TestResultAdapter)?.updateList(
|
||||
newItems
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
provider_test?.setOnPlayButtonListener { state ->
|
||||
when (state) {
|
||||
TestView.TestState.Stopped -> testViewModel.stopTest()
|
||||
TestView.TestState.Running -> testViewModel.startTest()
|
||||
TestView.TestState.None -> testViewModel.startTest()
|
||||
}
|
||||
}
|
||||
|
||||
if (isTrueTvSettings()) {
|
||||
tests_play_pause?.isFocusableInTouchMode = true
|
||||
tests_play_pause?.requestFocus()
|
||||
}
|
||||
|
||||
provider_test?.playPauseButton?.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
provider_test_appbar?.setExpanded(true, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun focusRecyclerView() {
|
||||
// Hack to make it possible to focus the recyclerview.
|
||||
if (isTrueTvSettings()) {
|
||||
provider_test_recycler_view?.requestFocus()
|
||||
provider_test_appbar?.setExpanded(false, true)
|
||||
}
|
||||
}
|
||||
|
||||
provider_test?.setOnMainClick {
|
||||
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.All)
|
||||
focusRecyclerView()
|
||||
}
|
||||
provider_test?.setOnFailedClick {
|
||||
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Failed)
|
||||
focusRecyclerView()
|
||||
}
|
||||
provider_test?.setOnPassedClick {
|
||||
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Passed)
|
||||
focusRecyclerView()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_testing, container, false)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package com.lagradost.cloudstream3.ui.settings.testing
|
||||
|
||||
import android.app.AlertDialog
|
||||
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.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.MainAPI
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.getAllMessages
|
||||
import com.lagradost.cloudstream3.mvvm.getStackTracePretty
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
|
||||
import com.lagradost.cloudstream3.utils.TestingUtils
|
||||
import kotlinx.android.synthetic.main.provider_test_item.view.*
|
||||
|
||||
class TestResultAdapter(override val items: MutableList<Pair<MainAPI, TestingUtils.TestResultProvider>>) :
|
||||
AppUtils.DiffAdapter<Pair<MainAPI, TestingUtils.TestResultProvider>>(items) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return ProviderTestViewHolder(
|
||||
LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.provider_test_item, parent, false),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is ProviderTestViewHolder -> {
|
||||
val item = items[position]
|
||||
holder.bind(item.first, item.second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ProviderTestViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val languageText: TextView = itemView.lang_icon
|
||||
private val providerTitle: TextView = itemView.main_text
|
||||
private val statusText: TextView = itemView.passed_failed_marker
|
||||
private val failDescription: TextView = itemView.fail_description
|
||||
private val logButton: ImageView = itemView.action_button
|
||||
|
||||
private fun String.lastLine(): String? {
|
||||
return this.lines().lastOrNull { it.isNotBlank() }
|
||||
}
|
||||
|
||||
fun bind(api: MainAPI, result: TestingUtils.TestResultProvider) {
|
||||
languageText.text = getFlagFromIso(api.lang)
|
||||
providerTitle.text = api.name
|
||||
|
||||
val (resultText, resultColor) = if (result.success) {
|
||||
R.string.test_passed to R.color.colorTestPass
|
||||
} else {
|
||||
R.string.test_failed to R.color.colorTestFail
|
||||
}
|
||||
|
||||
statusText.setText(resultText)
|
||||
statusText.setTextColor(ContextCompat.getColor(itemView.context, resultColor))
|
||||
|
||||
val stackTrace = result.exception?.getStackTracePretty(false)?.ifBlank { null }
|
||||
val messages = result.exception?.getAllMessages()?.ifBlank { null }
|
||||
val fullLog =
|
||||
result.log + (messages?.let { "\n\n$it" } ?: "") + (stackTrace?.let { "\n\n$it" } ?: "")
|
||||
|
||||
failDescription.text = messages?.lastLine() ?: result.log.lastLine()
|
||||
|
||||
logButton.setOnClickListener {
|
||||
val builder: AlertDialog.Builder =
|
||||
AlertDialog.Builder(it.context, R.style.AlertDialogCustom)
|
||||
builder.setMessage(fullLog)
|
||||
.setTitle(R.string.test_log)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package com.lagradost.cloudstream3.ui.settings.testing
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.ContentLoadingProgressBar
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.animateProgressTo
|
||||
|
||||
class TestView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : CardView(context, attrs) {
|
||||
enum class TestState(@StringRes val stringRes: Int, @DrawableRes val icon: Int) {
|
||||
None(R.string.start, R.drawable.ic_baseline_play_arrow_24),
|
||||
|
||||
// Paused(R.string.resume, R.drawable.ic_baseline_play_arrow_24),
|
||||
Stopped(R.string.restart, R.drawable.ic_baseline_play_arrow_24),
|
||||
Running(R.string.stop, R.drawable.pause_to_play),
|
||||
}
|
||||
|
||||
var mainSection: View? = null
|
||||
var testsPassedSection: View? = null
|
||||
var testsFailedSection: View? = null
|
||||
|
||||
var mainSectionText: TextView? = null
|
||||
var mainSectionHeader: TextView? = null
|
||||
var testsPassedSectionText: TextView? = null
|
||||
var testsFailedSectionText: TextView? = null
|
||||
var totalProgressBar: ContentLoadingProgressBar? = null
|
||||
|
||||
var playPauseButton: MaterialButton? = null
|
||||
var stateListener: (TestState) -> Unit = {}
|
||||
|
||||
private var state = TestState.None
|
||||
|
||||
init {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_test, this, true)
|
||||
|
||||
mainSection = findViewById(R.id.main_test_section)
|
||||
testsPassedSection = findViewById(R.id.passed_test_section)
|
||||
testsFailedSection = findViewById(R.id.failed_test_section)
|
||||
|
||||
mainSectionHeader = findViewById(R.id.main_test_header)
|
||||
mainSectionText = findViewById(R.id.main_test_section_progress)
|
||||
testsPassedSectionText = findViewById(R.id.passed_test_section_progress)
|
||||
testsFailedSectionText = findViewById(R.id.failed_test_section_progress)
|
||||
|
||||
totalProgressBar = findViewById(R.id.test_total_progress)
|
||||
playPauseButton = findViewById(R.id.tests_play_pause)
|
||||
|
||||
attrs?.let {
|
||||
val typedArray = context.obtainStyledAttributes(it, R.styleable.TestView)
|
||||
val headerText = typedArray.getString(R.styleable.TestView_header_text)
|
||||
mainSectionHeader?.text = headerText
|
||||
typedArray.recycle()
|
||||
}
|
||||
|
||||
playPauseButton?.setOnClickListener {
|
||||
val newState = when (state) {
|
||||
TestState.None -> TestState.Running
|
||||
TestState.Running -> TestState.Stopped
|
||||
TestState.Stopped -> TestState.Running
|
||||
}
|
||||
setState(newState)
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnPlayButtonListener(listener: (TestState) -> Unit) {
|
||||
stateListener = listener
|
||||
}
|
||||
|
||||
fun setState(newState: TestState) {
|
||||
state = newState
|
||||
stateListener.invoke(newState)
|
||||
playPauseButton?.setText(newState.stringRes)
|
||||
playPauseButton?.icon = ContextCompat.getDrawable(context, newState.icon)
|
||||
}
|
||||
|
||||
fun setProgress(passed: Int, failed: Int, total: Int?) {
|
||||
val totalProgress = passed + failed
|
||||
mainSectionText?.text = "$totalProgress / ${total?.toString() ?: "?"}"
|
||||
testsPassedSectionText?.text = passed.toString()
|
||||
testsFailedSectionText?.text = failed.toString()
|
||||
|
||||
totalProgressBar?.max = (total ?: 0) * 1000
|
||||
totalProgressBar?.animateProgressTo(totalProgress * 1000)
|
||||
|
||||
totalProgressBar?.isVisible = !(totalProgress == 0 || (total ?: 0) == 0)
|
||||
if (totalProgress == total) {
|
||||
setState(TestState.Stopped)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMainHeader(@StringRes header: Int) {
|
||||
mainSectionHeader?.setText(header)
|
||||
}
|
||||
|
||||
fun setOnMainClick(listener: OnClickListener) {
|
||||
mainSection?.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
fun setOnPassedClick(listener: OnClickListener) {
|
||||
testsPassedSection?.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
fun setOnFailedClick(listener: OnClickListener) {
|
||||
testsFailedSection?.setOnClickListener(listener)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
package com.lagradost.cloudstream3.ui.settings.testing
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.lagradost.cloudstream3.APIHolder
|
||||
import com.lagradost.cloudstream3.MainAPI
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||
import com.lagradost.cloudstream3.utils.TestingUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
|
||||
class TestViewModel : ViewModel() {
|
||||
data class TestProgress(
|
||||
val passed: Int,
|
||||
val failed: Int,
|
||||
val total: Int
|
||||
)
|
||||
|
||||
enum class ProviderFilter {
|
||||
All,
|
||||
Passed,
|
||||
Failed
|
||||
}
|
||||
|
||||
private val _providerProgress = MutableLiveData<TestProgress>(null)
|
||||
val providerProgress: LiveData<TestProgress> = _providerProgress
|
||||
|
||||
private val _providerResults =
|
||||
MutableLiveData<List<Pair<MainAPI, TestingUtils.TestResultProvider>>>(
|
||||
emptyList()
|
||||
)
|
||||
|
||||
val providerResults: LiveData<List<Pair<MainAPI, TestingUtils.TestResultProvider>>> =
|
||||
_providerResults
|
||||
|
||||
private var scope: CoroutineScope? = null
|
||||
val isRunningTest
|
||||
get() = scope != null
|
||||
|
||||
private var filter = ProviderFilter.All
|
||||
private val providers = threadSafeListOf<Pair<MainAPI, TestingUtils.TestResultProvider>>()
|
||||
private var passed = 0
|
||||
private var failed = 0
|
||||
private var total = 0
|
||||
|
||||
private fun updateProgress() {
|
||||
_providerProgress.postValue(TestProgress(passed, failed, total))
|
||||
postProviders()
|
||||
}
|
||||
|
||||
private fun postProviders() {
|
||||
synchronized(providers) {
|
||||
val filtered = when (filter) {
|
||||
ProviderFilter.All -> providers
|
||||
ProviderFilter.Passed -> providers.filter { it.second.success }
|
||||
ProviderFilter.Failed -> providers.filter { !it.second.success }
|
||||
}
|
||||
_providerResults.postValue(filtered)
|
||||
}
|
||||
}
|
||||
|
||||
fun setFilterMethod(filter: ProviderFilter) {
|
||||
if (this.filter == filter) return
|
||||
this.filter = filter
|
||||
postProviders()
|
||||
}
|
||||
|
||||
private fun addProvider(api: MainAPI, results: TestingUtils.TestResultProvider) {
|
||||
synchronized(providers) {
|
||||
val index = providers.indexOfFirst { it.first == api }
|
||||
if (index == -1) {
|
||||
providers.add(api to results)
|
||||
if (results.success) passed++ else failed++
|
||||
} else {
|
||||
providers[index] = api to results
|
||||
}
|
||||
updateProgress()
|
||||
}
|
||||
}
|
||||
|
||||
fun init() {
|
||||
val apis = APIHolder.allProviders
|
||||
total = apis.size
|
||||
updateProgress()
|
||||
}
|
||||
|
||||
fun startTest() {
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
val apis = APIHolder.allProviders
|
||||
total = apis.size
|
||||
failed = 0
|
||||
passed = 0
|
||||
providers.clear()
|
||||
updateProgress()
|
||||
|
||||
TestingUtils.getDeferredProviderTests(scope ?: return, apis, ::println) { api, result ->
|
||||
addProvider(api, result)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopTest() {
|
||||
scope?.cancel()
|
||||
scope = null
|
||||
}
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Activity.RESULT_CANCELED
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.database.Cursor
|
||||
|
@ -17,6 +20,7 @@ import android.os.*
|
|||
import android.provider.MediaStore
|
||||
import android.text.Spanned
|
||||
import android.util.Log
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
|
@ -25,6 +29,7 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.toSpanned
|
||||
import androidx.core.widget.ContentLoadingProgressBar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.navigation.fragment.findNavController
|
||||
|
@ -179,6 +184,36 @@ object AppUtils {
|
|||
touchSlopField.set(recyclerView, touchSlop * f) // "8" was obtained experimentally
|
||||
}
|
||||
|
||||
fun ContentLoadingProgressBar?.animateProgressTo(to: Int) {
|
||||
if (this == null) return
|
||||
val animation: ObjectAnimator = ObjectAnimator.ofInt(
|
||||
this,
|
||||
"progress",
|
||||
this.progress,
|
||||
to
|
||||
)
|
||||
animation.duration = 500
|
||||
animation.setAutoCancel(true)
|
||||
animation.interpolator = DecelerateInterpolator()
|
||||
animation.start()
|
||||
}
|
||||
|
||||
fun Context.createNotificationChannel(channelId: String, channelName: String, description: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
||||
val channel =
|
||||
NotificationChannel(channelId, channelName, importance).apply {
|
||||
this.description = description
|
||||
}
|
||||
|
||||
// Register the channel with the system.
|
||||
val notificationManager: NotificationManager =
|
||||
this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun getAllWatchNextPrograms(context: Context): Set<Long> {
|
||||
val COLUMN_WATCH_NEXT_ID_INDEX = 0
|
||||
|
@ -456,6 +491,12 @@ object AppUtils {
|
|||
}
|
||||
}
|
||||
|
||||
fun Context.isNetworkAvailable(): Boolean {
|
||||
val manager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val activeNetworkInfo = manager.activeNetworkInfo
|
||||
return activeNetworkInfo != null && activeNetworkInfo.isConnected || manager.allNetworkInfo?.any { it.isConnected } ?: false
|
||||
}
|
||||
|
||||
fun splitQuery(url: URL): Map<String, String> {
|
||||
val queryPairs: MutableMap<String, String> = LinkedHashMap()
|
||||
val query: String = url.query
|
||||
|
@ -780,4 +821,4 @@ object AppUtils {
|
|||
}
|
||||
return currentAudioFocusRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.APIHolder.capitalize
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.DubStatus
|
||||
import com.lagradost.cloudstream3.SearchQuality
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
|
@ -20,6 +17,7 @@ const val VIDEO_POS_DUR = "video_pos_dur"
|
|||
const val VIDEO_WATCH_STATE = "video_watch_state"
|
||||
const val RESULT_WATCH_STATE = "result_watch_state"
|
||||
const val RESULT_WATCH_STATE_DATA = "result_watch_state_data"
|
||||
const val RESULT_SUBSCRIBED_STATE_DATA = "result_subscribed_state_data"
|
||||
const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // changed due to id changes
|
||||
const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching"
|
||||
const val RESULT_RESUME_WATCHING_HAS_MIGRATED = "result_resume_watching_migrated"
|
||||
|
@ -42,6 +40,37 @@ object DataStoreHelper {
|
|||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to display notifications on new episodes and posters in library.
|
||||
**/
|
||||
data class SubscribedData(
|
||||
@JsonProperty("id") override var id: Int?,
|
||||
@JsonProperty("subscribedTime") val bookmarkedTime: Long,
|
||||
@JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long,
|
||||
@JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map<DubStatus, Int?>,
|
||||
@JsonProperty("name") override val name: String,
|
||||
@JsonProperty("url") override val url: String,
|
||||
@JsonProperty("apiName") override val apiName: String,
|
||||
@JsonProperty("type") override var type: TvType? = null,
|
||||
@JsonProperty("posterUrl") override var posterUrl: String?,
|
||||
@JsonProperty("year") val year: Int?,
|
||||
@JsonProperty("quality") override var quality: SearchQuality? = null,
|
||||
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
|
||||
) : SearchResponse {
|
||||
fun toLibraryItem(): SyncAPI.LibraryItem? {
|
||||
return SyncAPI.LibraryItem(
|
||||
name,
|
||||
url,
|
||||
id?.toString() ?: return null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
latestUpdatedTime,
|
||||
apiName, type, posterUrl, posterHeaders, quality, this.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class BookmarkedData(
|
||||
@JsonProperty("id") override var id: Int?,
|
||||
@JsonProperty("bookmarkedTime") val bookmarkedTime: Long,
|
||||
|
@ -63,7 +92,7 @@ object DataStoreHelper {
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
latestUpdatedTime,
|
||||
apiName, type, posterUrl, posterHeaders, quality, this.id
|
||||
)
|
||||
}
|
||||
|
@ -75,9 +104,7 @@ object DataStoreHelper {
|
|||
@JsonProperty("apiName") override val apiName: String,
|
||||
@JsonProperty("type") override var type: TvType? = null,
|
||||
@JsonProperty("posterUrl") override var posterUrl: String?,
|
||||
|
||||
@JsonProperty("watchPos") val watchPos: PosDur?,
|
||||
|
||||
@JsonProperty("id") override var id: Int?,
|
||||
@JsonProperty("parentId") val parentId: Int?,
|
||||
@JsonProperty("episode") val episode: Int?,
|
||||
|
@ -204,6 +231,41 @@ object DataStoreHelper {
|
|||
return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
|
||||
}
|
||||
|
||||
fun getAllSubscriptions(): List<SubscribedData> {
|
||||
return getKeys("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA")?.mapNotNull {
|
||||
getKey(it)
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
fun removeSubscribedData(id: Int?) {
|
||||
if (id == null) return
|
||||
AccountManager.localListApi.requireLibraryRefresh = true
|
||||
removeKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new seen episodes and update time
|
||||
**/
|
||||
fun updateSubscribedData(id: Int?, data: SubscribedData?, episodeResponse: EpisodeResponse?) {
|
||||
if (id == null || data == null || episodeResponse == null) return
|
||||
val newData = data.copy(
|
||||
latestUpdatedTime = unixTimeMS,
|
||||
lastSeenEpisodeCount = episodeResponse.getLatestEpisodes()
|
||||
)
|
||||
setKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString(), newData)
|
||||
}
|
||||
|
||||
fun setSubscribedData(id: Int?, data: SubscribedData) {
|
||||
if (id == null) return
|
||||
setKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString(), data)
|
||||
AccountManager.localListApi.requireLibraryRefresh = true
|
||||
}
|
||||
|
||||
fun getSubscribedData(id: Int?): SubscribedData? {
|
||||
if (id == null) return null
|
||||
return getKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString())
|
||||
}
|
||||
|
||||
fun setViewPos(id: Int?, pos: Long, dur: Long) {
|
||||
if (id == null) return
|
||||
if (dur < 30_000) return // too short
|
||||
|
|
|
@ -14,6 +14,7 @@ import androidx.core.app.NotificationCompat
|
|||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -47,24 +48,12 @@ class PackageInstallerService : Service() {
|
|||
.setSmallIcon(R.drawable.rdload)
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
||||
val channel =
|
||||
NotificationChannel(UPDATE_CHANNEL_ID, UPDATE_CHANNEL_NAME, importance).apply {
|
||||
description = UPDATE_CHANNEL_DESCRIPTION
|
||||
}
|
||||
|
||||
// Register the channel with the system
|
||||
val notificationManager: NotificationManager =
|
||||
this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
createNotificationChannel()
|
||||
this.createNotificationChannel(
|
||||
UPDATE_CHANNEL_ID,
|
||||
UPDATE_CHANNEL_NAME,
|
||||
UPDATE_CHANNEL_DESCRIPTION
|
||||
)
|
||||
startForeground(UPDATE_NOTIFICATION_ID, baseNotification.build())
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,267 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import kotlinx.coroutines.*
|
||||
import org.junit.Assert
|
||||
|
||||
object TestingUtils {
|
||||
open class TestResult(val success: Boolean) {
|
||||
companion object {
|
||||
val Pass = TestResult(true)
|
||||
val Fail = TestResult(false)
|
||||
}
|
||||
}
|
||||
|
||||
class TestResultSearch(val results: List<SearchResponse>) : TestResult(true)
|
||||
class TestResultLoad(val extractorData: String) : TestResult(true)
|
||||
|
||||
class TestResultProvider(success: Boolean, val log: String, val exception: Throwable?) :
|
||||
TestResult(success)
|
||||
|
||||
@Throws(AssertionError::class, CancellationException::class)
|
||||
suspend fun testHomepage(
|
||||
api: MainAPI,
|
||||
logger: (String) -> Unit
|
||||
): TestResult {
|
||||
if (api.hasMainPage) {
|
||||
try {
|
||||
val f = api.mainPage.first()
|
||||
val homepage =
|
||||
api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages))
|
||||
when {
|
||||
homepage == null -> {
|
||||
logger.invoke("Homepage provider ${api.name} did not correctly load homepage!")
|
||||
}
|
||||
homepage.items.isEmpty() -> {
|
||||
logger.invoke("Homepage provider ${api.name} does not contain any items!")
|
||||
}
|
||||
homepage.items.any { it.list.isEmpty() } -> {
|
||||
logger.invoke("Homepage provider ${api.name} does not have any items on result!")
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is NotImplementedError) {
|
||||
Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented")
|
||||
} else if (e is CancellationException) {
|
||||
throw e
|
||||
}
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
return TestResult.Pass
|
||||
}
|
||||
|
||||
@Throws(AssertionError::class, CancellationException::class)
|
||||
private suspend fun testSearch(
|
||||
api: MainAPI
|
||||
): TestResult {
|
||||
val searchQueries = listOf("over", "iron", "guy")
|
||||
val searchResults = searchQueries.firstNotNullOfOrNull { query ->
|
||||
try {
|
||||
api.search(query).takeIf { !it.isNullOrEmpty() }
|
||||
} catch (e: Throwable) {
|
||||
if (e is NotImplementedError) {
|
||||
Assert.fail("Provider has not implemented search()")
|
||||
} else if (e is CancellationException) {
|
||||
throw e
|
||||
}
|
||||
logError(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return if (searchResults.isNullOrEmpty()) {
|
||||
Assert.fail("Api ${api.name} did not return any valid search responses")
|
||||
TestResult.Fail // Should not be reached
|
||||
} else {
|
||||
TestResultSearch(searchResults)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Throws(AssertionError::class, CancellationException::class)
|
||||
private suspend fun testLoad(
|
||||
api: MainAPI,
|
||||
result: SearchResponse,
|
||||
logger: (String) -> Unit
|
||||
): TestResult {
|
||||
try {
|
||||
Assert.assertEquals(
|
||||
"Invalid apiName on SearchResponse on ${api.name}",
|
||||
result.apiName,
|
||||
api.name
|
||||
)
|
||||
|
||||
val loadResponse = api.load(result.url)
|
||||
|
||||
if (loadResponse == null) {
|
||||
logger.invoke("Returned null loadResponse on ${result.url} on ${api.name}")
|
||||
return TestResult.Fail
|
||||
}
|
||||
|
||||
Assert.assertEquals(
|
||||
"Invalid apiName on LoadResponse on ${api.name}",
|
||||
loadResponse.apiName,
|
||||
result.apiName
|
||||
)
|
||||
Assert.assertTrue(
|
||||
"Api ${api.name} on load does not contain any of the supportedTypes: ${loadResponse.type}",
|
||||
api.supportedTypes.contains(loadResponse.type)
|
||||
)
|
||||
|
||||
val url = when (loadResponse) {
|
||||
is AnimeLoadResponse -> {
|
||||
val gotNoEpisodes =
|
||||
loadResponse.episodes.keys.isEmpty() || loadResponse.episodes.keys.any { loadResponse.episodes[it].isNullOrEmpty() }
|
||||
|
||||
if (gotNoEpisodes) {
|
||||
logger.invoke("Api ${api.name} got no episodes on ${loadResponse.url}")
|
||||
return TestResult.Fail
|
||||
}
|
||||
|
||||
(loadResponse.episodes[loadResponse.episodes.keys.firstOrNull()])?.firstOrNull()?.data
|
||||
}
|
||||
is MovieLoadResponse -> {
|
||||
val gotNoEpisodes = loadResponse.dataUrl.isBlank()
|
||||
if (gotNoEpisodes) {
|
||||
logger.invoke("Api ${api.name} got no movie on ${loadResponse.url}")
|
||||
return TestResult.Fail
|
||||
}
|
||||
|
||||
loadResponse.dataUrl
|
||||
}
|
||||
is TvSeriesLoadResponse -> {
|
||||
val gotNoEpisodes = loadResponse.episodes.isEmpty()
|
||||
if (gotNoEpisodes) {
|
||||
logger.invoke("Api ${api.name} got no episodes on ${loadResponse.url}")
|
||||
return TestResult.Fail
|
||||
}
|
||||
loadResponse.episodes.firstOrNull()?.data
|
||||
}
|
||||
is LiveStreamLoadResponse -> {
|
||||
loadResponse.dataUrl
|
||||
}
|
||||
else -> {
|
||||
logger.invoke("Unknown load response: ${loadResponse.javaClass.name}")
|
||||
return TestResult.Fail
|
||||
}
|
||||
} ?: return TestResult.Fail
|
||||
|
||||
return TestResultLoad(url)
|
||||
|
||||
// val loadTest = testLoadResponse(api, load, logger)
|
||||
// if (loadTest is TestResultLoad) {
|
||||
// testLinkLoading(api, loadTest.extractorData, logger).success
|
||||
// } else {
|
||||
// false
|
||||
// }
|
||||
// if (!validResults) {
|
||||
// logger("Api ${api.name} did not load on the first search results: ${smallSearchResults.map { it.name }}")
|
||||
// }
|
||||
|
||||
// return TestResult(validResults)
|
||||
} catch (e: Throwable) {
|
||||
if (e is NotImplementedError) {
|
||||
Assert.fail("Provider has not implemented load()")
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(AssertionError::class, CancellationException::class)
|
||||
private suspend fun testLinkLoading(
|
||||
api: MainAPI,
|
||||
url: String?,
|
||||
logger: (String) -> Unit
|
||||
): TestResult {
|
||||
Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
|
||||
if (url == null) return TestResult.Fail // Should never trigger
|
||||
|
||||
var linksLoaded = 0
|
||||
try {
|
||||
val success = api.loadLinks(url, false, {}) { link ->
|
||||
logger.invoke("Video loaded: ${link.name}")
|
||||
Assert.assertTrue(
|
||||
"Api ${api.name} returns link with invalid url ${link.url}",
|
||||
link.url.length > 4
|
||||
)
|
||||
linksLoaded++
|
||||
}
|
||||
if (success) {
|
||||
logger.invoke("Links loaded: $linksLoaded")
|
||||
return TestResult(linksLoaded > 0)
|
||||
} else {
|
||||
Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
when (e) {
|
||||
is NotImplementedError -> {
|
||||
Assert.fail("Provider has not implemented loadLinks()")
|
||||
}
|
||||
else -> {
|
||||
logger.invoke("Failed link loading on ${api.name} using data: $url")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
return TestResult.Pass
|
||||
}
|
||||
|
||||
fun getDeferredProviderTests(
|
||||
scope: CoroutineScope,
|
||||
providers: List<MainAPI>,
|
||||
logger: (String) -> Unit,
|
||||
callback: (MainAPI, TestResultProvider) -> Unit
|
||||
) {
|
||||
providers.forEach { api ->
|
||||
scope.launch {
|
||||
var log = ""
|
||||
fun addToLog(string: String) {
|
||||
log += string + "\n"
|
||||
logger.invoke(string)
|
||||
}
|
||||
fun getLog(): String {
|
||||
return log.removeSuffix("\n")
|
||||
}
|
||||
|
||||
val result = try {
|
||||
addToLog("Trying ${api.name}")
|
||||
|
||||
// Test Homepage
|
||||
val homepage = testHomepage(api, logger).success
|
||||
Assert.assertTrue("Homepage failed to load", homepage)
|
||||
|
||||
// Test Search Results
|
||||
val searchResults = testSearch(api)
|
||||
Assert.assertTrue("Failed to get search results", searchResults.success)
|
||||
searchResults as TestResultSearch
|
||||
|
||||
// Test Load and LoadLinks
|
||||
// Only try the first 3 search results to prevent spamming
|
||||
val success = searchResults.results.take(3).any { searchResponse ->
|
||||
addToLog("Testing search result: ${searchResponse.url}")
|
||||
val loadResponse = testLoad(api, searchResponse, ::addToLog)
|
||||
if (loadResponse !is TestResultLoad) {
|
||||
false
|
||||
} else {
|
||||
testLinkLoading(api, loadResponse.extractorData, ::addToLog).success
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
logger.invoke("Success ${api.name}")
|
||||
TestResultProvider(true, getLog(), null)
|
||||
} else {
|
||||
logger.invoke("Error ${api.name}")
|
||||
TestResultProvider(false, getLog(), null)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
TestResultProvider(false, getLog(), e)
|
||||
}
|
||||
callback.invoke(api, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ import androidx.work.Data
|
|||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
|
@ -213,7 +214,7 @@ object VideoDownloadManager {
|
|||
}
|
||||
|
||||
private val cachedBitmaps = hashMapOf<String, Bitmap>()
|
||||
private fun Context.getImageBitmapFromUrl(url: String): Bitmap? {
|
||||
fun Context.getImageBitmapFromUrl(url: String, headers: Map<String, String>? = null): Bitmap? {
|
||||
try {
|
||||
if (cachedBitmaps.containsKey(url)) {
|
||||
return cachedBitmaps[url]
|
||||
|
@ -221,12 +222,14 @@ object VideoDownloadManager {
|
|||
|
||||
val bitmap = GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.load(url).into(720, 720)
|
||||
.load(GlideUrl(url) { headers ?: emptyMap() })
|
||||
.into(720, 720)
|
||||
.get()
|
||||
|
||||
if (bitmap != null) {
|
||||
cachedBitmaps[url] = bitmap
|
||||
}
|
||||
return null
|
||||
return bitmap
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
return null
|
||||
|
@ -426,7 +429,7 @@ object VideoDownloadManager {
|
|||
}
|
||||
|
||||
private const val reservedChars = "|\\?*<\":>+[]/\'"
|
||||
fun sanitizeFilename(name: String, removeSpaces: Boolean= false): String {
|
||||
fun sanitizeFilename(name: String, removeSpaces: Boolean = false): String {
|
||||
var tempName = name
|
||||
for (c in reservedChars) {
|
||||
tempName = tempName.replace(c, ' ')
|
||||
|
@ -1612,7 +1615,7 @@ object VideoDownloadManager {
|
|||
.mapIndexed { index, any -> DownloadQueueResumePackage(index, any) }
|
||||
.toTypedArray()
|
||||
setKey(KEY_RESUME_QUEUE_PACKAGES, dQueue)
|
||||
} catch (t : Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<vector 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="M12,14.67L3.41,6.09L2,7.5l8.5,8.5H4v2h16v-2h-6.5l5.15,-5.15C18.91,10.95 19.2,11 19.5,11c1.38,0 2.5,-1.12 2.5,-2.5S20.88,6 19.5,6S17,7.12 17,8.5c0,0.35 0.07,0.67 0.2,0.97L12,14.67z"/>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector 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="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.63,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.64,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2zM16,17L8,17v-6c0,-2.48 1.51,-4.5 4,-4.5s4,2.02 4,4.5v6z"/>
|
||||
</vector>
|
|
@ -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="?attr/white" android:pathData="M20.41,8.41l-4.83,-4.83C15.21,3.21 14.7,3 14.17,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V9.83C21,9.3 20.79,8.79 20.41,8.41zM7,7h7v2H7V7zM17,17H7v-2h10V17zM17,13H7v-2h10V13z"/>
|
||||
</vector>
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="50"
|
||||
android:viewportHeight="50"
|
||||
android:name="vector">
|
||||
<group android:scaleX="0.1755477"
|
||||
android:scaleY="0.1755477"
|
||||
android:translateX="0"
|
||||
android:translateY="0">
|
||||
<path android:name="path"
|
||||
|
||||
android:pathData="M 245.05 148.63 C 242.249 148.627 239.463 149.052 236.79 149.89 C 235.151 141.364 230.698 133.63 224.147 127.931 C 217.597 122.233 209.321 118.893 200.65 118.45 C 195.913 105.431 186.788 94.458 174.851 87.427 C 162.914 80.396 148.893 77.735 135.21 79.905 C 121.527 82.074 109.017 88.941 99.84 99.32 C 89.871 95.945 79.051 96.024 69.133 99.545 C 59.215 103.065 50.765 109.826 45.155 118.73 C 39.545 127.634 37.094 138.174 38.2 148.64 L 37.94 148.64 C 30.615 148.64 23.582 151.553 18.403 156.733 C 13.223 161.912 10.31 168.945 10.31 176.27 C 10.31 183.595 13.223 190.628 18.403 195.807 C 23.582 200.987 30.615 203.9 37.94 203.9 L 245.05 203.9 C 252.375 203.9 259.408 200.987 264.587 195.807 C 269.767 190.628 272.68 183.595 272.68 176.27 C 272.68 168.945 269.767 161.912 264.587 156.733 C 259.408 151.553 252.375 148.64 245.05 148.64 Z"
|
||||
android:fillColor="#FFFFFF" android:strokeWidth="1"
|
||||
tools:ignore="VectorPath"
|
||||
android:fillAlpha="0.55"/>
|
||||
<path android:name="path_1" android:pathData="M 208.61 125 C 208.61 123.22 208.55 121.45 208.48 119.69 C 205.919 119.01 203.296 118.595 200.65 118.45 C 195.913 105.431 186.788 94.458 174.851 87.427 C 162.914 80.396 148.893 77.735 135.21 79.905 C 121.527 82.074 109.017 88.941 99.84 99.32 C 89.871 95.945 79.051 96.024 69.133 99.545 C 59.215 103.065 50.765 109.826 45.155 118.73 C 39.545 127.634 37.094 138.174 38.2 148.64 L 37.94 148.64 C 30.615 148.64 23.582 151.553 18.403 156.733 C 13.223 161.912 10.31 168.945 10.31 176.27 C 10.31 183.595 13.223 190.628 18.403 195.807 C 23.582 200.987 30.615 203.9 37.94 203.9 L 179 203.9 C 198.116 182.073 208.646 154.015 208.61 125 Z"
|
||||
android:fillColor="#FFFFFF" android:strokeWidth="1"
|
||||
android:fillAlpha="0.55"/>
|
||||
<path android:name="path_2" android:pathData="M 99.84 99.32 C 89.871 95.945 79.051 96.024 69.133 99.545 C 59.215 103.065 50.765 109.826 45.155 118.73 C 39.545 127.634 37.094 138.174 38.2 148.64 L 37.94 148.64 C 30.783 148.665 23.909 151.471 18.779 156.461 C 13.648 161.452 10.653 168.246 10.43 175.399 C 10.207 182.553 12.773 189.52 17.583 194.82 C 22.392 200.121 29.079 203.349 36.22 203.82 C 67.216 202.93 96.673 189.98 118.284 167.742 C 139.895 145.504 151.997 115.689 152 84.68 C 152 83 151.94 81.33 151.87 79.68 C 149.443 79.361 146.998 79.194 144.55 79.18 C 136.095 79.171 127.735 80.962 120.026 84.434 C 112.317 87.907 105.435 92.982 99.84 99.32 Z"
|
||||
android:fillColor="#FFFFFF" android:strokeWidth="1"
|
||||
android:fillAlpha="1"/>
|
||||
</group>
|
||||
|
||||
</vector>
|
|
@ -1,34 +1,34 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text1"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:textStyle="bold"
|
||||
android:textSize="20sp"
|
||||
android:textColor="?attr/textColor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
tools:text="Test"
|
||||
android:layout_height="wrap_content" />
|
||||
android:id="@+id/text1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="Test" />
|
||||
|
||||
<ListView
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:nextFocusLeft="@id/apply_btt"
|
||||
|
||||
android:id="@+id/listview1"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:paddingTop="10dp"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:listitem="@layout/sort_bottom_single_choice_no_checkmark"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_rowWeight="1" />
|
||||
android:id="@+id/listview1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:nestedScrollingEnabled="true"
|
||||
android:nextFocusLeft="@id/apply_btt"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:paddingTop="10dp"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:listitem="@layout/sort_bottom_single_choice_no_checkmark" />
|
||||
</LinearLayout>
|
||||
|
|
|
@ -129,9 +129,9 @@
|
|||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/result_scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:paddingBottom="100dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:layout_height="wrap_content">
|
||||
android:paddingBottom="100dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
@ -326,13 +326,12 @@
|
|||
|
||||
<ImageView
|
||||
android:id="@+id/result_poster"
|
||||
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="140dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:contentDescription="@string/result_poster_img_des"
|
||||
android:foreground="@drawable/outline_drawable"
|
||||
android:scaleType="centerCrop"
|
||||
android:layout_gravity="bottom"
|
||||
tools:src="@drawable/example_poster" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
|
@ -516,8 +515,8 @@
|
|||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
style="@style/ChipParent"
|
||||
android:id="@+id/result_tag"
|
||||
style="@style/ChipParent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
<!--<com.lagradost.cloudstream3.widget.FlowLayout
|
||||
|
@ -818,10 +817,13 @@
|
|||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:drawableEnd="@drawable/ic_baseline_keyboard_arrow_down_24"
|
||||
android:nextFocusLeft="@id/result_episode_select"
|
||||
android:nextFocusRight="@id/result_episode_select"
|
||||
android:nextFocusUp="@id/result_description"
|
||||
android:nextFocusDown="@id/result_episodes"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:visibility="gone"
|
||||
tools:text="Season 1"
|
||||
tools:visibility="visible" />
|
||||
|
@ -829,16 +831,16 @@
|
|||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/result_episode_select"
|
||||
style="@style/MultiSelectButton"
|
||||
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
|
||||
android:drawableEnd="@drawable/ic_baseline_keyboard_arrow_down_24"
|
||||
android:nextFocusLeft="@id/result_season_button"
|
||||
android:nextFocusRight="@id/result_season_button"
|
||||
|
||||
android:nextFocusUp="@id/result_description"
|
||||
android:nextFocusDown="@id/result_episodes"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:visibility="gone"
|
||||
tools:text="50-100"
|
||||
tools:visibility="visible" />
|
||||
|
@ -846,15 +848,16 @@
|
|||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/result_dub_select"
|
||||
style="@style/MultiSelectButton"
|
||||
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:drawableEnd="@drawable/ic_baseline_keyboard_arrow_down_24"
|
||||
android:nextFocusLeft="@id/result_season_button"
|
||||
android:nextFocusRight="@id/result_season_button"
|
||||
|
||||
android:nextFocusUp="@id/result_description"
|
||||
android:nextFocusDown="@id/result_episodes"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:visibility="gone"
|
||||
tools:text="Dubbed"
|
||||
tools:visibility="visible" />
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:id="@+id/media_route_button_holder"
|
||||
android:animateLayoutChanges="true"
|
||||
android:layout_gravity="center_vertical|end">
|
||||
|
||||
<androidx.mediarouter.app.MediaRouteButton
|
||||
|
@ -69,15 +70,35 @@
|
|||
app:mediaRouteButtonTint="?attr/textColor" />
|
||||
|
||||
<ImageView
|
||||
android:visibility="gone"
|
||||
android:nextFocusUp="@id/result_back"
|
||||
android:nextFocusDown="@id/result_description"
|
||||
android:nextFocusLeft="@id/result_add_sync"
|
||||
android:nextFocusRight="@id/result_share"
|
||||
|
||||
tools:visibility="visible"
|
||||
|
||||
android:id="@+id/result_subscribe"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:layout_margin="5dp"
|
||||
android:elevation="10dp"
|
||||
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/baseline_notifications_none_24"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
app:tint="?attr/textColor" />
|
||||
|
||||
<ImageView
|
||||
android:nextFocusUp="@id/result_back"
|
||||
android:nextFocusDown="@id/result_description"
|
||||
android:nextFocusLeft="@id/result_subscribe"
|
||||
android:nextFocusRight="@id/result_open_in_browser"
|
||||
|
||||
android:id="@+id/result_share"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:layout_margin="5dp"
|
||||
android:elevation="10dp"
|
||||
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:backgroundTint="?attr/primaryBlackBackground"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/provider_test_appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/settings_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
android:paddingTop="@dimen/navbar_height"
|
||||
app:layout_scrollFlags="scroll|enterAlways"
|
||||
app:navigationIconTint="?attr/iconColor"
|
||||
app:titleTextColor="?attr/textColor"
|
||||
tools:title="@string/category_provider_test">
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
<com.lagradost.cloudstream3.ui.settings.testing.TestView
|
||||
android:id="@+id/provider_test"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:nextFocusDown="@id/provider_test_recycler_view"
|
||||
app:header_text="@string/category_provider_test"
|
||||
app:layout_scrollFlags="scroll|enterAlways" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/provider_test_recycler_view"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="60dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/provider_test_item" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -0,0 +1,71 @@
|
|||
<?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="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:nextFocusRight="@id/action_button"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/main_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="16sp"
|
||||
tools:text="Test repository" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/lang_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="5dp"
|
||||
tools:text="🇷🇼"
|
||||
android:textColor="?attr/grayTextColor"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/passed_failed_marker"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Failed"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fail_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?attr/grayTextColor"
|
||||
tools:text="Unable to load videos"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/action_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:layout_marginStart="10dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/baseline_text_snippet_24"
|
||||
app:tint="?attr/white" />
|
||||
</LinearLayout>
|
|
@ -0,0 +1,138 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView 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"
|
||||
style="@style/Widget.Material3.CardView.Elevated"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="?attr/primaryBlackBackground">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="10dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/main_test_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/outline_drawable_less"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="10dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/main_test_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="17sp"
|
||||
tools:text="Homepage test" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/main_test_section_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:textSize="17sp"
|
||||
tools:text="67 / 120 " />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="10dp"
|
||||
app:cardBackgroundColor="?attr/primaryGrayBackground"
|
||||
app:cardCornerRadius="@dimen/rounded_image_radius">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/passed_test_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/outline_drawable_less"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="10dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Tests passed"
|
||||
android:textSize="17sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/passed_test_section_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:textSize="17sp"
|
||||
tools:text="55" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/failed_test_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/outline_drawable_less"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="10dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Tests failed"
|
||||
android:textSize="17sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/failed_test_section_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:textSize="17sp"
|
||||
tools:text="12" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/tests_play_pause"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_margin="10dp"
|
||||
android:text="@string/start"
|
||||
app:icon="@drawable/ic_baseline_play_arrow_24">
|
||||
|
||||
</com.google.android.material.button.MaterialButton>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<androidx.core.widget.ContentLoadingProgressBar
|
||||
android:id="@+id/test_total_progress"
|
||||
style="@android:style/Widget.Material.ProgressBar.Horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginBottom="-1.5dp"
|
||||
android:progressBackgroundTint="?attr/colorPrimary"
|
||||
android:progressTint="?attr/colorPrimary"
|
||||
android:visibility="gone"
|
||||
tools:progress="50" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
|
@ -519,6 +519,23 @@
|
|||
app:popExitAnim="@anim/exit_anim"
|
||||
tools:layout="@layout/fragment_player" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_test_providers"
|
||||
android:name="com.lagradost.cloudstream3.ui.settings.testing.TestFragment"
|
||||
android:layout_height="match_parent"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim"
|
||||
tools:layout="@layout/fragment_testing">
|
||||
<action
|
||||
android:id="@+id/action_navigation_global_to_navigation_test_providers"
|
||||
app:destination="@id/navigation_test_providers"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_setup_language"
|
||||
|
|
|
@ -103,7 +103,7 @@
|
|||
<string name="action_open_watching">مزيد من المعلومات</string>
|
||||
<string name="vpn_might_be_needed">قد تكون هناك حاجة إلى VPN لكي يعمل هذا المزود بشكل صحيح</string>
|
||||
<string name="vpn_torrent">هذا المزود هو تورنت ، يوصى باستخدام شبكة ظاهرية خاصة</string>
|
||||
<string name="provider_info_meta">لا يتم توفير البيانات الوصفية بواسطة الموقع ، وسيفشل تحميل الفيديو إذا لم يكن موجودًا في الموقع.</string>
|
||||
<string name="provider_info_meta">لا يتم توفير البيانات الوصفية بواسطة الموقع، وسيفشل تحميل الفيديو إذا لم يكن موجودًا في الموقع.</string>
|
||||
<string name="torrent_plot">الوصف</string>
|
||||
<string name="normal_no_plot">لم يتم العثور على وصف</string>
|
||||
<string name="torrent_no_plot">لم يتم العثور على وصف</string>
|
||||
|
@ -170,7 +170,7 @@
|
|||
<string name="copy_link_toast">تم نسخ الرابط إلى الحافظة</string>
|
||||
<string name="play_episode_toast">تشغيل الحلقة</string>
|
||||
<string name="subs_default_reset_toast">إعادة التعيين إلى القيمة الافتراضية</string>
|
||||
<string name="acra_report_toast">عذرا ، تعطل التطبيق. سيتم إرسال تقرير خطأ مجهول إلى المطورين</string>
|
||||
<string name="acra_report_toast">عذرا، تعطل التطبيق. سيتم إرسال تقرير خطأ مجهول إلى المطورين</string>
|
||||
<string name="season">موسم</string>
|
||||
<string name="no_season">لا موسم</string>
|
||||
<string name="episode">حلقة</string>
|
||||
|
@ -266,8 +266,8 @@
|
|||
<string name="video_buffer_length_settings">طول التخزين المؤقت</string>
|
||||
<string name="video_buffer_disk_settings">التخزين المؤقت للفيديو على القرص</string>
|
||||
<string name="video_buffer_clear_settings">مسح التخزين المؤقت للصورة والفيديو</string>
|
||||
<string name="video_ram_description">يتسبب في حدوث أعطال إذا تم ضبطه على مستوى مرتفع جدا على الأجهزة ذات الذاكرة المنخفضة ، مثل Android TV.</string>
|
||||
<string name="video_disk_description">يسبب مشاكل إذا تم ضبطه على مستوى مرتفع جدا على الأجهزة ذات مساحة التخزين المنخفضة ، مثل Android TV.</string>
|
||||
<string name="video_ram_description">يتسبب في حدوث أعطال إذا تم ضبطه على مستوى مرتفع جدا على الأجهزة ذات الذاكرة المنخفضة، مثل تلفزيون أندرويد.</string>
|
||||
<string name="video_disk_description">يسبب مشاكل إذا تم ضبطه على مستوى مرتفع جدا على الأجهزة ذات مساحة التخزين المنخفضة، مثل تلفزيون أندرويد.</string>
|
||||
<string name="dns_pref">إستخدام DNS بدلا من HTTPS</string>
|
||||
<string name="dns_pref_summary">مفيد لتجاوز حجب مزود خدمة الإنترنت</string>
|
||||
<string name="add_site_pref">موقع بديل (نسخة)</string>
|
||||
|
@ -360,7 +360,7 @@
|
|||
https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog
|
||||
-->
|
||||
<string name="subtitles_example_text">نصٌّ حكيمٌ لهُ سِرٌّ قاطِعٌ وَذُو شَأنٍ عَظيمٍ مكتوبٌ على ثوبٍ أخضرَ ومُغلفٌ بجلدٍ أزرق</string>
|
||||
<string name="recommended">مُوصي به</string>
|
||||
<string name="recommended">مُوصى به</string>
|
||||
<string name="player_loaded_subtitles" formatted="true">تم تحميل %s</string>
|
||||
<string name="player_load_subtitles">إختيار ملف</string>
|
||||
<string name="player_load_subtitles_online">تحميل من الانترنت</string>
|
||||
|
@ -543,4 +543,16 @@
|
|||
<string name="pref_category_android_tv">تلفزيون أندرويد</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">مدة التقديم عنما يكون المشغل مرئيا</string>
|
||||
<string name="android_tv_interface_on_seek_settings">مدة التقديم- المشغل المرئي</string>
|
||||
<string name="test_failed">فشل</string>
|
||||
<string name="test_passed">نجح</string>
|
||||
<string name="category_provider_test">إختبار المزود</string>
|
||||
<string name="restart">إعادة التشغيل</string>
|
||||
<string name="test_log">سجل</string>
|
||||
<string name="start">بَدأ</string>
|
||||
<string name="stop">إيقاف</string>
|
||||
<string name="subscription_in_progress_notification">تحديث العروض التي تم الاشتراك فيها</string>
|
||||
<string name="subscription_deleted">إلغاء الاشتراك من %s</string>
|
||||
<string name="subscription_episode_released">تم إصدار الحلقة %d!</string>
|
||||
<string name="subscription_list_name">مشترك</string>
|
||||
<string name="subscription_new">مشترك في %s</string>
|
||||
</resources>
|
|
@ -535,4 +535,16 @@
|
|||
<string name="android_tv_interface_on_seek_settings">Zobrazený přehrávač - doba hledání</string>
|
||||
<string name="pref_category_android_tv">Android TV</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">Množství vyhledávané doby při zobrazeném přehrávači</string>
|
||||
<string name="test_log">Protokol</string>
|
||||
<string name="category_provider_test">Test poskytovatele</string>
|
||||
<string name="test_failed">Neúspěšné</string>
|
||||
<string name="test_passed">Úspěšné</string>
|
||||
<string name="restart">Restart</string>
|
||||
<string name="start">Spustit</string>
|
||||
<string name="stop">Zastavit</string>
|
||||
<string name="subscription_in_progress_notification">Aktualizace odebíraných pořadů</string>
|
||||
<string name="subscription_new">Přihlášeno k odběru %s</string>
|
||||
<string name="subscription_deleted">Odhlášen odběr od %s</string>
|
||||
<string name="subscription_episode_released">Byla vydána epizoda %d!</string>
|
||||
<string name="subscription_list_name">Odebíráno</string>
|
||||
</resources>
|
|
@ -53,7 +53,7 @@
|
|||
<string name="type_dropped">Abgebrochen</string>
|
||||
<string name="type_plan_to_watch">Geplant</string>
|
||||
<string name="type_none">Nichts</string>
|
||||
<string name="type_re_watching">Erneut anschauen</string>
|
||||
<string name="type_re_watching">Erneut schauen</string>
|
||||
<string name="play_movie_button">Film abspielen</string>
|
||||
<string name="play_livestream_button">Livestream abspielen</string>
|
||||
<string name="play_torrent_button">Torrent streamen</string>
|
||||
|
@ -212,7 +212,7 @@
|
|||
<string name="no_subtitles">Keine Untertitel</string>
|
||||
<string name="default_subtitles">Standard</string>
|
||||
<string name="free_storage">Frei</string>
|
||||
<string name="used_storage">Benutzt</string>
|
||||
<string name="used_storage">Belegt</string>
|
||||
<string name="app_storage">App</string>
|
||||
<string name="movies">Filme</string>
|
||||
<string name="tv_series">TV-Serien</string>
|
||||
|
@ -284,7 +284,7 @@
|
|||
<string name="resize_fill">Strecken</string>
|
||||
<string name="resize_zoom">Vergrößern</string>
|
||||
<string name="legal_notice">Haftungsausschluss</string>
|
||||
<string name="category_general">General</string>
|
||||
<string name="category_general">Allgemein</string>
|
||||
<string name="random_button_settings">Zufalls-Button</string>
|
||||
<string name="random_button_settings_desc">Zufallsbutton auf der Startseite anzeigen</string>
|
||||
<string name="provider_lang_settings">Anbieter-Sprachen</string>
|
||||
|
@ -460,11 +460,11 @@
|
|||
<string name="automatic_plugin_download_summary">Automatische Installation aller noch nicht installierten Plugins aus hinzugefügten Repositories.</string>
|
||||
<string name="redo_setup_process">Einrichtungsvorgang wiederholen</string>
|
||||
<string name="apk_installer_settings">APK-Installer</string>
|
||||
<string name="apk_installer_settings_des">Einige Telefone unterstützen das neue Installationsprogramm für Pakete nicht. Benutze die Legacy-Option, wenn sich die Updates nicht installieren lassen.</string>
|
||||
<string name="apk_installer_settings_des">Einige Telefone unterstützen den neuen Package-Installer nicht. Benutze die Legacy-Option, wenn sich die Updates nicht installieren lassen.</string>
|
||||
<string name="season_format">%s %d%s</string>
|
||||
<string name="pref_category_links">Links</string>
|
||||
<string name="pref_category_app_updates">App-Updates</string>
|
||||
<string name="pref_category_backup">Back-Up</string>
|
||||
<string name="pref_category_backup">Sicherung</string>
|
||||
<string name="pref_category_extensions">Erweiterungen</string>
|
||||
<string name="pref_category_actions">Wartung</string>
|
||||
<string name="pref_category_cache">Cache</string>
|
||||
|
@ -506,4 +506,16 @@
|
|||
<string name="empty_library_logged_in_message">Diese Liste scheint leer zu sein. Versuche, zu einer anderen Liste zu wechseln.</string>
|
||||
<string name="safe_mode_file">Datei für abgesicherten Modus gefunden!
|
||||
\nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird.</string>
|
||||
<string name="android_tv_interface_off_seek_settings">Player ausgeblendet - Betrag zum vor- und zurückspulen</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">Der Betrag, welcher verwendet wird, wenn der Player eingeblendet ist</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">Der Betrag, welcher verwendet wird, wenn der Player ausgeblendet ist</string>
|
||||
<string name="pref_category_android_tv">Android-TV</string>
|
||||
<string name="android_tv_interface_on_seek_settings">Player eingeblendet - Betrag zum vor- und zurückspulen</string>
|
||||
<string name="test_failed">Fehlgeschlagen</string>
|
||||
<string name="test_passed">Erfolgreich</string>
|
||||
<string name="category_provider_test">Anbieter-Test</string>
|
||||
<string name="stop">Stopp</string>
|
||||
<string name="test_log">Log</string>
|
||||
<string name="start">Start</string>
|
||||
<string name="restart">Neustarten</string>
|
||||
</resources>
|
|
@ -150,7 +150,7 @@
|
|||
<string name="episodes">Επεισόδια</string>
|
||||
<string name="episodes_range">%d-%d</string>
|
||||
<string name="episode_format" formatted="true">%d %s</string>
|
||||
<string name="season_short">Κ</string>
|
||||
<string name="season_short">Σ</string>
|
||||
<string name="episode_short">E</string>
|
||||
<string name="no_episodes_found">Δεν βρέθηκαν επεισόδια</string>
|
||||
<string name="delete_file">Διαγραφή αρχείου</string>
|
||||
|
|
|
@ -511,4 +511,16 @@
|
|||
<string name="pref_category_android_tv">Android TV</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">La cantidad de búsqueda utilizada cuando la jugadora es visible</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">La cantidad de búsqueda utilizada cuando el jugador está oculto</string>
|
||||
<string name="stop">Parar</string>
|
||||
<string name="test_failed">Falló</string>
|
||||
<string name="test_log">Registro</string>
|
||||
<string name="start">Empezar</string>
|
||||
<string name="test_passed">Aprobado</string>
|
||||
<string name="category_provider_test">Prueba del proveedor</string>
|
||||
<string name="restart">Reiniciar</string>
|
||||
<string name="subscription_list_name">Suscrito</string>
|
||||
<string name="subscription_new">Suscrito a %s</string>
|
||||
<string name="subscription_deleted">Darse de baja de %s</string>
|
||||
<string name="subscription_in_progress_notification">Actualizando los programas suscritos</string>
|
||||
<string name="subscription_episode_released">¡Episodio %d publicado!</string>
|
||||
</resources>
|
|
@ -6,12 +6,12 @@
|
|||
<string name="title_downloads">Téléchargements</string>
|
||||
<string name="title_settings">Paramètres</string>
|
||||
<string name="search_hint">Rechercher…</string>
|
||||
<string name="search_poster_img_des">Miniature</string>
|
||||
<string name="search_poster_img_des">Affiche</string>
|
||||
<string name="no_data">Aucune Donnée</string>
|
||||
<string name="episode_more_options_des">Plus d\'options</string>
|
||||
<string name="go_back_img_des">Retour</string>
|
||||
<string name="next_episode">Épisode suivant</string>
|
||||
<string name="result_poster_img_des">Miniature</string>
|
||||
<string name="result_poster_img_des">Affiche</string>
|
||||
<string name="result_tags">Genres</string>
|
||||
<string name="result_share">Partager</string>
|
||||
<string name="result_open_in_browser">Ouvrir dans le navigateur</string>
|
||||
|
@ -29,7 +29,7 @@
|
|||
<string name="pick_subtitle">Sous-titres</string>
|
||||
<string name="reload_error">Réessayer la connection…</string>
|
||||
<string name="go_back">Retour</string>
|
||||
<string name="episode_poster_img_des">Miniature de l\'Épisode</string>
|
||||
<string name="episode_poster_img_des">Affiche de l\'épisode</string>
|
||||
<string name="play_episode">Lire l\'Épisode</string>
|
||||
<!--<string name="need_storage">Permet de télécharger les épisodes</string>-->
|
||||
<string name="download">Télécharger</string>
|
||||
|
@ -51,10 +51,10 @@
|
|||
<string name="pref_disable_acra">Désactiver le rapport de bug automatique</string>
|
||||
<string name="home_more_info">Plus d\'informations</string>
|
||||
<string name="home_expanded_hide">Cacher</string>
|
||||
<string name="home_main_poster_img_des">Poster principal</string>
|
||||
<string name="home_main_poster_img_des">Affiche principale</string>
|
||||
<string name="home_play">Lecture</string>
|
||||
<string name="home_info">Info</string>
|
||||
<string name="home_next_random_img_des">Suivant Aléatoire</string>
|
||||
<string name="home_info">Infos</string>
|
||||
<string name="home_next_random_img_des">Aléatoire suivant</string>
|
||||
<string name="home_change_provider_img_des">Changer le fournisseur</string>
|
||||
<string name="filter_bookmarks">Filtrer les marques-pages</string>
|
||||
<string name="error_bookmarks_text">Marque-pages</string>
|
||||
|
@ -211,7 +211,7 @@
|
|||
<string name="actor_background">Arrière plan</string>
|
||||
<string name="home_source">Source</string>
|
||||
<string name="home_random">Aléatoire</string>
|
||||
<string name="coming_soon">À venir …</string>
|
||||
<string name="coming_soon">Bientôt disponible…</string>
|
||||
<string name="poster_image">Image de l\'affiche</string>
|
||||
<string name="authenticated_user">%s Connecté</string>
|
||||
<string name="action_add_to_bookmarks">Définir le statut de visionage</string>
|
||||
|
@ -490,4 +490,22 @@
|
|||
<string name="delayed_update_notice">L\'application sera mise à jour dès la fin de la session</string>
|
||||
<string name="plugin_downloaded">Plugin Téléchargé</string>
|
||||
<string name="action_remove_from_watched">Retirer de la vue</string>
|
||||
<string name="library">Bibliothèque</string>
|
||||
<string name="browser">Navigateur</string>
|
||||
<string name="sort">Trier</string>
|
||||
<string name="sort_rating_asc">Note (basse à haute)</string>
|
||||
<string name="sort_rating_desc">Note (haut à bas)</string>
|
||||
<string name="sort_alphabetical_a">Alphabétique (A à Z)</string>
|
||||
<string name="empty_library_no_accounts_message">On dirait que votre bibliothèque est vide :(
|
||||
\nConnectez-vous à un compte ou ajoutez des séries à votre bibliothèque locale</string>
|
||||
<string name="empty_library_logged_in_message">Il semble que cette liste soit vide, essayez d\'en choisir une autre</string>
|
||||
<string name="pref_category_android_tv">Android TV</string>
|
||||
<string name="sort_by">Trié par</string>
|
||||
<string name="sort_alphabetical_z">Alphabétique (Z à A)</string>
|
||||
<string name="select_library">Sélectionnez la bibliothèque</string>
|
||||
<string name="open_with">Ouvrir avec</string>
|
||||
<string name="sort_updated_new">Mis à jour (Nouveau vers ancien)</string>
|
||||
<string name="sort_updated_old">Mis à jour (ancien vers nouveau)</string>
|
||||
<string name="safe_mode_file">Fichier du mode sans échec trouvé !
|
||||
\nAucune extension ne sera chargée au démarrage avant que le fichier ne soit enlevé.</string>
|
||||
</resources>
|
|
@ -531,4 +531,21 @@
|
|||
<string name="empty_library_logged_in_message">Čini se da je ova lista prazna, pokušajte se prebaciti na drugu</string>
|
||||
<string name="safe_mode_file">Pronađena datoteka sigurnog načina rada!
|
||||
\nNe učitavaju se ekstenzije pri pokretanju dok se datoteka ne ukloni.</string>
|
||||
<string name="android_tv_interface_on_seek_settings">Prikazan player- iznos preskakanja</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">Količina preskakanja koja se koristi kada je player vidljiv</string>
|
||||
<string name="android_tv_interface_off_seek_settings">Player skriven - Količina preskakanja</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">Količina preskakanja koja se koristi kada je player skriven</string>
|
||||
<string name="pref_category_android_tv">Android TV</string>
|
||||
<string name="test_passed">Prošlo</string>
|
||||
<string name="restart">Restart</string>
|
||||
<string name="test_log">Log</string>
|
||||
<string name="start">Početak</string>
|
||||
<string name="test_failed">Neuspješno</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="category_provider_test">Test pružatelja usluga</string>
|
||||
<string name="subscription_in_progress_notification">Ažuriram pretplaćene serije</string>
|
||||
<string name="subscription_episode_released">Epizoda %d izbačena!</string>
|
||||
<string name="subscription_list_name">Pretplaćeno</string>
|
||||
<string name="subscription_new">Pretplaćen na %s</string>
|
||||
<string name="subscription_deleted">Otkazana pretplata sa %s</string>
|
||||
</resources>
|
|
@ -35,7 +35,7 @@
|
|||
<string name="skip_loading">Skip Loading</string>
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="type_watching">Sedang Menonton</string>
|
||||
<string name="type_on_hold">Tertahan</string>
|
||||
<string name="type_on_hold">Tertunda</string>
|
||||
<string name="type_completed">Selesai</string>
|
||||
<string name="type_dropped">Dihentikan</string>
|
||||
<string name="type_plan_to_watch">Rencana untuk Menonton</string>
|
||||
|
@ -387,7 +387,7 @@
|
|||
<string name="episode_format" formatted="true">%d %s</string>
|
||||
<string name="nsfw">17+</string>
|
||||
<string name="others">Lainnya</string>
|
||||
<string name="other_singular">Vidio</string>
|
||||
<string name="other_singular">Video</string>
|
||||
<string name="add_site_pref">Duplikasi Website</string>
|
||||
<string name="add_site_summary">Duplikasi website yang telah ada, dengan alamat berbeda</string>
|
||||
<string name="pref_category_links">Tautan</string>
|
||||
|
@ -395,7 +395,7 @@
|
|||
<string name="pref_category_backup">Cadangkan</string>
|
||||
<string name="pref_category_extensions">Fitur Tambahan</string>
|
||||
<string name="play_with_app_name">Putar di CloudStream</string>
|
||||
<string name="pref_filter_search_quality">Sembunyikan kualitas vidio terpilih di pencarian</string>
|
||||
<string name="pref_filter_search_quality">Sembunyikan kualitas video terpilih di pencarian</string>
|
||||
<string name="season_format">%s %d%s</string>
|
||||
<string name="livestreams">Siaran langsung</string>
|
||||
<string name="remove_site_pref">Hapus Website</string>
|
||||
|
@ -444,7 +444,7 @@
|
|||
<string name="extension_rating" formatted="true">Peringkat: %s</string>
|
||||
<string name="extension_authors">Pembuat</string>
|
||||
<string name="extension_language">Bahasa</string>
|
||||
<string name="player_pref">Pemutar vidio utama</string>
|
||||
<string name="player_pref">Pemutar video utama</string>
|
||||
<string name="player_settings_play_in_app">Pemutar Bawaan</string>
|
||||
<string name="player_settings_play_in_vlc">VLC</string>
|
||||
<string name="player_settings_play_in_mpv">MPV</string>
|
||||
|
@ -475,7 +475,7 @@
|
|||
<string name="subtitles_remove_captions">Hapus teks tertutup dari subtitel</string>
|
||||
<string name="subtitles_remove_bloat">Hapus karakter sampah dari subtitel</string>
|
||||
<string name="audio_tracks">Audio Trek</string>
|
||||
<string name="video_tracks">Vidio Trek</string>
|
||||
<string name="video_tracks">Video Trek</string>
|
||||
<string name="extension_types">Dukungan</string>
|
||||
<string name="hls_playlist">Daftar putar HLS</string>
|
||||
<string name="apk_installer_settings">Penginstal APK</string>
|
||||
|
@ -529,4 +529,21 @@
|
|||
<string name="empty_library_logged_in_message">Yahh daftar ini kosong, coba ganti ke yang lain</string>
|
||||
<string name="safe_mode_file">Mode aman file ditemukan!
|
||||
\nTidak memuat ekstensi pada startup sampai berkas dihapus.</string>
|
||||
<string name="android_tv_interface_off_seek_settings">Sembunyikan Pemutaran - Geser</string>
|
||||
<string name="android_tv_interface_on_seek_settings">Pemutar terlihat - Geser</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">Geser untuk menghilangkan</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">Geser untuk menghilangkan</string>
|
||||
<string name="pref_category_android_tv">Android TV</string>
|
||||
<string name="test_log">Log</string>
|
||||
<string name="test_passed">Berhasil</string>
|
||||
<string name="category_provider_test">Tes provider</string>
|
||||
<string name="stop">Berhenti</string>
|
||||
<string name="start">Mulai</string>
|
||||
<string name="restart">Mulai lagi</string>
|
||||
<string name="test_failed">Gagal</string>
|
||||
<string name="subscription_in_progress_notification">Memperbarui acara langganan</string>
|
||||
<string name="subscription_list_name">Berlangganan</string>
|
||||
<string name="subscription_new">Berlangganan ke %s</string>
|
||||
<string name="subscription_deleted">Berhenti berlangganan di %s</string>
|
||||
<string name="subscription_episode_released">Episode %d telah rilis!</string>
|
||||
</resources>
|
|
@ -528,4 +528,16 @@
|
|||
<string name="empty_library_logged_in_message">Sembra che questa lista sia vuota, prova a passare a un\'altra</string>
|
||||
<string name="safe_mode_file">File \"safe mode\" trovato!
|
||||
\nAll\'avvio non sarà caricata alcuna estensione finchè il file non verrà rimosso.</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">Quantità di ricerca usata quando il player è nascosto</string>
|
||||
<string name="pref_category_android_tv">TV Android</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">Quantità di ricerca usata quando il player è visibile</string>
|
||||
<string name="android_tv_interface_on_seek_settings">Player visibile - Quantità di ricerca</string>
|
||||
<string name="android_tv_interface_off_seek_settings">Player nascosto - Quantità di ricerca</string>
|
||||
<string name="test_log">Registro</string>
|
||||
<string name="start">Avvia</string>
|
||||
<string name="category_provider_test">Test del provider</string>
|
||||
<string name="restart">Riavvia</string>
|
||||
<string name="stop">Ferma</string>
|
||||
<string name="test_passed">Superato</string>
|
||||
<string name="test_failed">Fallito</string>
|
||||
</resources>
|
|
@ -0,0 +1,185 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="next_episode_time_min_format" formatted="true">%d分</string>
|
||||
<string name="title_downloads">ダウンロード</string>
|
||||
<string name="title_search">検索</string>
|
||||
<string name="title_settings">設定</string>
|
||||
<string name="result_share">シェア</string>
|
||||
<string name="movies">映画</string>
|
||||
<string name="title_home">ホーム</string>
|
||||
<string name="library">ライブラリ</string>
|
||||
<string name="home_play">再生</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="search_hint">検索…</string>
|
||||
<string name="download">ダウンロード</string>
|
||||
<string name="home_info">情報</string>
|
||||
<string name="season">シーズン</string>
|
||||
<string name="trailer">予告編</string>
|
||||
<string name="tv_series_singular">シリーズ</string>
|
||||
<string name="episodes">エピソード</string>
|
||||
<string name="player_speed_text_format" formatted="true">再生速度 (%.2fx)</string>
|
||||
<string name="next_episode">次のエピソード</string>
|
||||
<string name="sort_apply">適用</string>
|
||||
<string name="category_account">アカウント</string>
|
||||
<string name="cartoons">カートゥーン</string>
|
||||
<string name="tv_series">TVシリーズ</string>
|
||||
<string name="torrent">トレント</string>
|
||||
<string name="documentaries">ドキュメンタリー</string>
|
||||
<string name="ova">OVA</string>
|
||||
<string name="asian_drama">アジアドラマ</string>
|
||||
<string name="livestreams">ライブ配信</string>
|
||||
<string name="movies_singular">映画</string>
|
||||
<string name="others">その他</string>
|
||||
<string name="cartoons_singular">カートゥーン</string>
|
||||
<string name="torrent_singular">トレント</string>
|
||||
<string name="documentaries_singular">ドキュメンタリー</string>
|
||||
<string name="asian_drama_singular">アジアドラマ</string>
|
||||
<string name="live_singular">ライブ配信</string>
|
||||
<string name="nsfw_singular">NSFW</string>
|
||||
<string name="sort_cancel">キャンセル</string>
|
||||
<string name="anime">アニメ</string>
|
||||
<string name="video_lock">ロック</string>
|
||||
<string name="video_source">ソース</string>
|
||||
<string name="nsfw">NSFW</string>
|
||||
<string name="clear_history">履歴を削除</string>
|
||||
<string name="continue_watching">視聴中コンテンツ</string>
|
||||
<string name="category_general">全般</string>
|
||||
<string name="other_singular">動画</string>
|
||||
<string name="category_player">プレーヤー</string>
|
||||
<string name="type_plan_to_watch">懐う</string>
|
||||
<string name="play_trailer_button">予告編を再生</string>
|
||||
<string name="episode_short">エピソード</string>
|
||||
<string name="type_watching">視聴</string>
|
||||
<string name="result_tags">ジャンル</string>
|
||||
<string name="play_movie_button">映画を再生</string>
|
||||
<string name="pick_subtitle">字幕</string>
|
||||
<string name="app_name">CloudStream</string>
|
||||
<string name="play_with_app_name">CloudStreamで再生</string>
|
||||
<string name="browser">ブラウザ</string>
|
||||
<string name="type_completed">完成</string>
|
||||
<string name="type_dropped">放置</string>
|
||||
<string name="type_on_hold">保留</string>
|
||||
<string name="loading">ローディング…</string>
|
||||
<string name="result_open_in_browser">ブラウザで開く</string>
|
||||
<string name="season_short">シーズン</string>
|
||||
<string name="resume_time_left" formatted="true">残り
|
||||
\n%d分</string>
|
||||
<string name="play_episode">再生エピソード</string>
|
||||
<string name="downloaded">ダウンロード済</string>
|
||||
<string name="pref_category_backup">バックアップ</string>
|
||||
<string name="home_source">ソース</string>
|
||||
<string name="history">履歴</string>
|
||||
<string name="result_poster_img_des">ポスター</string>
|
||||
<string name="type_none">なし</string>
|
||||
<string name="sort_copy">コピー</string>
|
||||
<string name="sort_close">閉じる</string>
|
||||
<string name="sort_save">保存</string>
|
||||
<string name="sort_clear">消去</string>
|
||||
<string name="app_dub_sub_episode_text_format" formatted="true">%sエピ%d</string>
|
||||
<string name="cast_format" formatted="true">出演者:%s</string>
|
||||
<string name="search_poster_img_des">ポスター</string>
|
||||
<string name="episode_poster_img_des">エピソードポスター</string>
|
||||
<string name="home_main_poster_img_des">主要ポスター</string>
|
||||
<string name="home_next_random_img_des">次のランダム</string>
|
||||
<string name="go_back_img_des">戻り</string>
|
||||
<string name="rated_format" formatted="true">視聴率 %.1f</string>
|
||||
<string name="new_update_format" formatted="true">新しいアップデートを発見!
|
||||
\n%s -> %s</string>
|
||||
<string name="duration_format" formatted="true">%d分</string>
|
||||
<string name="search_hint_site" formatted="true">%sを検索…</string>
|
||||
<string name="pick_source">ソース</string>
|
||||
<string name="filler" formatted="true">ろくごうきじ</string>
|
||||
<string name="reload_error">接続を再試行…</string>
|
||||
<string name="go_back">戻り</string>
|
||||
<string name="action_remove_from_bookmarks">削除</string>
|
||||
<string name="home_more_info">詳細情報</string>
|
||||
<string name="home_expanded_hide">閉じる</string>
|
||||
<string name="category_updates">アップデート・バックアップ</string>
|
||||
<string name="app_language">アプリ言語</string>
|
||||
<string name="github">GitHub(ギットハブ)</string>
|
||||
<string name="go_back_30">-30</string>
|
||||
<string name="go_forward_30">+30</string>
|
||||
<string name="legal_notice">免責</string>
|
||||
<string name="pref_category_extensions">拡張機能</string>
|
||||
<string name="pref_category_app_updates">アプリ更新</string>
|
||||
<string name="category_providers">提供者</string>
|
||||
<string name="pref_category_subtitles">字幕</string>
|
||||
<string name="pref_category_ui_features">特徴</string>
|
||||
<string name="pref_category_defaults">デフォルト</string>
|
||||
<string name="automatic">自動</string>
|
||||
<string name="home_random">任意</string>
|
||||
<string name="extensions">拡張機能</string>
|
||||
<string name="pref_category_links">リンク</string>
|
||||
<string name="pref_category_android_tv">Android TV</string>
|
||||
<string name="login">ログイン</string>
|
||||
<string name="logout">ログアウト</string>
|
||||
<string name="max">最大</string>
|
||||
<string name="min">最小</string>
|
||||
<string name="none">なし</string>
|
||||
<string name="next">次</string>
|
||||
<string name="is_adult">18+</string>
|
||||
<string name="no">否</string>
|
||||
<string name="open_with">で開く</string>
|
||||
<string name="episode">エピソード</string>
|
||||
<string name="duration">時間</string>
|
||||
<string name="synopsis">概要</string>
|
||||
<string name="site">サイト</string>
|
||||
<string name="used_storage">使用</string>
|
||||
<string name="app_storage">アプリ</string>
|
||||
<string name="action_open_watching">詳細情報</string>
|
||||
<string name="action_remove_watching">削除</string>
|
||||
<string name="picture_in_picture">ピクチャーインピクチャー</string>
|
||||
<string name="player_subtitles_settings">字幕</string>
|
||||
<string name="settings_info">情報</string>
|
||||
<string name="pause">一時停止</string>
|
||||
<string name="play_episode_toast">再生エピソード</string>
|
||||
<string name="delete">削除</string>
|
||||
<string name="start">開始</string>
|
||||
<string name="status">状態</string>
|
||||
<string name="year">年</string>
|
||||
<string name="resume">再開</string>
|
||||
<string name="test_failed">失敗</string>
|
||||
<string name="test_passed">合格</string>
|
||||
<string name="free_storage">空き</string>
|
||||
<string name="status_completed">完成</string>
|
||||
<string name="status_ongoing">進行中</string>
|
||||
<string name="normal">デフォルト</string>
|
||||
<string name="player_settings_play_in_browser">ウェブブラウザ</string>
|
||||
<string name="player_settings_play_in_vlc">VLC</string>
|
||||
<string name="player_settings_play_in_mpv">MPV</string>
|
||||
<string name="extension_language">言語</string>
|
||||
<string name="extension_authors">作成者</string>
|
||||
<string name="extension_size">サイズ</string>
|
||||
<string name="extension_status">状態</string>
|
||||
<string name="extension_version">バージョン</string>
|
||||
<string name="extension_rating" formatted="true">視聴率 %s</string>
|
||||
<string name="rating">視聴率</string>
|
||||
<string name="default_subtitles">デフォルト</string>
|
||||
<string name="download_failed">ダウンロード失敗</string>
|
||||
<string name="download_started">ダウンロード開始</string>
|
||||
<string name="download_done">ダウンロード完了</string>
|
||||
<string name="download_canceled">ダウンロード終了</string>
|
||||
<string name="stream">ストリーム</string>
|
||||
<string name="update_started">アップデート開始</string>
|
||||
<string name="no_season">シーズンなし</string>
|
||||
<string name="no_subtitles">字幕なし</string>
|
||||
<string name="video_aspect_ratio_resize">アスペクト比</string>
|
||||
<string name="skip_loading">ロードをスキップする</string>
|
||||
<string name="episode_more_options_des">その他のオプション</string>
|
||||
<string name="no_data">データなし</string>
|
||||
<string name="downloading">ダウンロード中</string>
|
||||
<string name="error_bookmarks_text">ブックマーク</string>
|
||||
<string name="download_storage_text">内部記憶装置</string>
|
||||
<string name="download_paused">ダウンロードが一時停止</string>
|
||||
<string name="provider_info_meta">メタデータはこのサイトでは提供されません。メタデータがサイト上に存在しない場合、ビデオの読み込みに失敗します。</string>
|
||||
<string name="torrent_plot">記述</string>
|
||||
<string name="show_log_cat">Logcat 🐈を表示</string>
|
||||
<string name="test_log">ログ</string>
|
||||
<string name="search">検索</string>
|
||||
<string name="discord">Discordに参加</string>
|
||||
<string name="update">アップデート</string>
|
||||
<string name="check_for_update">アップデートを確認</string>
|
||||
<string name="show_title">作品名</string>
|
||||
<string name="update_notification_installing">アプリのアップデートをインストール中…</string>
|
||||
</resources>
|
|
@ -1,3 +1,128 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
<string name="app_dub_sub_episode_text_format" formatted="true">%sಎಪಿ%d</string>
|
||||
<string name="cast_format" formatted="true">ಕ್ಯಾಸ್ಟ್:%s</string>
|
||||
<string name="go_back_img_des">ಹಿಂದೆ ಹೋಗು</string>
|
||||
<string name="filler" formatted="true">ಫಿಲ್ಲರ್</string>
|
||||
<string name="title_search">ಹುಡುಕು</string>
|
||||
<string name="title_downloads">ಡೌನ್ಲೋಡ್</string>
|
||||
<string name="subs_font">ಫಾಂಟ್</string>
|
||||
<string name="search_provider_text_providers">ಪೂರೈಕೆದಾರರನ್ನು ಬಳಸಿಕೊಂಡು ಹುಡುಕಿ</string>
|
||||
<string name="search_provider_text_types">ಪ್ರಕಾರಗಳನ್ನು ಬಳಸಿಕೊಂಡು ಹುಡುಕಿ</string>
|
||||
<string name="benene_count_text_none">ಯಾವುದೇ ಬೆನೆನ್ಸ್ ನೀಡಿಲ್ಲ</string>
|
||||
<string name="subs_auto_select_language">ಸ್ವಯಂ-ಆಯ್ಕೆ ಭಾಷೆ</string>
|
||||
<string name="action_open_watching">ಹೆಚ್ಚಿನ ಮಾಹಿತಿ</string>
|
||||
<string name="action_open_play">\@ಸ್ಟ್ರಿಂಗ್/ಹೋಮ್_ಪ್ಲೇ</string>
|
||||
<string name="vpn_might_be_needed">ಈ ಪೂರೈಕೆದಾರರು ಸರಿಯಾಗಿ ಕೆಲಸ ಮಾಡಲು VPN ಬೇಕಾಗಬಹುದು</string>
|
||||
<string name="player_size_settings_des">ಕಪ್ಪು ಗಡಿಗಳನ್ನು ತೆಗೆದುಹಾಕಿ</string>
|
||||
<string name="next_episode_format" formatted="true">ಸಂಚಿಕೆ%d ಬಿಡುಗಡೆಯಾಗಲಿದೆ</string>
|
||||
<string name="next_episode_time_hour_format" formatted="true">%dh %dm</string>
|
||||
<string name="result_poster_img_des">ಪೋಸ್ಟರ್</string>
|
||||
<string name="search_poster_img_des">ಪೋಸ್ಟರ್</string>
|
||||
<string name="episode_poster_img_des">ಸಂಚಿಕೆ ಪೋಸ್ಟರ್</string>
|
||||
<string name="home_main_poster_img_des">ಮೇನ್ ಪೋಸ್ಟರ್</string>
|
||||
<string name="update_started">ಅಪ್ಡೇಟ್ ಪ್ರಾರಂಭವಾಗಿದೆ</string>
|
||||
<string name="error_loading_links_toast">ಲೋಡಿಂಗ್ ಲಿಂಕ್ ಎರರ್ ಬಂದಿದೆ</string>
|
||||
<string name="download_storage_text">ಇಂಟರ್ನಲ್ ಸ್ಟೋರೇಜ್</string>
|
||||
<string name="app_dubbed_text">ಡಬ್</string>
|
||||
<string name="app_subbed_text">ಸಬ್</string>
|
||||
<string name="pref_disable_acra">ಸ್ವಯಂಚಾಲಿತ ದೋಷ ವರದಿಯನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿ</string>
|
||||
<string name="home_expanded_hide">ಹೈಡ್</string>
|
||||
<string name="home_play">ಪ್ಲೇ</string>
|
||||
<string name="home_info">ಮಾಹಿತಿ</string>
|
||||
<string name="action_add_to_bookmarks">ಸೆಟ್ ವಾಚ್ ಸ್ಟೇಟಸ್</string>
|
||||
<string name="sort_apply">ಅನ್ವಯಿಸು</string>
|
||||
<string name="sort_cancel">ರದ್ದುಮಾಡು</string>
|
||||
<string name="subs_subtitle_elevation">ಸಬ್ ಟೈಟಲ್ಸ್ ಎಲೆವಷನ್</string>
|
||||
<string name="subs_font_size">ಫಾಂಟ್ ಸೈಜ್</string>
|
||||
<string name="subs_subtitle_languages">ಸಬ್ ಟೈಟಲ್ಸ್ ಭಾಷೆ</string>
|
||||
<string name="action_remove_watching">ತೆಗೆದುಹಾಕಿ</string>
|
||||
<string name="vpn_torrent">ಈ ಪೂರೈಕೆದಾರರು ಟೊರೆಂಟ್ ಆಗಿದೆ, VPN ಅನ್ನು ಶಿಫಾರಸು ಮಾಡಲಾಗಿದೆ</string>
|
||||
<string name="normal_no_plot">ಯಾವುದೇ ಪ್ಲಾಟ್ ಕಂಡುಬಂದಿಲ್ಲ</string>
|
||||
<string name="show_log_cat">ಲಾಗ್ಕ್ಯಾಟ್ 🐈 ತೋರಿಸಿ</string>
|
||||
<string name="test_log">ಲಾಗ್</string>
|
||||
<string name="picture_in_picture">ಚಿತ್ರದಲ್ಲಿ-ಚಿತ್ರದಲ್ಲಿ</string>
|
||||
<string name="player_size_settings">ಪ್ಲೇಯರ್ ಮರುಗಾತ್ರಗೊಳಿಸಿ ಬಟನ್</string>
|
||||
<string name="player_subtitles_settings">ಸಬ್ ಟೈಟಲ್ಸ್</string>
|
||||
<string name="player_subtitles_settings_des">ಪ್ಲೇಯರ್ ಸಬ್ ಟೈಟಲ್ಸ್ ಸೆಟ್ಟಿಂಗ್ಗಳು</string>
|
||||
<string name="chromecast_subtitles_settings_des">ಕ್ರೋಮ್ ಕ್ಯಾಸ್ಟ್ ಸಬ್ ಟೈಟಲ್ಸ್ ಸೆಟ್ಟಿಂಗ್ಸ್</string>
|
||||
<string name="go_back">ಹಿಂದೆ ಹೋಗು</string>
|
||||
<string name="popup_pause_download">ಡೌನ್ಲೋಡ್ ವಿರಾಮಗೊಳಿಸಿ</string>
|
||||
<string name="error_bookmarks_text">ಬುಕ್ಮಾರ್ಕ್</string>
|
||||
<string name="subs_background_color">ಬ್ಯಾಕ್ ಗ್ರೌಂಡ್ ಕಲರ್</string>
|
||||
<string name="benene_count_text">%d ಡೇವ್ಗಳಿಗೆ ಬೆನೆನೆಸ್ ನೀಡಲಾಗಿದೆ</string>
|
||||
<string name="subs_hold_to_reset_to_default">ಡೀಫಾಲ್ಟ್ಗೆ ಮರುಹೊಂದಿಸಲು ಹಿಡಿದುಕೊಳ್ಳಿ</string>
|
||||
<string name="provider_info_meta">ಸೈಟ್ನಿಂದ ಮೆಟಾಡೇಟಾವನ್ನು ಒದಗಿಸಲಾಗಿಲ್ಲ, ಅದು ಸೈಟ್ನಲ್ಲಿ ಅಸ್ತಿತ್ವದಲ್ಲಿಲ್ಲದಿದ್ದರೆ ವೀಡಿಯೊ ಲೋಡಿಂಗ್ ವಿಫಲಗೊಳ್ಳುತ್ತದೆ.</string>
|
||||
<string name="picture_in_picture_des">ಇತರ ಅಪ್ಲಿಕೇಶನ್ಗಳ ಮೇಲೆ ಚಿಕಣಿ ಪ್ಲೇಯರ್ನಲ್ಲಿ ಪ್ಲೇಬ್ಯಾಕ್ ಅನ್ನು ಮುಂದುವರಿಸುತ್ತದೆ</string>
|
||||
<string name="chromecast_subtitles_settings">ಕ್ರೋಮ್ ಕ್ಯಾಸ್ಟ್ ಸಬ್ ಟೈಟಲ್ಸ್</string>
|
||||
<string name="rated_format" formatted="true">ರೇಟೆಡ್:%.1f</string>
|
||||
<string name="action_remove_from_bookmarks">ತೆಗೆದುಹಾಕಿ</string>
|
||||
<string name="popup_resume_download">ಡೌನ್ಲೋಡ್ ಅನ್ನು ಪುನರಾರಂಭಿಸಿ</string>
|
||||
<string name="sort_close">ಕ್ಲೋಸ್</string>
|
||||
<string name="sort_clear">ಕ್ಲಿಯರ್</string>
|
||||
<string name="sort_save">ಸೇವ್</string>
|
||||
<string name="subtitles_settings">ಸಬ್ ಟೈಟಲ್ಸ್ ಸೆಟ್ಟಿಂಗ್ಸ್</string>
|
||||
<string name="popup_play_file">ಫೈಲ್ ಪ್ಲೇ</string>
|
||||
<string name="subs_text_color">ಟೆಕ್ಸ್ಟ್ ಕಲರ್</string>
|
||||
<string name="subs_outline_color">ಔಟ್ ಲೈನ್ ಕಲರ್</string>
|
||||
<string name="subs_window_color">ವಿಂಡೋ ಕಲರ್</string>
|
||||
<string name="subs_edge_type">ಎಡ್ಜ್ ಟೈಪ್</string>
|
||||
<string name="home_change_provider_img_des">ಪ್ರೊವೈಡರ್ ಬದಲಾಯಿಸಿ</string>
|
||||
<string name="duration_format" formatted="true">%dಮಿನ</string>
|
||||
<string name="torrent_plot">ವಿವರಣೆ</string>
|
||||
<string name="player_speed_text_format" formatted="true">ಸ್ಪೀಡ್(%.2fx)</string>
|
||||
<string name="title_home">ಹೋಂ</string>
|
||||
<string name="pick_subtitle">ಸಬ್ ಟೈಟಲ್ಸ್</string>
|
||||
<string name="title_settings">ಸೆಟ್ಟಿಂಗ್ಸ್</string>
|
||||
<string name="filter_bookmarks">ಬುಕ್ಮಾರ್ಕ್ಗಳನ್ನು ಫಿಲ್ಟರ್ ಮಾಡಿ</string>
|
||||
<string name="search_hint">ಹುಡುಕು…</string>
|
||||
<string name="play_movie_button">ಚಲನಚಿತ್ರವನ್ನು ಪ್ಲೇ ಮಾಡಿ</string>
|
||||
<string name="preview_background_img_des">ಪ್ರಿವ್ಯೂ ಹಿನ್ನೆಲೆ</string>
|
||||
<string name="next_episode">ಮುಂದಿನ ಸಂಚಿಕೆ</string>
|
||||
<string name="app_name">ಕ್ಲೌಡ್ ಸ್ಟ್ರೀಮ್</string>
|
||||
<string name="downloading">ಡೌನ್ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ</string>
|
||||
<string name="stream">ಸ್ಟ್ರೀಮ್</string>
|
||||
<string name="result_share">ಶೇರ್</string>
|
||||
<string name="popup_delete_file">ಫೈಲ್ ಅಳಿಸಿ</string>
|
||||
<string name="home_more_info">ಹೆಚ್ಚಿನ ಮಾಹಿತಿ</string>
|
||||
<string name="new_update_format" formatted="true">ಹೊಸ ಅಪ್ಡೇಟ್ ಬಂದಿದೆ
|
||||
\n%s-%s</string>
|
||||
<string name="loading">ಲೋಡಿಂಗ್…</string>
|
||||
<string name="subs_download_languages">ಡೌನ್ಲೋಡ್ ಭಾಷೆಗಳನ್ನು ಮಾಡಿ</string>
|
||||
<string name="play_livestream_button">ಲೈವ್ಸ್ಟ್ರೀಮ್ ಪ್ಲೇ ಮಾಡಿ</string>
|
||||
<string name="play_with_app_name">ಕ್ಲೌಡ್ ಸ್ಟ್ರೀಮ್ ಇದರೊಂದಿಗೆ ಪ್ಲೇ ಮಾಡಿ</string>
|
||||
<string name="type_plan_to_watch">ವೀಕ್ಷಿಸಲು ಯೋಜನೆ</string>
|
||||
<string name="play_episode">ಸಂಚಿಕೆಯನ್ನು ಪ್ಲೇ ಮಾಡಿ</string>
|
||||
<string name="continue_watching">ಕಂಟಿನ್ಯೂ ವಾಟಚಿಂಗ್</string>
|
||||
<string name="torrent_no_plot">ಯಾವುದೇ ವಿವರಣೆ ಕಂಡುಬಂದಿಲ್ಲ</string>
|
||||
<string name="play_torrent_button">ಸ್ಟ್ರೀಮ್ ಟೊರೆಂಟ್</string>
|
||||
<string name="download">ಡೌನ್ಲೋಡ್</string>
|
||||
<string name="sort_copy">ಕಾಪಿ</string>
|
||||
<string name="no_data">ನೋ ಡೇಟಾ</string>
|
||||
<string name="player_speed">ಪ್ಲೇಯರ್ ಸ್ಪೀಡ್</string>
|
||||
<string name="next_episode_time_day_format" formatted="true">%d %dh %dm</string>
|
||||
<string name="search_hint_site" formatted="true">ಹುಡುಕು %s…</string>
|
||||
<string name="episode_more_options_des">ಹೆಚ್ಚಿನ ಆಯ್ಕೆ</string>
|
||||
<string name="subs_import_text" formatted="true">ಫಾಂಟ್ಗಳನ್ನು ಇರಿಸುವ ಮೂಲಕ ಆಮದು ಮಾಡಿ %s</string>
|
||||
<string name="next_episode_time_min_format" formatted="true">%dm</string>
|
||||
<string name="result_tags">ಪ್ರಕಾರಗಳು</string>
|
||||
<string name="result_open_in_browser">ಬ್ರೌಸರ್ ತೆರೆಯಿರಿ</string>
|
||||
<string name="type_on_hold">ಆನ್-ಹೋಲ್ಡ್</string>
|
||||
<string name="type_none">ನನ್</string>
|
||||
<string name="reload_error">ಸಂಪರ್ಕವನ್ನು ಮರುಪ್ರಯತ್ನಿಸಿ…</string>
|
||||
<string name="download_paused">ಡೌನ್ಲೋಡ್ ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ</string>
|
||||
<string name="download_failed">ಡೌನ್ಲೋಡ್ ವಿಫಲವಾಗಿದೆ</string>
|
||||
<string name="download_done">ಡೌನ್ಲೋಡ್ ಮುಗಿದಿದೆ</string>
|
||||
<string name="browser">ಬ್ರೌಸರ್</string>
|
||||
<string name="skip_loading">ಸ್ಕಿಪ್ ಲೋಡಿಂಗ್</string>
|
||||
<string name="type_watching">ವಾಚಿಂಗ್</string>
|
||||
<string name="type_completed">ಪೂರ್ಣಗೊಂಡಿದೆ</string>
|
||||
<string name="type_dropped">ಕೈಬಿಡಲಾಯಿತು</string>
|
||||
<string name="type_re_watching">ಪುನಃ ವೀಕ್ಷಿಸುತ್ತಿದೆ</string>
|
||||
<string name="play_trailer_button">ಟ್ರೈಲರ್ ಪ್ಲೇ ಮಾಡಿ</string>
|
||||
<string name="pick_source">ಮೂಲಗಳು</string>
|
||||
<string name="downloaded">ಡೌನ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ</string>
|
||||
<string name="download_started">ಡೌನ್ಲೋಡ್ ಪ್ರಾರಂಭವಾಗಿದೆ</string>
|
||||
<string name="download_canceled">ಡೌನ್ಲೋಡ್ ರದ್ದುಗೊಳಿಸಲಾಗಿದೆ</string>
|
||||
<string name="home_next_random_img_des">ಮುಂದಿನ ರಾಂಡಮ್</string>
|
||||
</resources>
|
|
@ -373,7 +373,7 @@
|
|||
<string name="skip_setup">Pomiń setup</string>
|
||||
<string name="app_layout_subtext">Dostosuj wygląd aplikacji do urządzenia</string>
|
||||
<string name="crash_reporting_title">Zgłaszanie błędów</string>
|
||||
<string name="preferred_media_subtext">Co chciałbyś obejrzeć\?</string>
|
||||
<string name="preferred_media_subtext">Co chciałbyś obejrzeć</string>
|
||||
<string name="setup_done">Gotowe</string>
|
||||
<string name="extensions">Rozszerzenia</string>
|
||||
<string name="add_repository">Dodaj repozytorium</string>
|
||||
|
@ -509,4 +509,9 @@
|
|||
<string name="empty_library_logged_in_message">Wygląda na to, że ta lista jest pusta, spróbuj przełączyć się na inną</string>
|
||||
<string name="safe_mode_file">Znaleziono plik trybu bezpiecznego.
|
||||
\nRozszerzenia nie zostaną wczytane, dopóki plik nie zostanie usunięty.</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">Używana ilość przewijania, gdy widoczny jest odtwarzacz</string>
|
||||
<string name="android_tv_interface_off_seek_settings">Ukryty odtwarzacz - ilość przewijania</string>
|
||||
<string name="pref_category_android_tv">Android TV</string>
|
||||
<string name="android_tv_interface_on_seek_settings">Pokazany odtwarzacz — ilość przewijania</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">Używana ilość przewijania, gdy ukryty jest odtwarzacz</string>
|
||||
</resources>
|
|
@ -9,7 +9,7 @@
|
|||
<string name="next_episode_time_min_format" formatted="true">%dm</string>
|
||||
<!-- IS NOT NEEDED TO TRANSLATE AS THEY ARE ONLY USED FOR SCREEN READERS AND WONT SHOW UP TO NORMAL USERS -->
|
||||
<string name="result_poster_img_des">Poster</string>
|
||||
<string name="search_poster_img_des">\@string/result_poster_img_des</string>
|
||||
<string name="search_poster_img_des">Poster</string>
|
||||
<string name="episode_poster_img_des">Poster Episod</string>
|
||||
<string name="home_main_poster_img_des">Poster Principal</string>
|
||||
<string name="home_next_random_img_des">Următorul la Întâmplare</string>
|
||||
|
@ -142,7 +142,7 @@
|
|||
<string name="restore_success">Fișier de rezervă încărcat</string>
|
||||
<string name="restore_failed_format" formatted="true">Imposibilitatea de a restaura datele din %s</string>
|
||||
<string name="backup_success">Date stocate</string>
|
||||
<string name="backup_failed">Permisiuni de arhivare lipsă, vă rugăm să încercați din nou</string>
|
||||
<string name="backup_failed">Permisiunea de arhivare lipșe, vă rugăm să încercați din nou.</string>
|
||||
<string name="backup_failed_error_format">Eroare de backup %s</string>
|
||||
<string name="search">Căutare</string>
|
||||
<string name="category_account">Conturi și credite</string>
|
||||
|
@ -154,7 +154,7 @@
|
|||
<string name="bug_report_settings_on">Nu trimiteți niciun fel de date</string>
|
||||
<string name="show_fillers_settings">Afișează etichetele [filler] pentru anime</string>
|
||||
<string name="show_trailers_settings">Arată trailerul</string>
|
||||
<string name="kitsu_settings">Arată posterele de la Kitsu</string>
|
||||
<string name="kitsu_settings">Arată afișele de la Kitsu</string>
|
||||
<string name="updates_settings">Afișați actualizările aplicației</string>
|
||||
<string name="updates_settings_des">Căutați automat noi actualizări la pornire</string>
|
||||
<string name="uprereleases_settings">Actualizați la prerelease</string>
|
||||
|
@ -384,4 +384,8 @@
|
|||
<string name="autoplay_next_settings_des">Începe următorul episod când se termină episodul curent</string>
|
||||
<string name="pref_filter_search_quality">Ascundeți calitatea video selectată în rezultatele căutării</string>
|
||||
<string name="play_livestream_button">Redare Livestream</string>
|
||||
<string name="library">Librărie</string>
|
||||
<string name="test_log">Log</string>
|
||||
<string name="browser">Browser</string>
|
||||
<string name="play_with_app_name">Joacă cu CloudStream</string>
|
||||
</resources>
|
|
@ -506,4 +506,16 @@
|
|||
<string name="android_tv_interface_on_seek_settings">Плеер показан - Перемотки объем</string>
|
||||
<string name="android_tv_interface_off_seek_settings">Плеер спрятан - Перемотки объем</string>
|
||||
<string name="subtitles_remove_bloat">Удалять лишнее из субтитров</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">Местоположение ползунка, когда игрок скрыт</string>
|
||||
<string name="pref_category_android_tv">Android TV</string>
|
||||
<string name="actor_supporting">Второго планa</string>
|
||||
<string name="skip_type_mixed_op">Смешанный опенинг</string>
|
||||
<string name="skip_type_mixed_ed">Смешанный конец</string>
|
||||
<string name="category_provider_test">Тест провайдер</string>
|
||||
<string name="test_log">Журнал</string>
|
||||
<string name="start">Запустить</string>
|
||||
<string name="test_passed">Выполнено</string>
|
||||
<string name="test_failed">Неудачный</string>
|
||||
<string name="stop">Прекратить</string>
|
||||
<string name="restart">Перезапустить</string>
|
||||
</resources>
|
|
@ -17,7 +17,7 @@
|
|||
<string name="next_episode_time_day_format" formatted="true">%dd %dh %dm</string>
|
||||
<string name="next_episode_time_min_format" formatted="true">%dm</string>
|
||||
<string name="duration_format" formatted="true">%d min</string>
|
||||
<string name="search_poster_img_des">\@string/result_poster_img_des</string>
|
||||
<string name="search_poster_img_des">Plagát</string>
|
||||
<string name="episode_poster_img_des">Plagát epizódy</string>
|
||||
<string name="home_main_poster_img_des">Hlavný plagát</string>
|
||||
<string name="play_with_app_name">Prehrať s CloudStream</string>
|
||||
|
|
|
@ -507,4 +507,20 @@
|
|||
<string name="safe_mode_file">Файл безпечного режиму знайдено!
|
||||
\nРозширеня не завантажуються під час запуску, доки файл не буде видалено.</string>
|
||||
<string name="pref_category_android_tv">Android TV</string>
|
||||
<string name="android_tv_interface_off_seek_settings">Плеєр сховано - обсяг пошуку</string>
|
||||
<string name="android_tv_interface_on_seek_settings">Плеєр показано - обсяг пошуку</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">Обсяг пошуку, який використовується, коли плеєр видимий</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">Обсяг пошуку, який використовується, коли гравець прихований</string>
|
||||
<string name="test_failed">Не вдалося</string>
|
||||
<string name="test_passed">Пройдено</string>
|
||||
<string name="restart">Перезапуск</string>
|
||||
<string name="test_log">Журнал</string>
|
||||
<string name="start">Старт</string>
|
||||
<string name="stop">Стоп</string>
|
||||
<string name="category_provider_test">Тест постачальника</string>
|
||||
<string name="subscription_in_progress_notification">Оновлення підписаних шоу</string>
|
||||
<string name="subscription_list_name">Підписано</string>
|
||||
<string name="subscription_new">Підписано на %s</string>
|
||||
<string name="subscription_deleted">Відписатися від %s</string>
|
||||
<string name="subscription_episode_released">Епізод %d випущено!</string>
|
||||
</resources>
|
|
@ -19,7 +19,7 @@
|
|||
<string name="next_episode_time_min_format" formatted="true">%dm</string>
|
||||
<!-- IS NOT NEEDED TO TRANSLATE AS THEY ARE ONLY USED FOR SCREEN READERS AND WONT SHOW UP TO NORMAL USERS -->
|
||||
<string name="result_poster_img_des">封面</string>
|
||||
<string name="search_poster_img_des">\@string/result_poster_img_des</string>
|
||||
<string name="search_poster_img_des">封面</string>
|
||||
<string name="episode_poster_img_des">劇集封面</string>
|
||||
<string name="home_main_poster_img_des">主封面</string>
|
||||
<string name="home_next_random_img_des">隨機下一個</string>
|
||||
|
@ -533,4 +533,5 @@
|
|||
<string name="pref_category_defaults">預設</string>
|
||||
<string name="pref_category_looks">外觀</string>
|
||||
<string name="pref_category_ui_features">功能</string>
|
||||
<string name="browser">瀏覽器</string>
|
||||
</resources>
|
|
@ -554,4 +554,21 @@
|
|||
<string name="empty_library_no_accounts_message">看来您的库是空的 :(
|
||||
\n登录库账户或添加节目到您的本地库</string>
|
||||
<string name="empty_library_logged_in_message">看来此列表是空的,请尝试切换到另一个</string>
|
||||
<string name="android_tv_interface_on_seek_settings">播放器显示 - 快进快退秒数</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">播放器可见时使用的快进快退秒数</string>
|
||||
<string name="android_tv_interface_off_seek_settings">播放器隐藏 - 快进快退秒数</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">播放器隐藏时使用的快进快退秒数</string>
|
||||
<string name="pref_category_android_tv">Android TV</string>
|
||||
<string name="test_failed">失败</string>
|
||||
<string name="category_provider_test">片源测试</string>
|
||||
<string name="restart">重启</string>
|
||||
<string name="stop">停止</string>
|
||||
<string name="subscription_in_progress_notification">正在更新订阅节目</string>
|
||||
<string name="subscription_list_name">已订阅</string>
|
||||
<string name="subscription_new">已订阅 %s</string>
|
||||
<string name="subscription_deleted">已取消订阅 %s</string>
|
||||
<string name="start">开始</string>
|
||||
<string name="subscription_episode_released">第 %d 集已发布!</string>
|
||||
<string name="test_passed">成功</string>
|
||||
<string name="test_log">日志</string>
|
||||
</resources>
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="FlowLayout_Layout">
|
||||
<attr format="dimension" name="itemSpacing" />
|
||||
<attr name="itemSpacing" format="dimension" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="FlowLayout_Layout_layout_space" />
|
||||
|
||||
|
@ -13,6 +13,10 @@
|
|||
<item name="customCastBackgroundColor">?attr/colorPrimary</item>
|
||||
</style>
|
||||
|
||||
<declare-styleable name="TestView">
|
||||
<attr name="header_text" format="string" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="MainColors">
|
||||
<attr name="colorPrimary" format="color" />
|
||||
<attr name="colorSearch" format="color" />
|
||||
|
|
|
@ -84,4 +84,7 @@
|
|||
<color name="colorPrimaryBrown">#622C00</color>
|
||||
<color name="colorPrimaryOrange">#CE8500</color>
|
||||
<color name="colorPrimaryDandelionYellow">#F5BB00</color>
|
||||
|
||||
<color name="colorTestPass">#48E484</color>
|
||||
<color name="colorTestFail">#ea596e</color>
|
||||
</resources>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<string name="fast_forward_button_time_key" translatable="false">fast_forward_button_time</string>
|
||||
<string name="benene_count" translatable="false">benene_count</string>
|
||||
<string name="subtitle_settings_key" translatable="false">subtitle_settings_key</string>
|
||||
<string name="test_providers_key" translatable="false">test_providers_key</string>
|
||||
<string name="subtitle_settings_chromecast_key" translatable="false">subtitle_settings_chromecast_key</string>
|
||||
<string name="quality_pref_key" translatable="false">quality_pref_key</string>
|
||||
<string name="player_pref_key" translatable="false">player_pref_key</string>
|
||||
|
@ -42,6 +43,7 @@
|
|||
<string name="random_button_key" translatable="false">random_button_key</string>
|
||||
<string name="provider_lang_key" translatable="false">provider_lang_key</string>
|
||||
<string name="dns_key" translatable="false">dns_key</string>
|
||||
<string name="jsdelivr_proxy_key" translatable="false">jsdelivr_proxy_key</string>
|
||||
<string name="download_path_key" translatable="false">download_path_key</string>
|
||||
<string name="app_name_download_path" translatable="false">AquaStream</string>
|
||||
<string name="app_layout_key" translatable="false">app_layout_key</string>
|
||||
|
@ -195,6 +197,7 @@
|
|||
<string name="normal_no_plot">No Plot Found</string>
|
||||
<string name="torrent_no_plot">No Description Found</string>
|
||||
<string name="show_log_cat">Show Logcat 🐈</string>
|
||||
<string name="test_log">Log</string>
|
||||
<string name="picture_in_picture">Picture-in-picture</string>
|
||||
<string name="picture_in_picture_des">Continues playback in a miniature player on top of other apps</string>
|
||||
<string name="player_size_settings">Player resize button</string>
|
||||
|
@ -283,6 +286,9 @@
|
|||
<string name="delete">Delete</string>
|
||||
<string name="cancel" translatable="false">@string/sort_cancel</string>
|
||||
<string name="pause">Pause</string>
|
||||
<string name="start">Start</string>
|
||||
<string name="test_failed">Failed</string>
|
||||
<string name="test_passed">Passed</string>
|
||||
<string name="resume">Resume</string>
|
||||
<string name="go_back_30">-30</string>
|
||||
<string name="go_forward_30">+30</string>
|
||||
|
@ -374,6 +380,9 @@
|
|||
<string name="video_disk_description">Causes problems if set too high on devices with low storage space, such as Android TV.</string>
|
||||
<string name="dns_pref">DNS over HTTPS</string>
|
||||
<string name="dns_pref_summary">Useful for bypassing ISP blocks</string>
|
||||
<string name="jsdelivr_proxy">raw.githubusercontent.com Proxy</string>
|
||||
<string name="jsdelivr_enabled">Failed to reach GitHub, enabling jsdelivr proxy.</string>
|
||||
<string name="jsdelivr_proxy_summary">Bypasses blocking of GitHub using jsdelivr, may cause updates to be delayed by few days.</string>
|
||||
<string name="add_site_pref">Clone site</string>
|
||||
<string name="remove_site_pref">Remove site</string>
|
||||
<string name="add_site_summary">Add a clone of an existing site, with a different URL</string>
|
||||
|
@ -401,6 +410,7 @@
|
|||
responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use
|
||||
AquaStream at your own risk.
|
||||
</string>
|
||||
<string name="pref_category_bypass">ISP Bypasses</string>
|
||||
<string name="pref_category_links">Links</string>
|
||||
<string name="pref_category_app_updates">App updates</string>
|
||||
<string name="pref_category_backup">Backup</string>
|
||||
|
@ -424,6 +434,7 @@
|
|||
<string name="enable_nsfw_on_providers">Enable NSFW on supported providers</string>
|
||||
<string name="subtitles_encoding">Subtitle encoding</string>
|
||||
<string name="category_providers">Providers</string>
|
||||
<string name="category_provider_test">Provider test</string>
|
||||
<string name="category_ui">Layout</string>
|
||||
<string name="automatic">Auto</string>
|
||||
<string name="tv_layout">TV layout</string>
|
||||
|
@ -580,6 +591,8 @@
|
|||
<string name="audio_tracks">Audio tracks</string>
|
||||
<string name="video_tracks">Video tracks</string>
|
||||
<string name="apply_on_restart">Apply on Restart</string>
|
||||
<string name="restart">Restart</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="safe_mode_title">Safe mode on</string>
|
||||
<string name="safe_mode_description">All extensions were turned off due to a crash to help you find the one causing trouble.</string>
|
||||
<string name="safe_mode_crash_info">View crash info</string>
|
||||
|
@ -637,4 +650,10 @@
|
|||
<string name="empty_library_no_accounts_message">Looks like your library is empty :(\nLogin to a library account or add shows to your local library</string>
|
||||
<string name="empty_library_logged_in_message">Looks like this list is empty, try switching to another one</string>
|
||||
<string name="safe_mode_file">Safe mode file found!\nNot loading any extensions on startup until file is removed.</string>
|
||||
<string name="revert">Revert</string>
|
||||
<string name="subscription_in_progress_notification">Updating subscribed shows</string>
|
||||
<string name="subscription_list_name">Subscribed</string>
|
||||
<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>
|
||||
</resources>
|
|
@ -1,24 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<Preference
|
||||
android:icon="@drawable/ic_baseline_language_24"
|
||||
android:key="@string/provider_lang_key"
|
||||
android:title="@string/provider_lang_settings" />
|
||||
android:icon="@drawable/ic_baseline_language_24"
|
||||
android:key="@string/provider_lang_key"
|
||||
android:title="@string/provider_lang_settings" />
|
||||
|
||||
<Preference
|
||||
android:key="@string/prefer_media_type_key"
|
||||
android:title="@string/preferred_media_settings"
|
||||
android:icon="@drawable/ic_baseline_play_arrow_24" />
|
||||
android:icon="@drawable/ic_baseline_play_arrow_24"
|
||||
android:key="@string/prefer_media_type_key"
|
||||
android:title="@string/preferred_media_settings" />
|
||||
<Preference
|
||||
android:key="@string/display_sub_key"
|
||||
android:title="@string/display_subbed_dubbed_settings"
|
||||
android:icon="@drawable/ic_outline_voice_over_off_24" />
|
||||
android:icon="@drawable/ic_outline_voice_over_off_24"
|
||||
android:key="@string/display_sub_key"
|
||||
android:title="@string/display_subbed_dubbed_settings" />
|
||||
|
||||
<SwitchPreference
|
||||
android:key="@string/enable_nsfw_on_providers_key"
|
||||
android:title="@string/enable_nsfw_on_providers"
|
||||
android:icon="@drawable/ic_baseline_extension_24"
|
||||
android:summary="@string/apply_on_restart"
|
||||
app:defaultValue="false"/>
|
||||
android:icon="@drawable/ic_baseline_extension_24"
|
||||
android:key="@string/enable_nsfw_on_providers_key"
|
||||
android:summary="@string/apply_on_restart"
|
||||
android:title="@string/enable_nsfw_on_providers"
|
||||
app:defaultValue="false" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/baseline_network_ping_24"
|
||||
android:key="@string/test_providers_key"
|
||||
android:title="Test all providers" />
|
||||
|
||||
</PreferenceScreen>
|
|
@ -35,7 +35,31 @@
|
|||
app:summary="@string/benene_des" />
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_category_links">
|
||||
android:title="@string/pref_category_bypass">
|
||||
|
||||
<Preference
|
||||
android:key="@string/override_site_key"
|
||||
android:title="@string/add_site_pref"
|
||||
android:summary="@string/add_site_summary"
|
||||
android:icon="@drawable/ic_baseline_add_24" />
|
||||
|
||||
<Preference
|
||||
android:key="@string/dns_key"
|
||||
android:title="@string/dns_pref"
|
||||
android:summary="@string/dns_pref_summary"
|
||||
android:icon="@drawable/ic_baseline_dns_24" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/ic_github_logo"
|
||||
android:key="@string/jsdelivr_proxy_key"
|
||||
android:title="@string/jsdelivr_proxy"
|
||||
android:summary="@string/jsdelivr_proxy_summary" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_category_links">
|
||||
|
||||
<Preference
|
||||
android:title="@string/github"
|
||||
|
|
Loading…
Reference in New Issue