diff --git a/README.md b/README.md
index 5e961c61..dcd4c5ed 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,8 @@
***The list of supported languages:***
* 🇱🇧 Arabic
* 🇧🇬 Bulgarian
+* 🇨🇳 Chinese Simplified
+* 🇹🇼 Chinese Traditional
* 🇭🇷 Croatian
* 🇨🇿 Czech
* 🇳🇱 Dutch
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f72eb321..26e7d3a5 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -191,7 +191,8 @@ dependencies {
// implementation("com.squareup.okhttp3:okhttp:4.9.2")
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
implementation("com.github.Blatzar:NiceHttp:0.3.5")
-
+ // To fix SSL fuckery on android 9
+ implementation("org.conscrypt:conscrypt-android:2.2.1")
// Util to skip the URI file fuckery 🙏
implementation("com.github.tachiyomiorg:unifile:17bec43")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 47676059..ae8479fe 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -110,6 +110,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
index 47a195d1..ef55eff0 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
@@ -108,9 +108,18 @@ object CommonActivity {
}
}
+ /**
+ * Not all languages can be fetched from locale with a code.
+ * This map allows sidestepping the default Locale(languageCode)
+ * when setting the app language.
+ **/
+ val appLanguageExceptions = hashMapOf(
+ "zh_TW" to Locale.TRADITIONAL_CHINESE
+ )
+
fun setLocale(context: Context?, languageCode: String?) {
if (context == null || languageCode == null) return
- val locale = Locale(languageCode)
+ val locale = appLanguageExceptions[languageCode] ?: Locale(languageCode)
val resources: Resources = context.resources
val config = resources.configuration
Locale.setDefault(locale)
@@ -146,8 +155,8 @@ object CommonActivity {
val resultCode = result.resultCode
val data = result.data
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
- val pos = data.getLongExtra(resumeApp.position, -1L)
- val dur = data.getLongExtra(resumeApp.duration, -1L)
+ val pos = resumeApp.getPosition(data)
+ val dur = resumeApp.getDuration(data)
if (dur > 0L && pos > 0L)
DataStoreHelper.setViewPos(getKey(resumeApp.lastId), pos, dur)
removeKey(resumeApp.lastId)
@@ -421,4 +430,4 @@ object CommonActivity {
}
return null
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
index 5c9f3071..e8ad476a 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
@@ -1143,9 +1143,9 @@ fun getDurationFromString(input: String?): Int? {
if (values.size == 3) {
val hours = values[1].toIntOrNull()
val minutes = values[2].toIntOrNull()
- return if (minutes != null && hours != null) {
- hours * 60 + minutes
- } else null
+ if (minutes != null && hours != null) {
+ return hours * 60 + minutes
+ }
}
}
Regex("([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
@@ -1153,6 +1153,27 @@ fun getDurationFromString(input: String?): Int? {
return values[1].toIntOrNull()
}
}
+ Regex("(\\s\\d+\\shr)|(\\s\\d+\\shour)|(\\s\\d+\\smin)|(\\s\\d+\\ssec)").findAll(input).let { values ->
+ var seconds = 0
+ values.forEach {
+ val time_text = it.value
+ if (time_text.isNotBlank()) {
+ val time = time_text.filter { s -> s.isDigit() }.trim().toInt()
+ val scale = time_text.filter { s -> !s.isDigit() }.trim()
+ //println("Scale: $scale")
+ val timeval = when (scale) {
+ "hr", "hour" -> time * 60 * 60
+ "min" -> time * 60
+ "sec" -> time
+ else -> 0
+ }
+ seconds += timeval
+ }
+ }
+ if (seconds > 0) {
+ return seconds / 60
+ }
+ }
return null
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
index ff74d6cc..c038d23a 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
@@ -16,11 +16,9 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
-import androidx.navigation.NavController
-import androidx.navigation.NavDestination
+import androidx.navigation.*
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
-import androidx.navigation.NavOptions
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
@@ -44,17 +42,21 @@ import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.mvvm.logError
+import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.plugins.PluginManager
+import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
+import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
@@ -88,11 +90,9 @@ import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
-import okhttp3.ConnectionSpec
-import okhttp3.OkHttpClient
-import okhttp3.internal.applyConnectionSpec
import java.io.File
import java.net.URI
+import java.net.URLDecoder
import java.nio.charset.Charset
import kotlin.reflect.KClass
@@ -115,13 +115,15 @@ val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlay
val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
//TODO REFACTOR AF
-data class ResultResume(
+open class ResultResume(
val packageString: String,
val action: String = Intent.ACTION_VIEW,
val position: String? = null,
val duration: String? = null,
var launcher: ActivityResultLauncher? = null,
) {
+ val defaultTime = -1L
+
val lastId get() = "${packageString}_last_open_id"
suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
val intent = Intent(action)
@@ -135,21 +137,45 @@ data class ResultResume(
callback.invoke(intent)
launcher?.launch(intent)
}
+
+ open fun getPosition(intent: Intent?): Long {
+ return defaultTime
+ }
+
+ open fun getDuration(intent: Intent?): Long {
+ return defaultTime
+ }
}
-val VLC = ResultResume(
+val VLC = object : ResultResume(
VLC_PACKAGE,
"org.videolan.vlc.player.result",
"extra_position",
"extra_duration",
-)
+) {
+ override fun getPosition(intent: Intent?): Long {
+ return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
+ }
-val MPV = ResultResume(
+ override fun getDuration(intent: Intent?): Long {
+ return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
+ }
+}
+
+val MPV = object : ResultResume(
MPV_PACKAGE,
//"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
- position = "position",
+ position = "position",
duration = "duration",
-)
+) {
+ override fun getPosition(intent: Intent?): Long {
+ return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime
+ }
+
+ override fun getDuration(intent: Intent?): Long {
+ return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime
+ }
+}
val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
@@ -188,6 +214,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
companion object {
const val TAG = "MAINACT"
+ /**
+ * Setting this will automatically enter the query in the search
+ * next time the search fragment is opened.
+ * This variable will clear itself after one use. Null does nothing.
+ *
+ * This is a very bad solution but I was unable to find a better one.
+ **/
+ private var nextSearchQuery: String? = null
+
/**
* Fires every time a new batch of plugins have been loaded, no guarantee about how often this is run and on which thread
* */
@@ -206,6 +241,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
isWebview: Boolean
): Boolean =
with(activity) {
+ // Invalid URIs can crash
+ fun safeURI(uri: String) = normalSafeApiCall { URI(uri) }
+
if (str != null && this != null) {
if (str.startsWith("https://cs.repo")) {
val realUrl = "https://" + str.substringAfter("?")
@@ -241,10 +279,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
return true
}
}
- } else if (URI(str).scheme == appStringRepo) {
+ } else if (safeURI(str)?.scheme == appStringRepo) {
val url = str.replaceFirst(appStringRepo, "https")
loadRepository(url)
return true
+ } else if (safeURI(str)?.scheme == appStringSearch) {
+ nextSearchQuery =
+ URLDecoder.decode(str.substringAfter("$appStringSearch://"), "UTF-8")
+ nav_view.selectedItemId = R.id.navigation_search
} else if (!isWebview) {
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
this.navigate(R.id.navigation_downloads)
@@ -553,7 +595,16 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
) {
PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
} else {
- PluginManager.loadAllOnlinePlugins(this@MainActivity)
+ loadAllOnlinePlugins(this@MainActivity)
+ }
+
+ //Automatically download not existing plugins
+ if (settingsManager.getBoolean(
+ getString(R.string.auto_download_plugins_key),
+ false
+ )
+ ) {
+ PluginManager.downloadNotExistingPluginsAndLoad(this@MainActivity)
}
}
@@ -619,6 +670,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
+
+ navController.addOnDestinationChangedListener { _: NavController, navDestination: NavDestination, bundle: Bundle? ->
+ // Intercept search and add a query
+ if (navDestination.matchDestination(R.id.navigation_search) && !nextSearchQuery.isNullOrBlank()) {
+ bundle?.apply {
+ this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery)
+ nextSearchQuery = null
+ }
+ }
+ }
+
//val navController = findNavController(R.id.nav_host_fragment)
/*navOptions = NavOptions.Builder()
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VoeExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VoeExtractor.kt
index d2f3f832..ad3f0150 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VoeExtractor.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VoeExtractor.kt
@@ -13,39 +13,42 @@ open class VoeExtractor : ExtractorApi() {
override val requiresReferer = false
private data class ResponseLinks(
- @JsonProperty("hls") val url: String?,
+ @JsonProperty("hls") val hls: String?,
+ @JsonProperty("mp4") val mp4: String?,
@JsonProperty("video_height") val label: Int?
//val type: String // Mp4
)
override suspend fun getUrl(url: String, referer: String?): List {
- val extractedLinksList: MutableList = mutableListOf()
- val doc = app.get(url).text
- if (doc.isNotBlank()) {
- val start = "const sources ="
- var src = doc.substring(doc.indexOf(start))
- src = src.substring(start.length, src.indexOf(";"))
+ val html = app.get(url).text
+ if (html.isNotBlank()) {
+ val src = html.substringAfter("const sources =").substringBefore(";")
+ // Remove last comma, it is not proper json otherwise
.replace("0,", "0")
- .trim()
+ // Make json use the proper quotes
+ .replace("'", "\"")
+
//Log.i(this.name, "Result => (src) ${src}")
- parseJson(src)?.let { voelink ->
- //Log.i(this.name, "Result => (voelink) ${voelink}")
- val linkUrl = voelink.url
- val linkLabel = voelink.label?.toString() ?: ""
+ parseJson(src)?.let { voeLink ->
+ //Log.i(this.name, "Result => (voeLink) ${voeLink}")
+
+ // Always defaults to the hls link, but returns the mp4 if null
+ val linkUrl = voeLink.hls ?: voeLink.mp4
+ val linkLabel = voeLink.label?.toString() ?: ""
if (!linkUrl.isNullOrEmpty()) {
- extractedLinksList.add(
+ return listOf(
ExtractorLink(
name = this.name,
source = this.name,
url = linkUrl,
quality = getQualityFromName(linkLabel),
referer = url,
- isM3u8 = true
+ isM3u8 = voeLink.hls != null
)
)
}
}
}
- return extractedLinksList
+ return emptyList()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/DdosGuardKiller.kt b/app/src/main/java/com/lagradost/cloudstream3/network/DdosGuardKiller.kt
index dca3ee00..b5783f78 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/network/DdosGuardKiller.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/network/DdosGuardKiller.kt
@@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.network
import androidx.annotation.AnyThread
import com.lagradost.cloudstream3.app
-import com.lagradost.nicehttp.Requests.Companion.await
+import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.cookies
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
@@ -41,7 +41,8 @@ class DdosGuardKiller(private val alwaysBypass: Boolean) : Interceptor {
savedCookiesMap[request.url.host]
// If no cookies are found fetch and save em.
?: (request.url.scheme + "://" + request.url.host + (ddosBypassPath ?: "")).let {
- app.get(it, cacheTime = 0).cookies.also { cookies ->
+ // Somehow app.get fails
+ Requests().get(it).cookies.also { cookies ->
savedCookiesMap[request.url.host] = cookies
}
}
@@ -51,6 +52,6 @@ class DdosGuardKiller(private val alwaysBypass: Boolean) : Interceptor {
request.newBuilder()
.headers(headers)
.build()
- ).await()
+ ).execute()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt
index 8bf1f91b..a1d84f6c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt
@@ -4,15 +4,19 @@ import android.content.Context
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT
+import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ignoreAllSSLErrors
import okhttp3.Cache
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders
import okhttp3.OkHttpClient
+import org.conscrypt.Conscrypt
import java.io.File
+import java.security.Security
fun Requests.initClient(context: Context): OkHttpClient {
+ normalSafeApiCall { Security.insertProviderAt(Conscrypt.newProvider(), 1) }
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
val dns = settingsManager.getInt(context.getString(R.string.dns_pref), 0)
baseClient = OkHttpClient.Builder()
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
index b9c775c0..f2dbb02f 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
@@ -13,11 +13,13 @@ import androidx.core.app.NotificationManagerCompat
import com.fasterxml.jackson.annotation.JsonProperty
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.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.showToast
+import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.mvvm.debugPrint
import com.lagradost.cloudstream3.mvvm.logError
@@ -26,6 +28,8 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDE
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins
+import com.lagradost.cloudstream3.ui.result.UiText
+import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.Coroutines.main
@@ -219,9 +223,7 @@ object PluginManager {
fun updateAllOnlinePluginsAndLoadThem(activity: Activity) {
// Load all plugins as fast as possible!
loadAllOnlinePlugins(activity)
-
- afterPluginsLoadedEvent.invoke(true)
-
+ afterPluginsLoadedEvent.invoke(true)
val urls = (getKey>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES
@@ -265,16 +267,98 @@ object PluginManager {
}
main {
- createNotification(activity, updatedPlugins)
+ val uitext = txt(R.string.plugins_updated, updatedPlugins.size)
+ createNotification(activity, uitext, updatedPlugins)
}
- // ioSafe {
+ // ioSafe {
afterPluginsLoadedEvent.invoke(true)
- // }
+ // }
Log.i(TAG, "Plugin update done!")
}
+ /**
+ * Automatically download plugins not yet existing on local
+ * 1. Gets all online data from online plugins repo
+ * 2. Fetch all not downloaded plugins
+ * 3. Download them and reload plugins
+ **/
+ fun downloadNotExistingPluginsAndLoad(activity: Activity) {
+ val newDownloadPlugins = mutableListOf()
+ val urls = (getKey>(REPOSITORIES_KEY)
+ ?: emptyArray()) + PREBUILT_REPOSITORIES
+ val onlinePlugins = urls.toList().apmap {
+ getRepoPlugins(it.url)?.toList() ?: emptyList()
+ }.flatten().distinctBy { it.second.url }
+
+ val providerLang = activity.getApiProviderLangSettings()
+ //Log.i(TAG, "providerLang => ${providerLang.toJson()}")
+
+ // Iterate online repos and returns not downloaded plugins
+ val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData ->
+ val sitePlugin = onlineData.second
+ //Don't include empty urls
+ if (sitePlugin.url.isBlank()) { return@mapNotNull null }
+ if (sitePlugin.repositoryUrl.isNullOrBlank()) { return@mapNotNull null }
+
+ //Omit already existing plugins
+ if (getPluginPath(activity, sitePlugin.internalName, onlineData.first).exists()) {
+ Log.i(TAG, "Skip > ${sitePlugin.internalName}")
+ return@mapNotNull null
+ }
+
+ //Omit lang not selected on language setting
+ val lang = sitePlugin.language ?: return@mapNotNull null
+ //If set to 'universal', don't skip any language
+ if (!providerLang.contains(AllLanguagesName) && !providerLang.contains(lang)) {
+ return@mapNotNull null
+ }
+ //Log.i(TAG, "sitePlugin lang => $lang")
+
+ //Omit NSFW, if disabled
+ sitePlugin.tvTypes?.let { tvtypes ->
+ if (!settingsForProvider.enableAdult) {
+ if (tvtypes.contains(TvType.NSFW.name)) {
+ return@mapNotNull null
+ }
+ }
+ }
+ val savedData = PluginData(
+ url = sitePlugin.url,
+ internalName = sitePlugin.internalName,
+ isOnline = true,
+ filePath = "",
+ version = sitePlugin.version
+ )
+ OnlinePluginData(savedData, onlineData)
+ }
+ //Log.i(TAG, "notDownloadedPlugins => ${notDownloadedPlugins.toJson()}")
+
+ notDownloadedPlugins.apmap { pluginData ->
+ downloadAndLoadPlugin(
+ activity,
+ pluginData.onlineData.second.url,
+ pluginData.savedData.internalName,
+ pluginData.onlineData.first
+ ).let { success ->
+ if (success)
+ newDownloadPlugins.add(pluginData.onlineData.second.name)
+ }
+ }
+
+ main {
+ val uitext = txt(R.string.plugins_downloaded, newDownloadPlugins.size)
+ createNotification(activity, uitext, newDownloadPlugins)
+ }
+
+ // ioSafe {
+ afterPluginsLoadedEvent.invoke(true)
+ // }
+
+ Log.i(TAG, "Plugin download done!")
+ }
+
/**
* Use updateAllOnlinePluginsAndLoadThem
* */
@@ -527,12 +611,14 @@ object PluginManager {
private fun createNotification(
context: Context,
- extensionNames: List
+ uitext: UiText,
+ extensions: List
): Notification? {
try {
- if (extensionNames.isEmpty()) return null
- val content = extensionNames.joinToString(", ")
+ if (extensions.isEmpty()) return null
+
+ val content = extensions.joinToString(", ")
// main { // DON'T WANT TO SLOW IT DOWN
val builder = NotificationCompat.Builder(context, EXTENSIONS_CHANNEL_ID)
.setAutoCancel(false)
@@ -541,7 +627,8 @@ object PluginManager {
.setSilent(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
- .setContentTitle(context.getString(R.string.plugins_updated, extensionNames.size))
+ .setContentTitle(uitext.asString(context))
+ //.setContentTitle(context.getString(title, extensionNames.size))
.setSmallIcon(R.drawable.ic_baseline_extension_24)
.setStyle(
NotificationCompat.BigTextStyle()
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
index 825ff673..388e1774 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
@@ -43,6 +43,9 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
const val appString = "cloudstreamapp"
const val appStringRepo = "cloudstreamrepo"
+ // Instantly start the search given a query
+ const val appStringSearch = "cloudstreamsearch"
+
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
val unixTimeMs: Long
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt
index bfa65f62..f22fdd8b 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt
@@ -15,6 +15,8 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
import com.lagradost.cloudstream3.utils.AppUtils
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets
class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
override val idPrefix = "opensubtitles"
@@ -175,7 +177,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
val searchQueryUrl = when (imdbId > 0) {
//Use imdb_id to search if its valid
true -> "$host/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
- false -> "$host/subtitles?query=$queryText&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
+ false -> "$host/subtitles?query=${URLEncoder.encode(queryText.lowercase(), StandardCharsets.UTF_8.toString())}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
}
val req = app.get(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt
index 0f9a6548..c79cdd76 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt
@@ -612,6 +612,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
//player_media_route_button?.isClickable = !isGone
player_go_back_holder?.isGone = isGone
player_sources_btt?.isGone = isGone
+ player_skip_episode?.isClickable = !isGone
}
private fun updateLockUI() {
@@ -1101,7 +1102,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
}
protected fun uiReset() {
- isLocked = false
isShowing = false
// if nothing has loaded these buttons should not be visible
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt
index 0c26f69c..da900b0a 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt
@@ -1582,7 +1582,6 @@ class ResultViewModel2 : ViewModel() {
return
}
- val episodes = currentEpisodes[indexer]
val ranges = currentRanges[indexer]
if (ranges?.contains(range) != true) {
@@ -1594,7 +1593,6 @@ class ResultViewModel2 : ViewModel() {
}
}
- val size = episodes?.size
val isMovie = currentResponse?.isMovie() == true
currentIndex = indexer
currentRange = range
@@ -1604,6 +1602,7 @@ class ResultViewModel2 : ViewModel() {
text to r
} ?: emptyList())
+ val size = currentEpisodes[indexer]?.size
_episodesCountText.postValue(
some(
if (isMovie) null else
@@ -1683,9 +1682,12 @@ class ResultViewModel2 : ViewModel() {
generator = if (isMovie) {
getMovie()?.let { RepoLinkGenerator(listOf(it), page = currentResponse) }
} else {
- episodes?.let { list ->
- RepoLinkGenerator(list, page = currentResponse)
- }
+ val episodes = currentEpisodes.filter { it.key.dubStatus == indexer.dubStatus }
+ .toList()
+ .sortedBy { it.first.season }
+ .flatMap { it.second }
+
+ RepoLinkGenerator(episodes, page = currentResponse)
}
if (isMovie) {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt
index 4da88af7..4e59e6a0 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt
@@ -73,6 +73,14 @@ class SearchFragment : Fragment() {
}
}
}
+
+ const val SEARCH_QUERY = "search_query"
+
+ fun newInstance(query: String): Bundle {
+ return Bundle().apply {
+ putString(SEARCH_QUERY, query)
+ }
+ }
}
private val searchViewModel: SearchViewModel by activityViewModels()
@@ -132,7 +140,8 @@ class SearchFragment : Fragment() {
val default = enumValues().sorted().filter { it != TvType.NSFW }
.map { it.ordinal.toString() }.toSet()
val preferredTypes = (PreferenceManager.getDefaultSharedPreferences(ctx)
- .getStringSet(this.getString(R.string.prefer_media_type_key), default)?.ifEmpty { default } ?: default)
+ .getStringSet(this.getString(R.string.prefer_media_type_key), default)
+ ?.ifEmpty { default } ?: default)
.mapNotNull { it.toIntOrNull() ?: return@mapNotNull null }
val settings = ctx.getApiSettings()
@@ -487,6 +496,14 @@ class SearchFragment : Fragment() {
search_master_recycler?.adapter = masterAdapter
search_master_recycler?.layoutManager = GridLayoutManager(context, 1)
+ // Automatically search the specified query, this allows the app search to launch from intent
+ arguments?.getString(SEARCH_QUERY)?.let { query ->
+ if (query.isBlank()) return@let
+ main_search?.setQuery(query, true)
+ // Clear the query as to not make it request the same query every time the page is opened
+ arguments?.putString(SEARCH_QUERY, null)
+ }
+
// SubtitlesFragment.push(activity)
//searchViewModel.search("iron man")
//(activity as AppCompatActivity).loadResult("https://shiro.is/overlord-dubbed", "overlord-dubbed", "Shiro")
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt
index 8ea76cda..551a80ab 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt
@@ -47,7 +47,7 @@ fun getCurrentLocale(context: Context): String {
// Change locale settings in the app.
// val dm = res.displayMetrics
val conf = res.configuration
- return conf?.locale?.language ?: "en"
+ return conf?.locale?.toString() ?: "en"
}
// idk, if you find a way of automating this it would be great
@@ -75,7 +75,8 @@ val appLanguages = arrayListOf(
Triple("\uD83C\uDDE7\uD83C\uDDF7", "Brazilian Portuguese", "bp"),
Triple("", "Romanian", "ro"),
Triple("", "Italian", "it"),
- Triple("", "Chinese", "zh"),
+ Triple("", "Chinese Simplified", "zh"),
+ Triple("\uD83C\uDDF9\uD83C\uDDFC", "Chinese Traditional", "zh_TW"),
Triple("\uD83C\uDDEE\uD83C\uDDE9", "Indonesian", "in"),
Triple("", "Czech", "cs"),
Triple("", "Croatian", "hr"),
@@ -368,4 +369,4 @@ class SettingsGeneral : PreferenceFragmentCompat() {
e.printStackTrace()
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt
index bacd26c8..bd44a058 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt
@@ -8,6 +8,8 @@ import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
+import com.lagradost.cloudstream3.AllLanguagesName
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.mvvm.observe
@@ -45,6 +47,15 @@ class PluginsFragment : Fragment() {
pluginViewModel.languages = listOf()
pluginViewModel.search(null)
+ // Filter by language set on preferred media
+ activity?.let {
+ val providerLangs = it.getApiProviderLangSettings().toList()
+ if (!providerLangs.contains(AllLanguagesName)) {
+ pluginViewModel.languages = mutableListOf("none") + providerLangs
+ //Log.i("DevDebug", "providerLang => ${pluginViewModel.languages.toJson()}")
+ }
+ }
+
val name = arguments?.getString(PLUGINS_BUNDLE_NAME)
val url = arguments?.getString(PLUGINS_BUNDLE_URL)
val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true
diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml
index 6ae2fa04..14d750a0 100644
--- a/app/src/main/res/navigation/mobile_navigation.xml
+++ b/app/src/main/res/navigation/mobile_navigation.xml
@@ -274,7 +274,12 @@
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim"
- tools:layout="@layout/fragment_search" />
+ tools:layout="@layout/fragment_search">
+
+
Appliquer
Annuler
Vitesse de lecture
- Paramètres de sous-titres
- Couleur du texte
- Couleur de la bordure exterieur
- Couleur de fond
- Couleur de la fenètre
- Type de bordure
- Élévation des sous-titres
Aperçu de l\'arrière-plan
- Police
- Rechercher en utilisant les fournisseurs
- Rechercher en utilisant les types
- %d Benenes données au dev
- Aucune Benenes donnée
- Sélection automatique de la langue
- Télécharger les langues
- Maintenir pour réinitialiser les valeurs par défaut
- Continuer à regarder
- Supprimer
- Plus d\'info
- Un VPN peut être requit pour que ce fournisseur fonctionne
- Ce fournisseur est un torrent, un VPN est recommandé
- Description
- Aucune description trouvée
- Aucune description trouvée
- Lecteur en mode Picture-in-Picture
- Continuer la lecture dans une fenêtre miniature en superposition sur d\'autres applis
- Bouton de redimensionnement du lecteur
- Supprimer les bordures noires
- Sous-titres
- Paramètres des sous-titres du lecteur
- Vitesse de lecture
- Ajouter l\'option de vitesse sur le lecteur
- Balayer pour avancer rapidement
- Balayer vers la gauche ou la droite pour contrôler le temps du lecteur vidéo
- Balayer pour changer les paramètres
- Balayer sur le coté droit ou gauche pour changer le niveau de luminosité ou de volume
- Taper deux fois pour rechercher
-
- Taper deux fois pour mettre en pause
-
- Taper deux fois sur le coté droit ou gauche pour avancer ou reculer
- Rechercher
- Informations
- Recherche Avancée
- Donne les résultats séparés par les fournisseurs
- N\'envoyer les données que lors d\'un crash
- N\'envoyer aucune données
- Afficher les mises-à-jour de l\'application
- Chercher des mises-à-jour automatiquement au démarage
- Mettre à jour vers une version bêta
- Rechercher pour une mise à jour vers une version bêta au lieu des version complètes seulement
- Github
- L\'application Light Novel par les mêmes devs
- Application d\'animés par les mêmes devs
- Rejoindre le serveur Discord
Donner une benene aux devs
benenes données
Language de l\'application
@@ -192,16 +138,12 @@
Couleur principale
Thème de l\'application
Vitesse (%.2fx)
- Utiliser la luminosité du système
Général
DNS avec HTTPS
Afficher les animés en Anglais (Dub) / sous-titrés
Disposition en mode téléphone
%s Ep %d
Note : %.1f
- Taille de la police
- Utiliser la luminosité du système dans le lecteur de l\'application au lieu d\'un écran noir
- Afficher les épisodes spéciaux pour les animés
Zoom
Adapter à l\'écran
Disposition de l\'application
@@ -211,27 +153,8 @@
Auto
Acteurs: %s
%d min
- Rechercher sur %s...
+ Rechercher sur %s…
À re-regarder
- Copier
- Coller
- Effacer
- Enregister
- Importer des polices en les plaçants dans %s
- Les metadonnées ne sont pas fournies par le site, le chargement de la vidéo va échouer si elle n\'existe pas sur le site.
- Afficher les logs 🐈
- Sous-titres Chromecast
- Paramètres des sous-titres Chromecast
- Taper au milieu pour mettre en pause
- Mettre à jour la progression de visionnage
- Synchroniser automatiquement votre progression de l\'épisode actuel
- Restaurer les données sauvegardées
- Sauvegarder les données
- Fichier de sauvegarde chargé
- Échec de la restauration des données depuis le fichier
- Restauration des données réussie
- Permission d\'accès au stockage manquante
- Erreur pendant la sauvegarde %s
Aucun épisode trouvé
Documentaires
OVA
@@ -289,8 +212,117 @@
Arrière plan
Source
Aléatoire
- À venir ...
+ À venir …
Image de l\'affiche
Connecté %s
Définir le statut de visionage
+ Copier
+ Fermer
+ Vider
+ Enregistrer
+
+
+ Paramètres des sous-titres
+ Couleur du texte
+ Couleur des contours
+ Couleur d\'arrière-plan
+ Couleur de la fenêtre
+ Type de bordure
+ Elevation des sous-titres
+ Police
+ Taille de la police
+
+ Recherche par fournisseur
+ Recherche par types
+
+ %d Donner une banane aux devs
+ Aucun Bananes donné
+
+ Sélection automatique de la langue
+ Télécharger les langues
+ Langue des sous-titres
+ Maintenir pour rétablir les valeurs par défaut
+ Importez des polices en les plaçant dans %s
+ Continuer à regarder
+
+ Retirer
+ Plus d\'informations
+ @string/home_play
+
+ Un VPN peut être nécessaire pour que ce fournisseur fonctionne correctement
+ Ce fournisseur est un torrent, un VPN est recommandé
+
+ Les métadonnées ne sont pas fournies par le site, le chargement de la vidéo échouera si elles n\'existent pas sur le site.
+
+ Description
+ Aucune trace trouvée
+ Aucune description trouvée
+
+ Afficher le logcat 🐈
+
+ Picture-in-picture
+ Poursuite de la lecture dans un lecteur miniature au-dessus d\'autres applications
+ Bouton de redimensionnement du lecteur
+ Supprimer les bordures noires
+ Sous-titres
+ Paramètres des sous-titres du lecteur
+ Sous-titres Chromecast
+ Paramètres des sous-titres Chromecast
+
+ Mode Eigengravy
+ Ajout d\'une option de vitesse dans le lecteur
+ Balayez pour chercher
+ Balayez vers la gauche ou la droite pour contrôler le temps dans le lecteur vidéo.
+ Balayez pour modifier les paramètres
+ Glissez sur le côté gauche ou droit pour modifier la luminosité ou le volume.
+
+ Lecture automatique du prochain épisode
+ Démarrer l\'épisode suivant lorsque l\'épisode en cours se termine
+
+ Double tape pour chercher
+ Double tape pour mettre en pause
+ Player seek amount
+ Tapez deux fois sur le côté droit ou gauche pour aller en avant ou en arrière.
+
+ Tapez au milieu pour mettre en pause
+ Utiliser la luminosité du système
+ Utiliser la luminosité du système dans le lecteur d\'applications au lieu du
+ sombre
+
+
+ Mise à jour de la progression de la veille
+ Synchronisation automatique de la progression de votre épisode en cours
+
+ Restaurer des données à partir d\'une sauvegarde
+
+ Sauvegarde des données
+ Fichier de sauvegarde chargé
+ Échec de la restauration des données du fichier %s
+ Données stockées avec succès
+ Permissions de stockage manquantes, veuillez réessayer
+ Erreur de sauvegarde %s
+
+ Recherche
+ Comptes
+ Mises à jour et sauvegarde
+
+ Info
+ Recherche avancée
+ Vous donne les résultats de la recherche séparés par fournisseur
+ Envoi de données uniquement en cas d\'accident
+ N\'envoie aucune donnée
+ Afficher les épisodes spéciaux pour les animés
+ Montrer les bandes-annonces
+ Montrer les affiches de kitsu
+ Masquer la qualité vidéo sélectionnée dans les résultats de recherche
+
+ Mises à jour automatiques des plugins
+ Afficher les mises à jour de l\'application
+ Recherche automatique de nouvelles mises à jour au démarrage
+ Mettre à jour vers une version bêta
+ Recherche pour une mise à jour vers une version bêta au lieu des version complètes seulement
+ Github
+ Application Light Novel par les mêmes devs
+ Anime app by the same devs
+ Rejoignez le Discord
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 00000000..5b71a3f9
--- /dev/null
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,591 @@
+
+
+
+ %d %s | %s
+ %s • %s
+ %s / %s
+ %s %s
+ +%d
+ -%d
+ %d
+ %d
+ %.1f/10.0
+ %d
+ %s 共 %d 集
+ 演員:%s
+ 第 %d 集即將發佈於
+ %dd %dh %dm
+ %dh %dm
+ %dm
+
+
+ 封面
+ @string/result_poster_img_des
+ 劇集封面
+ 主封面
+ 隨機下一個
+ @string/play_episode
+ 返回
+ @string/home_change_provider_img_des
+ 更改片源
+ 預覽背景
+
+
+ 速度(%.2fx)
+ 評分:%.1f
+ 發現新版本!\n%s -> %s
+ 填充
+ %d 分鐘
+
+ CloudStream
+ 使用 CloudStream 播放
+ 主頁
+ 搜尋
+ 下載
+ 設定
+
+ 搜尋…
+ 搜尋 %s…
+
+ 無資料
+ 更多選項
+ 下一集
+ @string/synopsis
+ 類型
+ 分享
+ 在瀏覽器中打開
+ 跳過載入
+ 載入中…
+
+ 正在觀看
+ 暫時擱置
+ 觀看完畢
+ 放棄觀看
+ 計畫觀看
+ 無
+ 重新觀看
+
+ 播放電影
+ 播放直播
+ 播放種子
+ 來源
+ 字幕
+ 重試連接…
+ 返回
+ 播放劇集
+
+
+ 下載
+ 已下載
+ 下載中
+ 下載暫停
+ 下載開始
+ 下載失敗
+ 下載取消
+ 下載完畢
+ %s - %s
+ 播放
+
+ 載入連結錯誤
+ 內部存儲
+
+ 配音
+ 字幕
+
+ 刪除檔案
+ 播放檔案
+ 繼續下載
+ 暫停下載
+
+ 禁用自動錯誤報告
+ 更多資訊
+ 隱藏
+ 播放
+ 資訊
+ 篩選書籤
+ 書籤
+ 移除
+ 設定觀看狀態
+ 套用
+ 取消
+ 複製
+ 關閉
+ 清除
+ 保存
+
+ 播放速度
+
+ 字幕設定
+ 字體顏色
+ 輪廓顏色
+ 背景顏色
+ 視窗顏色
+ 邊緣類型
+ 字幕高度
+ 字體
+ 字體大小
+
+ 按片源搜尋
+ 按類型搜尋
+
+ 送開發者 %d 根香蕉
+ 不送香蕉
+
+ 自動選擇語言
+ 下載語言
+ 字幕語言
+ 按住重設為預設值
+ 將字體導入到 %s
+ 繼續觀看
+
+ 移除
+ 更多資訊
+ @string/home_play
+
+ 此片源可能需要 VPN 才能正常使用
+ 此片源是種子,建議使用 VPN
+
+ 站點不提供元數據,如果站點上不存在元數據,影片載入將失敗。
+
+ 簡介
+ 未找到簡介
+ 未找到簡介
+
+ 顯示 logcat 🐈
+
+ 字母畫面
+ 在其他應用程式上的子母畫面中繼續播放
+ 播放器調整大小按鈕
+ 移除黑色邊框
+ 字幕
+ 播放器字幕設定
+ Chromecast 字幕
+ Chromecast 字幕設定
+
+ 播放速度
+ 在播放器中添加播放速度選項
+ 活動控制進度
+ 左右滑動控制播放進度
+ 滑動更改設定
+ 上下滑動更改亮度或音量
+
+ 自動播放下一集
+ 播放完畢後播放下一集
+
+ 輕按兩下以控制進度
+ 輕按兩下以暫停
+ 輕按兩下以控制進度時間
+ 在右側或左側輕按兩次以向前或向後快轉
+
+ 輕按兩下中間以暫停
+ 使用系統亮度
+ 在應用程序播放器中使用系統亮度替代黑色遮罩
+
+ 更新觀看進度
+ 自動同步當前劇集進度
+
+ 從備份中恢復資料
+
+ 備份資料
+ 已載入備份資料
+ 無法從 %s 檔案中還原資料
+ 成功儲存資料
+ 缺少儲存權限,請重試
+ 備份 %s 錯誤
+
+ 搜尋
+ 帳號
+ 更新與備份
+
+ 資訊
+ 進階搜尋
+ 為您提供按片源分開的搜尋結果
+ 僅在崩潰時傳送資料
+ 不傳送資料
+ 顯示動畫外傳
+ 顯示預告片
+ 顯示來自 Kitsu 的封面
+ 在搜尋結果中隱藏選中的影片畫質
+
+ 自動更新外掛程式
+ 顯示應用更新
+ 啟動時自動搜尋更新
+ 更新至預覽版
+ 搜尋預覽版更新而不是僅搜尋正式版
+ Github
+ 由相同開發者開發的輕小說應用程式
+ 由相同開發者開發的動漫應用程式
+ 加入 Discord
+ 送開發者一根香蕉
+ 送香蕉
+
+ 應用程式語言
+
+ 此片源不支援 Chromecast
+ 未找到連結
+ 連結已複製到剪貼簿
+ 播放劇集
+ 重設為預設值
+ 很抱歉,應用崩潰了,將傳送一份匿名錯誤報告給開發者
+
+ 季
+ %s %d%s
+ 無季
+ 集
+ 集
+ %d-%d
+ %d %s
+ S
+ E
+ 未找到劇集
+
+ 刪除文件
+ 刪除
+ @string/sort_cancel
+ 暫停
+ 繼續
+ -30
+ +30
+ 這將永遠刪除 %s\n你確定嗎?
+ 剩餘 %d 分鐘
+
+
+ 連載中
+ 已完結
+ 狀態
+ 年份
+ 評分
+ 時間
+ 網站
+ 簡介
+
+ 已加入佇列
+ 無字幕
+ 預設
+
+ 空閒
+ 已使用
+ 應用程式
+
+
+ 電影
+ 電視劇
+ 卡通
+ 動漫
+ 種子
+ 紀錄片
+ 原創動畫錄影帶
+ 亞洲劇
+ 直播
+ NSFW
+ 其他
+
+
+ 電影
+ 電視劇
+ 卡通
+ @string/anime
+ @string/ova
+ 種子
+ 紀錄片
+ 亞洲劇
+ 直播
+ NSFW
+ 其他
+
+ 來源錯誤
+ 遠端錯誤
+ 渲染器錯誤
+ 意料之外的播放器錯誤
+ 下載錯誤,請檢查儲存權限
+
+ Chromecast 劇集
+ Chromecast 鏡像
+ 在應用程式中播放
+ 在 %s 中播放
+ 在瀏覽器中播放
+ 複製連結
+ 自動下載
+ 下載鏡像
+ 重新載入連結
+ 下載字幕
+
+ 畫質標籤
+ 配音標籤
+ 字幕標籤
+ 標題
+ show_hd_key
+ show_dub_key
+ show_sub_key
+ show_title_key
+ 封面內容
+
+ 未找到更新
+ 检查更新
+
+ 鎖定
+ 調整大小
+ 來源
+ 跳過片頭
+
+ 不再顯示
+ 跳過此更新
+ 更新
+ 偏好播放畫質
+ 影片播放器標題最大字數
+ 影片播放器標題
+
+ 影片緩衝大小
+ 影片緩衝長度
+ 影片快取存儲
+ 清除影片和圖片快取
+
+ 如果設定得太高會導致隨機崩潰。 如果您的記憶體不足(例如 Android TV 或舊手機),請不要更改
+ 如果您將其設定得太高,可能會導致儲存空間不足的系統(例如 Android TV 設備)出現問題
+
+ DNS over HTTPS
+ 用於繞過網路服務供應商的封鎖
+
+ 複製片源
+ 移除片源
+ 添加具有不同URL的現有站點複製
+
+ 下載路徑
+
+ Nginx 伺服器連結
+
+ 顯示有配音/字幕的動漫
+
+ 適應螢幕
+ 拉伸
+ 縮放
+
+ 免責聲明
+ legal_notice_key
+ Any legal issues regarding the content on this application
+ should be taken up with the actual file hosts and providers themselves as we are not affiliated with them.
+
+ In case of copyright infringement, please directly contact the responsible parties or the streaming websites.
+
+ The app is purely for educational and personal use.
+
+ CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down.
+ CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or
+ manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient,
+ user-friendly interface.
+
+ It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the
+ responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use
+ CloudStream 3 at your own risk.
+
+ 通用
+ 隨機按鈕
+ 在主頁中顯示隨機按鈕
+ 片源語言
+ 應用佈局
+ 偏好類型
+ 在支援的片源中啟用 NSFW 內容
+ 字幕編碼
+ 片源
+ 佈局
+
+ 自動
+ 電視佈局
+ 手機佈局
+ 模擬器佈局
+
+ 主題色
+ 應用程式主題
+ 封面標題位置
+ 將標題移到封面下方
+
+
+
+ anilist_key
+ mal_key
+ opensubtitles_key
+ nginx_key
+ 密碼
+ 用戶名
+ 電子郵件
+ IP
+ 網站名稱
+ 網站連結
+ 語言代號 (zh_TW)
+
+
+ %s %s
+ 帳號
+ 登出
+ 登入
+ 切換帳號
+ 添加帳號
+ 創建帳號
+ 添加同步
+ 已添加 %s
+ 同步
+ 評分
+ %d / 10
+ /??
+ /%d
+ 已驗證 %s
+ 驗證 %s 失敗
+
+
+ 無
+ 普通
+ 全部
+ 最大
+ 最小
+ @string/none
+ 輪廓
+ 凹陷
+ 陰影
+ 凸出
+ 同步字幕
+ 1000ms
+ 字幕延遲
+ 如果字幕過早顯示 %dms ,請使用此選項
+ 如果字幕過晚顯示 %dms ,請使用此選項
+ 無字幕延遲
+
+
+ The quick brown fox jumps over the lazy dog
+
+ 推薦
+ 已載入 %s
+ 從檔案載入
+ 從網路載入
+ 下載的檔案
+ 主角
+ 配角
+ 群演
+
+ 來源
+ 隨機
+
+ 即將到來…
+
+ Cam
+ Cam
+ Cam
+ HQ
+ HD
+ TS
+ TC
+ BlueRay
+ WP
+ DVD
+ 4K
+ SD
+ UHD
+ HDR
+ SDR
+ Web
+
+ 封面圖片
+ 播放器
+ 解析度與標題
+ 標題
+ 解析度
+ 無效 ID
+ 無效資料
+ 無效連結
+ 錯誤
+ 移除隱藏式字幕
+ 移除字幕廣告
+ 按偏好片源語言過濾
+ 附加
+ 預告片
+ 播放連結
+ 推薦
+ 下一個
+ 觀看這些語言的影片
+ 上一個
+ 跳過設定
+ 更改應用程式的外觀以適應你的設備
+ 崩潰報告
+ 你想要看什麼
+ 完成
+ 擴充功能
+ 添加資源庫
+ 資源庫名稱
+ 資源庫連結
+ 外掛程式已載入
+ 外掛程式已刪除
+ 載入 %s 失敗
+ 18+
+ 開始下載 %d %s
+ 下載 %d %s 成功
+ 全部 %s 已經下載
+ 批次下載
+ 外掛程式
+ 外掛程式
+ 這也將刪除所有資源庫外掛程式
+ 刪除資源庫
+ 下載你所需的片源
+ 已下載:%d
+ 已禁用:%d
+ 未下載:%d
+ 已更新 %d 外掛程式
+ CloudStream 預設沒有安裝任何片源。您需要從資源庫安裝站點。\n\n由於 Sky Uk Limited 的無腦 DMCA 刪除🤮,我們無法在應用程式中連結資源庫站點。\n\n加入我們的 Discord 獲得連結或自己在網路上搜尋
+ 查看
+ 公開列表
+ 字幕全大寫
+
+ 從此資源庫下載所有外掛程式?
+ %s (禁用)
+ 軌道
+ 音頻軌道
+ 影片軌道
+ 重新啟動時生效
+
+ 安全模式已啟用
+ 發生了不可恢復的崩潰,我們已自動禁用所有外掛程式,因此您可以找到並刪除導致問題的應用程式。
+ 查看崩潰資訊
+
+ 評分:%s
+ 簡介
+ 版本
+ 狀態
+ 大小
+ 作者
+ 類型
+ 語言
+ 請先安裝外掛程式
+
+ HLS 播放清單
+
+ 偏好影片播放器
+ 內部播放器
+ VLC
+ MPV
+ 網路影片播放
+ 瀏覽器
+ 未找到應用
+ 所有語言
+
+ 跳過 %s
+ 片頭
+ 片尾
+ 前情回顧
+ 混合片尾
+ 混合片頭
+ 致謝名單
+ 介紹
+
+ 清除歷史紀錄
+ 歷史紀錄
+
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 19d9bd43..d609b9af 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -209,6 +209,7 @@
在搜索结果中隐藏选中视频画质
自动更新插件
+ 自动下载插件
显示应用更新
启动时自动搜索更新
更新至预览版
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9381372c..db042b95 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -6,6 +6,7 @@
search_type_list
auto_update
auto_update_plugins
+ auto_download_plugins_key
skip_update_key
prerelease_update
manual_check_update
@@ -269,6 +270,7 @@
Hide selected video quality on Search results
Automatic plugin updates
+ Automatically download plugins
Show app updates
Automatically search for new updates on start
Update to prereleases
diff --git a/app/src/main/res/xml/settings_updates.xml b/app/src/main/res/xml/settings_updates.xml
index eaceb785..3a17f393 100644
--- a/app/src/main/res/xml/settings_updates.xml
+++ b/app/src/main/res/xml/settings_updates.xml
@@ -33,6 +33,11 @@
android:icon="@drawable/ic_baseline_extension_24"
android:key="@string/auto_update_plugins_key"
android:title="@string/automatic_plugin_updates" />
+