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" /> +