diff --git a/.github/locales.py b/.github/locales.py index e95ec902..1c79c093 100644 --- a/.github/locales.py +++ b/.github/locales.py @@ -7,7 +7,7 @@ SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/Settin START_MARKER = "/* begin language list */" END_MARKER = "/* end language list */" XML_NAME = "app/src/main/res/values-" -ISO_MAP_URL = "https://gist.githubusercontent.com/Josantonius/b455e315bc7f790d14b136d61d9ae469/raw" +ISO_MAP_URL = "https://raw.githubusercontent.com/haliaeetus/iso-639/master/data/iso_639-1.min.json" INDENT = " "*4 iso_map = requests.get(ISO_MAP_URL, timeout=300).json() @@ -27,7 +27,8 @@ for lang in re.finditer(r'Triple\("(.*)", "(.*)", "(.*)"\)', rest): for folder in glob.glob(f"{XML_NAME}*"): iso = folder[len(XML_NAME):] if iso not in languages.keys(): - languages[iso] = ("", iso_map.get(iso.lower(),iso)) + entry = iso_map.get(iso.lower(),{'nativeName':iso}) + languages[iso] = ("", entry['nativeName'].split(',')[0]) # Create triples triples = [] diff --git a/.github/workflows/issue_action.yml b/.github/workflows/issue_action.yml index 28b737b3..108cec82 100644 --- a/.github/workflows/issue_action.yml +++ b/.github/workflows/issue_action.yml @@ -15,6 +15,7 @@ jobs: app_id: ${{ secrets.GH_APP_ID }} private_key: ${{ secrets.GH_APP_KEY }} - name: Similarity analysis + id: similarity uses: actions-cool/issues-similarity-analysis@v1 with: token: ${{ steps.generate_token.outputs.token }} @@ -24,6 +25,18 @@ jobs: ### Your issue looks similar to these issues: Please close if duplicate. comment-body: '${index}. ${similarity} #${number}' + - name: Label if possible duplicate + if: steps.similarity.outputs.similar-issues-found =='true' + uses: actions/github-script@v6 + with: + github-token: ${{ steps.generate_token.outputs.token }} + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ["possible duplicate"] + }) - uses: actions/checkout@v2 - name: Automatically close issues that dont follow the issue template uses: lucasbento/auto-close-issues@v1.0.2 @@ -53,6 +66,18 @@ jobs: Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM). Found provider name: `${{ steps.provider_check.outputs.name }}` + - name: Label if mentions provider + if: steps.provider_check.outputs.name != 'none' + uses: actions/github-script@v6 + with: + github-token: ${{ steps.generate_token.outputs.token }} + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ["possible provider issue"] + }) - name: Add eyes reaction to all issues uses: actions-cool/emoji-helper@v1.0.0 with: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f8e0091c..3c855d28 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -47,8 +47,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 55 - versionName = "3.4.0" + versionCode = 57 + versionName = "4.0.0" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") @@ -190,7 +190,7 @@ dependencies { // Networking // implementation("com.squareup.okhttp3:okhttp:4.9.2") // implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") - implementation("com.github.Blatzar:NiceHttp:0.4.1") + implementation("com.github.Blatzar:NiceHttp:0.4.2") // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 @@ -220,6 +220,9 @@ dependencies { // Library/extensions searching with Levenshtein distance implementation("me.xdrop:fuzzywuzzy:1.4.0") + + // color pallette for images -> colors + implementation("androidx.palette:palette-ktx:1.0.0") } tasks.register("androidSourcesJar", Jar::class) { @@ -250,4 +253,4 @@ tasks.withType().configureEach { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 198f0f4c..0351b1ff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -43,9 +43,9 @@ class CustomReportSender : ReportSender { override fun send(context: Context, errorContent: CrashReportData) { println("Sending report") val url = - "https://docs.google.com/forms/u/0/d/e/1FAIpQLSe9Vff8oHGMRXcjgCXZwkjvx3eBdNpn4DzjO0FkcWEU1gEQpA/formResponse" + "https://docs.google.com/forms/d/e/1FAIpQLSdOlbgCx7NeaxjvEGyEQlqdh2nCvwjm2vwpP1VwW7REj9Ri3Q/formResponse" val data = mapOf( - "entry.1586460852" to errorContent.toJSON() + "entry.753293084" to errorContent.toJSON() ) thread { // to not run it on main thread diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 6565b144..a277f622 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -13,11 +13,11 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi +import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.player.SubtitleData -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf -import com.lagradost.cloudstream3.utils.ExtractorLink import okhttp3.Interceptor import java.text.SimpleDateFormat import java.util.* @@ -81,8 +81,8 @@ object APIHolder { synchronized(allProviders) { initMap() return apiMap?.get(apiName)?.let { apis.getOrNull(it) } - // Leave the ?. null check, it can crash regardless - ?: allProviders.firstOrNull { it?.name == apiName } + // Leave the ?. null check, it can crash regardless + ?: allProviders.firstOrNull { it.name == apiName } } } @@ -160,6 +160,53 @@ object APIHolder { return null } + private var trackerCache: HashMap = hashMapOf() + + /** + * Get anime tracker information based on title, year and type. + * Both titles are attempted to be matched with both Romaji and English title. + * Uses the consumet api. + * + * @param titles uses first index to search, but if you have multiple titles and want extra guarantee to match you can also have that + * @param types Optional parameter to narrow down the scope to Movies, TV, etc. See TrackerType.getTypes() + * @param year Optional parameter to only get anime with a specific year + **/ + suspend fun getTracker( + titles: List, + types: Set?, + year: Int? + ): Tracker? { + return try { + require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" } + + val mainTitle = titles[0] + val search = + trackerCache[mainTitle] + ?: app.get("https://api.consumet.org/meta/anilist/$mainTitle") + .parsedSafe()?.also { + trackerCache[mainTitle] = it + } ?: return null + + val res = search.results?.find { media -> + val matchingYears = year == null || media.releaseDate == year + val matchingTitles = media.title?.let { title -> + titles.any { userTitle -> + title.isMatchingTitles(userTitle) + } + } ?: false + + val matchingTypes = types?.any { it.name.equals(media.type, true) } == true + matchingTitles && matchingTypes && matchingYears + } ?: return null + + Tracker(res.malId, res.aniId, res.image, res.cover) + } catch (t: Throwable) { + logError(t) + null + } + } + + fun Context.getApiSettings(): HashSet { //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) @@ -318,6 +365,57 @@ object APIHolder { } } +/* +// THIS IS WORK IN PROGRESS API +interface ITag { + val name: UiText +} + +data class SimpleTag(override val name: UiText, val data: String) : ITag + +enum class SelectType { + SingleSelect, + MultiSelect, + MultiSelectAndExclude, +} + +enum class SelectValue { + Selected, + Excluded, +} + +interface GenreSelector { + val title: UiText + val id : Int +} + +data class TagSelector( + override val title: UiText, + override val id : Int, + val tags: Set, + val defaultTags : Set = setOf(), + val selectType: SelectType = SelectType.SingleSelect, +) : GenreSelector + +data class BoolSelector( + override val title: UiText, + override val id : Int, + + val defaultValue : Boolean = false, +) : GenreSelector + +data class InputField( + override val title: UiText, + override val id : Int, + + val hint : UiText? = null, +) : GenreSelector + +// This response describes how a user might filter the homepage or search results +data class GenreResponse( + val searchSelectors : List, + val filterSelectors: List = searchSelectors +) */ /* 0 = Site not good @@ -459,6 +557,20 @@ abstract class MainAPI { open val hasMainPage = false open val hasQuickSearch = false + /** + * A set of which ids the provider can open with getLoadUrl() + * If the set contains SyncIdName.Imdb then getLoadUrl() can be started with + * an Imdb class which inherits from SyncId. + * + * getLoadUrl() is then used to get page url based on that ID. + * + * Example: + * "tt6723592" -> getLoadUrl(ImdbSyncId("tt6723592")) -> "mainUrl/imdb/tt6723592" -> load("mainUrl/imdb/tt6723592") + * + * This is used to launch pages from personal lists or recommendations using IDs. + **/ + open val supportedSyncNames = setOf() + open val supportedTypes = setOf( TvType.Movie, TvType.TvSeries, @@ -529,6 +641,14 @@ abstract class MainAPI { open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? { return null } + + /** + * Get the load() url based on a sync ID like IMDb or MAL. + * Only contains SyncIds based on supportedSyncUrls. + **/ + open suspend fun getLoadUrl(name: SyncIdName, id: String): String? { + return null + } } /** Might need a different implementation for desktop*/ @@ -1514,3 +1634,61 @@ fun fetchUrls(text: String?): List { fun String?.toRatingInt(): Int? = this?.replace(" ", "")?.trim()?.toDoubleOrNull()?.absoluteValue?.times(1000f)?.toInt() + +data class Tracker( + val malId: Int? = null, + val aniId: String? = null, + val image: String? = null, + val cover: String? = null, +) + +data class Title( + @JsonProperty("romaji") val romaji: String? = null, + @JsonProperty("english") val english: String? = null, +) { + fun isMatchingTitles(title: String?): Boolean { + if (title == null) return false + return english.equals(title, true) || romaji.equals(title, true) + } +} + +data class Results( + @JsonProperty("id") val aniId: String? = null, + @JsonProperty("malId") val malId: Int? = null, + @JsonProperty("title") val title: Title? = null, + @JsonProperty("releaseDate") val releaseDate: Int? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("image") val image: String? = null, + @JsonProperty("cover") val cover: String? = null, +) + +data class AniSearch( + @JsonProperty("results") val results: ArrayList? = arrayListOf() +) + +/** + * used for the getTracker() method + **/ +enum class TrackerType { + MOVIE, + TV, + TV_SHORT, + ONA, + OVA, + SPECIAL, + MUSIC; + + companion object { + fun getTypes(type: TvType): Set { + return when (type) { + TvType.Movie -> setOf(MOVIE) + TvType.AnimeMovie -> setOf(MOVIE) + TvType.TvSeries -> setOf(TV, TV_SHORT) + TvType.Anime -> setOf(TV, TV_SHORT, ONA, OVA) + TvType.OVA -> setOf(OVA, SPECIAL, ONA) + TvType.Others -> setOf(MUSIC) + else -> emptySet() + } + } + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 4907411f..eddec15e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1,16 +1,14 @@ package com.lagradost.cloudstream3 import android.content.ComponentName -import android.content.DialogInterface +import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.content.res.Configuration import android.os.Bundle +import android.util.AttributeSet import android.util.Log -import android.view.KeyEvent -import android.view.Menu -import android.view.MenuItem -import android.view.WindowManager +import android.view.* import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.annotation.IdRes @@ -19,6 +17,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.ViewModelProvider import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy @@ -31,6 +30,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper 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.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.lagradost.cloudstream3.APIHolder.allProviders @@ -46,8 +46,7 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent 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.mvvm.* import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins @@ -61,9 +60,13 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStri 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.WatchType import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.home.HomeViewModel +import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST +import com.lagradost.cloudstream3.ui.result.setImage +import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.search.SearchFragment import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings @@ -74,6 +77,7 @@ import com.lagradost.cloudstream3.ui.settings.SettingsGeneral import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY 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.loadCache import com.lagradost.cloudstream3.utils.AppUtils.loadRepository @@ -86,9 +90,11 @@ import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate @@ -96,6 +102,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.android.synthetic.main.bottom_resultview_preview.* import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -244,6 +251,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { Event() // homepage api, used to speed up time to load for homepage val afterRepositoryLoadedEvent = Event() + // kinda shitty solution, but cant com main->home otherwise for popups + val bookmarksUpdatedEvent = Event() + + /** * @return true if the str has launched an app task (be it successful or not) * @param isWebview does not handle providers and opening download page if true. Can still add repos and login. @@ -336,6 +347,16 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } + var lastPopup: SearchResponse? = null + fun loadPopup(result: SearchResponse) { + lastPopup = result + viewModel.load( + this, result.url, result.apiName, false, if (getApiDubstatusSettings() + .contains(DubStatus.Dubbed) + ) DubStatus.Dubbed else DubStatus.Subbed, null + ) + } + override fun onColorSelected(dialogId: Int, color: Int) { onColorSelectedEvent.invoke(Pair(dialogId, color)) } @@ -367,6 +388,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val isNavVisible = listOf( R.id.navigation_home, R.id.navigation_search, + R.id.navigation_library, R.id.navigation_downloads, R.id.navigation_settings, R.id.navigation_download_child, @@ -417,6 +439,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { nav_view?.isVisible = isNavVisible && !landscape nav_rail_view?.isVisible = isNavVisible && landscape + + // Hide library on TV since it is not supported yet :( + val isTrueTv = isTrueTvSettings() + nav_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + nav_rail_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv } //private var mCastSession: CastSession? = null @@ -619,6 +646,37 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } + lateinit var viewModel: ResultViewModel2 + + override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { + viewModel = + ViewModelProvider(this)[ResultViewModel2::class.java] + + return super.onCreateView(name, context, attrs) + } + + private fun hidePreviewPopupDialog() { + viewModel.clear() + bottomPreviewPopup.dismissSafe(this) + } + + var bottomPreviewPopup: BottomSheetDialog? = null + private fun showPreviewPopupDialog(): BottomSheetDialog { + val ret = (bottomPreviewPopup ?: run { + val builder = + BottomSheetDialog(this) + builder.setContentView(R.layout.bottom_resultview_preview) + builder.setOnDismissListener { + bottomPreviewPopup = null + viewModel.clear() + } + builder.setCanceledOnTouchOutside(true) + builder.show() + builder + }) + bottomPreviewPopup = ret + return ret + } override fun onCreate(savedInstanceState: Bundle?) { app.initClient(this) @@ -658,7 +716,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { changeStatusBarState(isEmulatorSettings()) - if (lastError == null) { + + if (PluginManager.checkSafeModeFile()) { + normalSafeApiCall { + showToast(this, R.string.safe_mode_file, Toast.LENGTH_LONG) + } + } else if (lastError == null) { ioSafe { getKey(USER_SELECTED_HOMEPAGE_API)?.let { homeApi -> mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi)) @@ -708,6 +771,78 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { builder.show().setDefaultFocus() } + observeNullable(viewModel.page) { resource -> + if (resource == null) { + bottomPreviewPopup.dismissSafe(this) + return@observeNullable + } + when (resource) { + is Resource.Failure -> { + showToast(this, R.string.error) + hidePreviewPopupDialog() + } + is Resource.Loading -> { + showPreviewPopupDialog().apply { + resultview_preview_loading?.isVisible = true + resultview_preview_result?.isVisible = false + resultview_preview_loading_shimmer?.startShimmer() + } + } + is Resource.Success -> { + val d = resource.value + showPreviewPopupDialog().apply { + resultview_preview_loading?.isVisible = false + resultview_preview_result?.isVisible = true + resultview_preview_loading_shimmer?.stopShimmer() + + resultview_preview_title?.text = d.title + + resultview_preview_meta_type.setText(d.typeText) + resultview_preview_meta_year.setText(d.yearText) + resultview_preview_meta_duration.setText(d.durationText) + resultview_preview_meta_rating.setText(d.ratingText) + + resultview_preview_description?.setText(d.plotText) + resultview_preview_poster?.setImage( + d.posterImage ?: d.posterBackgroundImage + ) + + resultview_preview_poster?.setOnClickListener { + //viewModel.updateWatchStatus(WatchType.PLANTOWATCH) + val value = viewModel.watchStatus.value ?: WatchType.NONE + + this@MainActivity.showBottomDialog( + WatchType.values().map { getString(it.stringRes) }.toList(), + value.ordinal, + this@MainActivity.getString(R.string.action_add_to_bookmarks), + showApply = false, + {}) { + viewModel.updateWatchStatus(WatchType.values()[it]) + bookmarksUpdatedEvent(true) + } + } + + if (!isTvSettings()) // dont want this clickable on tv layout + resultview_preview_description?.setOnClickListener { view -> + view.context?.let { ctx -> + val builder: AlertDialog.Builder = + AlertDialog.Builder(ctx, R.style.AlertDialogCustom) + builder.setMessage(d.plotText.asString(ctx).html()) + .setTitle(d.plotHeaderText.asString(ctx)) + .show() + } + } + + resultview_preview_more_info?.setOnClickListener { + hidePreviewPopupDialog() + lastPopup?.let { + loadSearchResult(it) + } + } + } + } + } + } // ioSafe { // val plugins = diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt new file mode 100644 index 00000000..3e0a03c0 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt @@ -0,0 +1,23 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.utils.* + +open class ByteShare : ExtractorApi() { + override val name = "ByteShare" + override val mainUrl = "https://byteshare.net" + override val requiresReferer = false + + override suspend fun getUrl(url: String, referer: String?): List { + val sources = mutableListOf() + sources.add( + ExtractorLink( + name, + name, + url.replace("/embed/", "/download/"), + "", + Qualities.Unknown.value, + ) + ) + return sources + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt index 8e3dc730..bc910a7e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt @@ -1,32 +1,51 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import java.net.URI + +class FileMoon : Filesim() { + override val mainUrl = "https://filemoon.to" + override val name = "FileMoon" +} open class Filesim : ExtractorApi() { override val name = "Filesim" override val mainUrl = "https://files.im" override val requiresReferer = false - override suspend fun getUrl(url: String, referer: String?): List { - val sources = mutableListOf() + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { with(app.get(url).document) { - this.select("script").map { script -> + this.select("script").forEach { script -> if (script.data().contains("eval(function(p,a,c,k,e,d)")) { - val data = getAndUnpack(script.data()).substringAfter("sources:[").substringBefore("]") - tryParseJson>("[$data]")?.map { - M3u8Helper.generateM3u8( - name, - it.file, - "$mainUrl/", - ).forEach { m3uData -> sources.add(m3uData) } + val data = getAndUnpack(script.data()) + val foundData = Regex("""sources:\[(.*?)]""").find(data)?.groupValues?.get(1) ?: return@forEach + val fixedData = foundData.replace("file:", """"file":""") + + parseJson>("[$fixedData]").forEach { + callback.invoke( + ExtractorLink( + name, + name, + it.file, + "$mainUrl/", + Qualities.Unknown.value, + URI(it.file).path.endsWith(".m3u8") + ) + ) } } } } - return sources } private data class ResponseSource( diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt index f25cb5ba..2adc00d5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt @@ -6,6 +6,11 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* +class Vanfem : GuardareStream() { + override var name = "Vanfem" + override var mainUrl = "https://vanfem.com/" +} + class CineGrabber : GuardareStream() { override var name = "CineGrabber" override var mainUrl = "https://cinegrabber.com" diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt index b910f9dd..a27bf188 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt @@ -59,8 +59,8 @@ open class VidSrcExtractor : ExtractorApi() { if (datahash.isNotBlank()) { val links = try { app.get( - "$absoluteUrl/src/$datahash", - referer = "https://source.vidsrc.me/" + "$absoluteUrl/srcrcp/$datahash", + referer = "https://rcp.vidsrc.me/" ).url } catch (e: Exception) { "" @@ -71,7 +71,7 @@ open class VidSrcExtractor : ExtractorApi() { serverslist.amap { server -> val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/") - if (linkfixed.contains("/pro")) { + if (linkfixed.contains("/prorcp")) { val srcresponse = app.get(server, referer = absoluteUrl).text val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)") val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/AnilistRedirector.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/AnilistRedirector.kt deleted file mode 100644 index 208db14b..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/AnilistRedirector.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.lagradost.cloudstream3.metaproviders - -import com.lagradost.cloudstream3.ErrorLoadingException -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi -import com.lagradost.cloudstream3.utils.SyncUtil - -object SyncRedirector { - val syncApis = SyncApis - - suspend fun redirect(url: String, preferredUrl: String): String { - for (api in syncApis) { - if (url.contains(api.mainUrl)) { - val otherApi = when (api.name) { - aniListApi.name -> "anilist" - malApi.name -> "myanimelist" - else -> return url - } - - return SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl -> - realUrl.contains(preferredUrl) - } ?: run { - throw ErrorLoadingException("Page does not exist on $preferredUrl") - } - } - } - return url - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt new file mode 100644 index 00000000..75e96bec --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt @@ -0,0 +1,56 @@ +package com.lagradost.cloudstream3.metaproviders + +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis +import com.lagradost.cloudstream3.syncproviders.SyncIdName + +object SyncRedirector { + val syncApis = SyncApis + private val syncIds = + listOf( + SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""), + SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""") + ) + + suspend fun redirect( + url: String, + providerApi: MainAPI + ): String { + // Deprecated since providers should do this instead! + + // Tries built in ID -> ProviderUrl + /* + for (api in syncApis) { + if (url.contains(api.mainUrl)) { + val otherApi = when (api.name) { + aniListApi.name -> "anilist" + malApi.name -> "myanimelist" + else -> return url + } + + SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl -> + realUrl.contains(providerApi.mainUrl) + }?.let { + return it + } +// ?: run { +// throw ErrorLoadingException("Page does not exist on $preferredUrl") +// } + } + } + */ + + // Tries provider solution + // This goes through all sync ids and finds supported id by said provider + return syncIds.firstNotNullOfOrNull { (syncName, syncRegex) -> + if (providerApi.supportedSyncNames.contains(syncName)) { + syncRegex.find(url)?.value?.let { + suspendSafeApiCall { + providerApi.getLoadUrl(syncName, it) + } + } + } else null + } ?: url + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt index 068cb968..6950d961 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt @@ -64,7 +64,9 @@ class CloudflareKiller : Interceptor { } private fun getWebViewCookie(url: String): String? { - return CookieManager.getInstance()?.getCookie(url) + return normalSafeApiCall { + CookieManager.getInstance()?.getCookie(url) + } } /** 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 54fe5d75..3533d6a8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -144,8 +144,10 @@ object PluginManager { return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray() } - private val LOCAL_PLUGINS_PATH = - Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/plugins" + private val CLOUD_STREAM_FOLDER = + Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/" + + private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins" public var currentlyLoading: String? = null @@ -421,6 +423,21 @@ object PluginManager { afterPluginsLoadedEvent.invoke(forceReload) } + /** + * This can be used to override any extension loading to fix crashes! + * @return true if safe mode file is present + **/ + fun checkSafeModeFile(): Boolean { + return normalSafeApiCall { + val folder = File(CLOUD_STREAM_FOLDER) + if (!folder.exists()) return@normalSafeApiCall false + val files = folder.listFiles { _, name -> + name.equals("safe", ignoreCase = true) + } + files?.any() + } ?: false + } + /** * @return True if successful, false if not * */ 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 f09bf8fe..f17086c1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -13,6 +13,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val openSubtitlesApi = OpenSubtitlesApi(0) val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() + val localListApi = LocalList() // used to login via app intent val OAuth2Apis @@ -29,7 +30,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { // used for active syncing val SyncApis get() = listOf( - SyncRepo(malApi), SyncRepo(aniListApi) + SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi) ) val inAppAuths diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index 5aa56a02..8c76c5bf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -1,10 +1,31 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.ui.library.ListSorting +import com.lagradost.cloudstream3.ui.result.UiText +import me.xdrop.fuzzywuzzy.FuzzySearch + +enum class SyncIdName { + Anilist, + MyAnimeList, + Trakt, + Imdb, + LocalList +} interface SyncAPI : OAuth2API { + /** + * Set this to true if the user updates something on the list like watch status or score + **/ + var requireLibraryRefresh: Boolean val mainUrl: String + /** + * Allows certain providers to open pages from + * library links. + **/ + val syncIdName: SyncIdName + /** -1 -> None 0 -> Watching @@ -22,7 +43,9 @@ interface SyncAPI : OAuth2API { suspend fun search(name: String): List? - fun getIdFromUrl(url : String) : String + suspend fun getPersonalLibrary(): LibraryMetadata? + + fun getIdFromUrl(url: String): String data class SyncSearchResult( override val name: String, @@ -42,7 +65,7 @@ interface SyncAPI : OAuth2API { val score: Int?, val watchedEpisodes: Int?, var isFavorite: Boolean? = null, - var maxEpisodes : Int? = null, + var maxEpisodes: Int? = null, ) data class SyncResult( @@ -63,9 +86,9 @@ interface SyncAPI : OAuth2API { var genres: List? = null, var synonyms: List? = null, var trailers: List? = null, - var isAdult : Boolean? = null, + var isAdult: Boolean? = null, var posterUrl: String? = null, - var backgroundPosterUrl : String? = null, + var backgroundPosterUrl: String? = null, /** In unixtime */ var startDate: Long? = null, @@ -76,4 +99,61 @@ interface SyncAPI : OAuth2API { var prevSeason: SyncSearchResult? = null, var actors: List? = null, ) + + + data class Page( + val title: UiText, var items: List + ) { + fun sort(method: ListSorting?, query: String? = null) { + items = when (method) { + ListSorting.Query -> + if (query != null) { + items.sortedBy { + -FuzzySearch.partialRatio( + query.lowercase(), it.name.lowercase() + ) + } + } else items + ListSorting.RatingHigh -> items.sortedBy { -(it.personalRating ?: 0) } + ListSorting.RatingLow -> items.sortedBy { (it.personalRating ?: 0) } + ListSorting.AlphabeticalA -> items.sortedBy { it.name } + ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed() + ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) } + ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime } + else -> items + } + } + } + + data class LibraryMetadata( + val allLibraryLists: List, + val supportedListSorting: Set + ) + + data class LibraryList( + val name: UiText, + val items: List + ) + + data class LibraryItem( + override val name: String, + override val url: String, + /** + * Unique unchanging string used for data storage. + * This should be the actual id when you change scores and status + * since score changes from library might get added in the future. + **/ + val syncId: String, + val episodesCompleted: Int?, + val episodesTotal: Int?, + /** Out of 100 */ + val personalRating: Int?, + val lastUpdatedUnixTime: Long?, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override var posterHeaders: Map?, + override var quality: SearchQuality?, + override var id: Int? = null, + ) : SearchResponse } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt index b621e81a..85b877e0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt @@ -11,26 +11,38 @@ class SyncRepo(private val repo: SyncAPI) { val icon = repo.icon val mainUrl = repo.mainUrl val requiresLogin = repo.requiresLogin + val syncIdName = repo.syncIdName + var requireLibraryRefresh: Boolean + get() = repo.requireLibraryRefresh + set(value) { + repo.requireLibraryRefresh = value + } suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource { return safeApiCall { repo.score(id, status) } } - suspend fun getStatus(id : String) : Resource { + suspend fun getStatus(id: String): Resource { return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") } } - suspend fun getResult(id : String) : Resource { + suspend fun getResult(id: String): Resource { return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") } } - suspend fun search(query : String) : Resource> { + suspend fun search(query: String): Resource> { return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() } } - fun hasAccount() : Boolean { + suspend fun getPersonalLibrary(): Resource { + return safeApiCall { repo.getPersonalLibrary() ?: throw ErrorLoadingException() } + } + + fun hasAccount(): Boolean { return normalSafeApiCall { repo.loginInfo() != null } ?: false } - fun getIdFromUrl(url : String) : String = repo.getIdFromUrl(url) + fun getIdFromUrl(url: String): String? = normalSafeApiCall { + repo.getIdFromUrl(url) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index d4742d94..7d9de43a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -1,10 +1,10 @@ package com.lagradost.cloudstream3.syncproviders.providers +import androidx.annotation.StringRes import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.mvvm.logError @@ -12,6 +12,9 @@ import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.ui.library.ListSorting +import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.toJson @@ -27,10 +30,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override val key = "6871" override val redirectUrl = "anilistlogin" override val idPrefix = "anilist" + override var requireLibraryRefresh = true override var mainUrl = "https://anilist.co" override val icon = R.drawable.ic_anilist_icon override val requiresLogin = false override val createAccountUrl = "$mainUrl/signup" + override val syncIdName = SyncIdName.Anilist override fun loginInfo(): AuthAPI.LoginInfo? { // context.getUser(true)?. @@ -45,6 +50,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { } override fun logOut() { + requireLibraryRefresh = true removeAccountKeys() } @@ -64,8 +70,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { switchToNewAccount() setKey(accountId, ANILIST_UNIXTIME_KEY, endTime) setKey(accountId, ANILIST_TOKEN_KEY, token) - setKey(ANILIST_SHOULD_UPDATE_LIST, true) val user = getUser() + requireLibraryRefresh = true return user != null } @@ -140,7 +146,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { this.name, recMedia.id?.toString() ?: return@mapNotNull null, getUrlFromId(recMedia.id), - recMedia.coverImage?.large ?: recMedia.coverImage?.medium + recMedia.coverImage?.extraLarge ?: recMedia.coverImage?.large + ?: recMedia.coverImage?.medium ) }, trailers = when (season.trailer?.site?.lowercase()?.trim()) { @@ -170,7 +177,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { fromIntToAnimeStatus(status.status), status.score, status.watchedEpisodes - ) + ).also { + requireLibraryRefresh = requireLibraryRefresh || it + } } companion object { @@ -181,7 +190,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { const val ANILIST_TOKEN_KEY: String = "anilist_token" // anilist token for api const val ANILIST_USER_KEY: String = "anilist_user" // user data like profile const val ANILIST_CACHED_LIST: String = "anilist_cached_list" - const val ANILIST_SHOULD_UPDATE_LIST: String = "anilist_should_update_list" private fun fixName(name: String): String { return name.lowercase(Locale.ROOT).replace(" ", "") @@ -219,7 +227,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { romaji } idMal - coverImage { medium large } + coverImage { medium large extraLarge } averageScore } } @@ -232,7 +240,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { format id idMal - coverImage { medium large } + coverImage { medium large extraLarge } averageScore title { english @@ -292,15 +300,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { val shows = searchShows(name.replace(blackListRegex, "")) shows?.data?.Page?.media?.find { - malId ?: "NONE" == it.idMal.toString() + (malId ?: "NONE") == it.idMal.toString() }?.let { return it } val filtered = shows?.data?.Page?.media?.filter { - ( - it.startDate.year ?: year.toString() == year.toString() - || year == null - ) + (((it.startDate.year ?: year.toString()) == year.toString() + || year == null)) } filtered?.forEach { it.title.romaji?.let { romaji -> @@ -312,14 +318,14 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { } // Changing names of these will show up in UI - enum class AniListStatusType(var value: Int) { - Watching(0), - Completed(1), - Paused(2), - Dropped(3), - Planning(4), - ReWatching(5), - None(-1) + enum class AniListStatusType(var value: Int, @StringRes val stringRes: Int) { + Watching(0, R.string.type_watching), + Completed(1, R.string.type_completed), + Paused(2, R.string.type_on_hold), + Dropped(3, R.string.type_dropped), + Planning(4, R.string.type_plan_to_watch), + ReWatching(5, R.string.type_re_watching), + None(-1, R.string.none) } fun fromIntToAnimeStatus(inp: Int): AniListStatusType {//= AniListStatusType.values().first { it.value == inp } @@ -335,7 +341,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { } } - fun convertAnilistStringToStatus(string: String): AniListStatusType { + fun convertAniListStringToStatus(string: String): AniListStatusType { return fromIntToAnimeStatus(aniListStatusString.indexOf(string)) } @@ -526,7 +532,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { app.post( "https://graphql.anilist.co/", headers = mapOf( - "Authorization" to "Bearer " + (getAuth() ?: return@suspendSafeApiCall null), + "Authorization" to "Bearer " + (getAuth() + ?: return@suspendSafeApiCall null), if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" ), cacheTime = 0, @@ -575,7 +582,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { data class CoverImage( @JsonProperty("medium") val medium: String?, - @JsonProperty("large") val large: String? + @JsonProperty("large") val large: String?, + @JsonProperty("extraLarge") val extraLarge: String? ) data class Media( @@ -602,7 +610,29 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("score") val score: Int, @JsonProperty("private") val private: Boolean, @JsonProperty("media") val media: Media - ) + ) { + fun toLibraryItem(): SyncAPI.LibraryItem { + return SyncAPI.LibraryItem( + // English title first + this.media.title.english ?: this.media.title.romaji + ?: this.media.synonyms.firstOrNull() + ?: "", + "https://anilist.co/anime/${this.media.id}/", + this.media.id.toString(), + this.progress, + this.media.episodes, + this.score, + this.updatedAt.toLong(), + "AniList", + TvType.Anime, + this.media.coverImage.extraLarge ?: this.media.coverImage.large + ?: this.media.coverImage.medium, + null, + null, + null + ) + } + } data class Lists( @JsonProperty("status") val status: String?, @@ -617,40 +647,59 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection ) - fun getAnilistListCached(): Array? { + private fun getAniListListCached(): Array? { return getKey(ANILIST_CACHED_LIST) as? Array } - suspend fun getAnilistAnimeListSmart(): Array? { + private suspend fun getAniListAnimeListSmart(): Array? { if (getAuth() == null) return null if (checkToken()) return null - return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) { - val list = getFullAnilistList()?.data?.MediaListCollection?.lists?.toTypedArray() + return if (requireLibraryRefresh) { + val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray() if (list != null) { setKey(ANILIST_CACHED_LIST, list) - setKey(ANILIST_SHOULD_UPDATE_LIST, false) } list } else { - getAnilistListCached() + getAniListListCached() } } - private suspend fun getFullAnilistList(): FullAnilistList? { - var userID: Int? = null - /** WARNING ASSUMES ONE USER! **/ - getKeys(ANILIST_USER_KEY)?.forEach { key -> - getKey(key, null)?.let { - userID = it.id - } - } + override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata { + val list = getAniListAnimeListSmart()?.groupBy { + convertAniListStringToStatus(it.status ?: "").stringRes + }?.mapValues { group -> + group.value.map { it.entries.map { entry -> entry.toLibraryItem() } }.flatten() + } ?: emptyMap() - val fixedUserID = userID ?: return null + // To fill empty lists when AniList does not return them + val baseMap = + AniListStatusType.values().filter { it.value >= 0 }.associate { + it.stringRes to emptyList() + } + + return SyncAPI.LibraryMetadata( + (baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) }, + setOf( + ListSorting.AlphabeticalA, + ListSorting.AlphabeticalZ, + ListSorting.UpdatedNew, + ListSorting.UpdatedOld, + ListSorting.RatingHigh, + ListSorting.RatingLow, + ) + ) + } + + private suspend fun getFullAniListList(): FullAnilistList? { + /** WARNING ASSUMES ONE USER! **/ + + val userID = getKey(accountId, ANILIST_USER_KEY)?.id ?: return null val mediaType = "ANIME" val query = """ - query (${'$'}userID: Int = $fixedUserID, ${'$'}MEDIA: MediaType = $mediaType) { + query (${'$'}userID: Int = $userID, ${'$'}MEDIA: MediaType = $mediaType) { MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) { lists { status @@ -661,7 +710,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { startedAt { year month day } updatedAt progress - score + score (format: POINT_100) private media { @@ -677,7 +726,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { english romaji } - coverImage { medium } + coverImage { extraLarge large medium } synonyms nextAiringEpisode { timeUntilAiring diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt new file mode 100644 index 00000000..0b081220 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -0,0 +1,100 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import androidx.fragment.app.FragmentActivity +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.syncproviders.AuthAPI +import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.syncproviders.SyncIdName +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.getAllWatchStateIds +import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState + +class LocalList : SyncAPI { + override val name = "Local" + override val icon: Int = R.drawable.ic_baseline_storage_24 + override val requiresLogin = false + override val createAccountUrl: Nothing? = null + override val idPrefix = "local" + override var requireLibraryRefresh = true + + override fun loginInfo(): AuthAPI.LoginInfo { + return AuthAPI.LoginInfo( + null, + null, + 0 + ) + } + + override fun logOut() { + + } + + override val key: String = "" + override val redirectUrl = "" + override suspend fun handleRedirect(url: String): Boolean { + return true + } + + override fun authenticate(activity: FragmentActivity?) { + } + + override val mainUrl = "" + override val syncIdName = SyncIdName.LocalList + override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { + return true + } + + override suspend fun getStatus(id: String): SyncAPI.SyncStatus? { + return null + } + + override suspend fun getResult(id: String): SyncAPI.SyncResult? { + return null + } + + override suspend fun search(name: String): List? { + return null + } + + override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata? { + val watchStatusIds = ioWork { + getAllWatchStateIds()?.map { id -> + Pair(id, getResultWatchState(id)) + } + }?.distinctBy { it.first } ?: return null + + val list = ioWork { + watchStatusIds.groupBy { + it.second.stringRes + }.mapValues { group -> + group.value.mapNotNull { + getBookmarkedData(it.first)?.toLibraryItem(it.first.toString()) + } + } + } + + val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate { + // None is not something to display + it.stringRes to emptyList() + } + return SyncAPI.LibraryMetadata( + (baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) }, + setOf( + ListSorting.AlphabeticalA, + ListSorting.AlphabeticalZ, +// ListSorting.UpdatedNew, +// ListSorting.UpdatedOld, +// ListSorting.RatingHigh, +// ListSorting.RatingLow, + ) + ) + } + + override fun getIdFromUrl(url: String): String { + return url + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index c08958ce..5164b606 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.syncproviders.providers import android.util.Base64 +import androidx.annotation.StringRes import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -8,11 +9,15 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ShowStatus +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.ui.library.ListSorting +import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject @@ -31,13 +36,15 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override val redirectUrl = "mallogin" override val idPrefix = "mal" override var mainUrl = "https://myanimelist.net" - val apiUrl = "https://api.myanimelist.net" + private val apiUrl = "https://api.myanimelist.net" override val icon = R.drawable.mal_logo override val requiresLogin = false - + override val syncIdName = SyncIdName.MyAnimeList + override var requireLibraryRefresh = true override val createAccountUrl = "$mainUrl/register.php" override fun logOut() { + requireLibraryRefresh = true removeAccountKeys() } @@ -90,7 +97,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { fromIntToAnimeStatus(status.status), status.score, status.watchedEpisodes - ) + ).also { + requireLibraryRefresh = requireLibraryRefresh || it + } } data class MalAnime( @@ -248,10 +257,45 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { const val MAL_USER_KEY: String = "mal_user" // user data like profile const val MAL_CACHED_LIST: String = "mal_cached_list" - const val MAL_SHOULD_UPDATE_LIST: String = "mal_should_update_list" const val MAL_UNIXTIME_KEY: String = "mal_unixtime" // When token expires const val MAL_REFRESH_TOKEN_KEY: String = "mal_refresh_token" // refresh token const val MAL_TOKEN_KEY: String = "mal_token" // anilist token for api + + fun convertToStatus(string: String): MalStatusType { + return fromIntToAnimeStatus(malStatusAsString.indexOf(string)) + } + + enum class MalStatusType(var value: Int, @StringRes val stringRes: Int) { + Watching(0, R.string.type_watching), + Completed(1, R.string.type_completed), + OnHold(2, R.string.type_on_hold), + Dropped(3, R.string.type_dropped), + PlanToWatch(4, R.string.type_plan_to_watch), + None(-1, R.string.type_none) + } + + private fun fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp } + return when (inp) { + -1 -> MalStatusType.None + 0 -> MalStatusType.Watching + 1 -> MalStatusType.Completed + 2 -> MalStatusType.OnHold + 3 -> MalStatusType.Dropped + 4 -> MalStatusType.PlanToWatch + 5 -> MalStatusType.Watching + else -> MalStatusType.None + } + } + + private fun parseDateLong(string: String?): Long? { + return try { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse( + string ?: return null + )?.time?.div(1000) + } catch (e: Exception) { + null + } + } } override suspend fun handleRedirect(url: String): Boolean { @@ -275,7 +319,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { switchToNewAccount() storeToken(res) val user = getMalUser() - setKey(MAL_SHOULD_UPDATE_LIST, true) + requireLibraryRefresh = true return user != null } } @@ -308,9 +352,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime)) setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token) setKey(accountId, MAL_TOKEN_KEY, token.access_token) + requireLibraryRefresh = true } } catch (e: Exception) { - e.printStackTrace() + logError(e) } } @@ -329,7 +374,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ).text storeToken(res) } catch (e: Exception) { - e.printStackTrace() + logError(e) } } @@ -382,7 +427,24 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class Data( @JsonProperty("node") val node: Node, @JsonProperty("list_status") val list_status: ListStatus?, - ) + ) { + fun toLibraryItem(): SyncAPI.LibraryItem { + return SyncAPI.LibraryItem( + this.node.title, + "https://myanimelist.net/anime/${this.node.id}/", + this.node.id.toString(), + this.list_status?.num_episodes_watched, + this.node.num_episodes, + this.list_status?.score?.times(10), + parseDateLong(this.list_status?.updated_at), + "MAL", + TvType.Anime, + this.node.main_picture?.large ?: this.node.main_picture?.medium, + null, + null, + ) + } + } data class Paging( @JsonProperty("next") val next: String? @@ -413,18 +475,43 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { return getKey(MAL_CACHED_LIST) as? Array } - suspend fun getMalAnimeListSmart(): Array? { + private suspend fun getMalAnimeListSmart(): Array? { if (getAuth() == null) return null - return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) { + return if (requireLibraryRefresh) { val list = getMalAnimeList() setKey(MAL_CACHED_LIST, list) - setKey(MAL_SHOULD_UPDATE_LIST, false) list } else { getMalAnimeListCached() } } + override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata { + val list = getMalAnimeListSmart()?.groupBy { + convertToStatus(it.list_status?.status ?: "").stringRes + }?.mapValues { group -> + group.value.map { it.toLibraryItem() } + } ?: emptyMap() + + // To fill empty lists when MAL does not return them + val baseMap = + MalStatusType.values().filter { it.value >= 0 }.associate { + it.stringRes to emptyList() + } + + return SyncAPI.LibraryMetadata( + (baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) }, + setOf( + ListSorting.AlphabeticalA, + ListSorting.AlphabeticalZ, + ListSorting.UpdatedNew, + ListSorting.UpdatedOld, + ListSorting.RatingHigh, + ListSorting.RatingLow, + ) + ) + } + private suspend fun getMalAnimeList(): Array { checkMalToken() var offset = 0 @@ -440,10 +527,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { return fullList.toTypedArray() } - fun convertToStatus(string: String): MalStatusType { - return fromIntToAnimeStatus(malStatusAsString.indexOf(string)) - } - private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? { val user = "@me" val auth = getAuth() ?: return null @@ -557,28 +640,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { return user } - enum class MalStatusType(var value: Int) { - Watching(0), - Completed(1), - OnHold(2), - Dropped(3), - PlanToWatch(4), - None(-1) - } - - private fun fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp } - return when (inp) { - -1 -> MalStatusType.None - 0 -> MalStatusType.Watching - 1 -> MalStatusType.Completed - 2 -> MalStatusType.OnHold - 3 -> MalStatusType.Dropped - 4 -> MalStatusType.PlanToWatch - 5 -> MalStatusType.Watching - else -> MalStatusType.None - } - } - private suspend fun setScoreRequest( id: Int, status: MalStatusType? = null, 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 8f06c98e..3e372c2d 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 @@ -166,7 +166,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val fixedLang = fixLanguage(query.lang) val imdbId = query.imdb ?: 0 - val queryText = query.query.replace(" ", "+") + val queryText = query.query val epNum = query.epNumber ?: 0 val seasonNum = query.seasonNumber ?: 0 val yearNum = query.year ?: 0 @@ -177,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=${URLEncoder.encode(queryText.lowercase(), StandardCharsets.UTF_8.toString())}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" + false -> "$host/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" } val req = app.get( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt index 138084fc..b4c07792 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt @@ -7,7 +7,8 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs -class GrdLayoutManager(val context: Context, _spanCount: Int) : GridLayoutManager(context, _spanCount) { +class GrdLayoutManager(val context: Context, _spanCount: Int) : + GridLayoutManager(context, _spanCount) { override fun onFocusSearchFailed( focused: View, focusDirection: Int, @@ -34,7 +35,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : GridLayoutManage val pos = maxOf(0, getPosition(focused!!) - 2) parent.scrollToPosition(pos) super.onRequestChildFocus(parent, state, child, focused) - } catch (e: Exception){ + } catch (e: Exception) { false } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index bb183f12..5cf6fc8e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -32,6 +32,7 @@ import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent +import com.lagradost.cloudstream3.MainActivity.Companion.bookmarksUpdatedEvent import com.lagradost.cloudstream3.MainActivity.Companion.mainPluginsLoadedEvent import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError @@ -435,6 +436,11 @@ class HomeFragment : Fragment() { return inflater.inflate(layout, container, false) } + override fun onDestroyView() { + bottomSheetDialog?.ownHide() + super.onDestroyView() + } + private fun fixGrid() { activity?.getSpanCount()?.let { currentSpan = it @@ -461,14 +467,20 @@ class HomeFragment : Fragment() { fixGrid() } + fun bookmarksUpdated(_data : Boolean) { + reloadStored() + } + override fun onResume() { super.onResume() reloadStored() + bookmarksUpdatedEvent += ::bookmarksUpdated afterPluginsLoadedEvent += ::afterPluginsLoaded mainPluginsLoadedEvent += ::afterMainPluginsLoaded } override fun onStop() { + bookmarksUpdatedEvent -= ::bookmarksUpdated afterPluginsLoadedEvent -= ::afterPluginsLoaded mainPluginsLoadedEvent -= ::afterMainPluginsLoaded super.onStop() @@ -557,7 +569,7 @@ class HomeFragment : Fragment() { val mutableListOfResponse = mutableListOf() listHomepageItems.clear() - (home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList( + (home_master_recycler?.adapter as? ParentItemAdapter)?.updateList( d.values.toMutableList(), home_master_recycler ) @@ -609,7 +621,7 @@ class HomeFragment : Fragment() { //home_loaded?.isVisible = false } is Resource.Loading -> { - (home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList(listOf()) + (home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(listOf()) home_loading_shimmer?.startShimmer() home_loading?.isVisible = true home_loading_error?.isVisible = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index 8b7832e2..e6999c9e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -184,9 +184,8 @@ open class ParentItemAdapter( private val expandCallback: ((String) -> Unit)? = null, ) : RecyclerView.ViewHolder(itemView) { - val title: TextView = itemView.home_parent_item_title + val title: TextView = itemView.home_child_more_info val recyclerView: RecyclerView = itemView.home_child_recyclerview - private val moreInfo: FrameLayout? = itemView.home_child_more_info fun update(expand: HomeViewModel.ExpandableHomepageList) { val info = expand.list @@ -248,9 +247,10 @@ open class ParentItemAdapter( }) //(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged() - - moreInfo?.setOnClickListener { - moreInfoClickCallback.invoke(expand) + if (!isTvSettings()) { + title.setOnClickListener { + moreInfoClickCallback.invoke(expand) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 7564a9de..94a1a526 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -14,6 +14,7 @@ import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity +import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse @@ -31,9 +32,10 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectSt import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.activity_main_tv.view.* +import kotlinx.android.synthetic.main.activity_main.view.* import kotlinx.android.synthetic.main.fragment_home_head.view.* import kotlinx.android.synthetic.main.fragment_home_head.view.home_bookmarked_child_recyclerview +import kotlinx.android.synthetic.main.fragment_home_head.view.home_watch_parent_item_title import kotlinx.android.synthetic.main.fragment_home_head_tv.view.* import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_bookmarked_holder import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_none_padding @@ -46,11 +48,12 @@ import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_on_ho import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_watching_btt import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_watch_child_recyclerview import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_watch_holder +import kotlinx.android.synthetic.main.toast.view.* class HomeParentItemAdapterPreview( items: MutableList, val clickCallback: (SearchClickCallback) -> Unit, - moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, + private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, expandCallback: ((String) -> Unit)? = null, private val loadCallback: (LoadClickCallback) -> Unit, private val loadMoreCallback: (() -> Unit), @@ -136,7 +139,8 @@ class HomeParentItemAdapterPreview( clickCallback, reloadStored, loadStoredData, - searchQueryCallback + searchQueryCallback, + moreInfoClickCallback ).also { this.holder = it } @@ -182,7 +186,8 @@ class HomeParentItemAdapterPreview( private val searchClickCallback: (SearchClickCallback) -> Unit, private val reloadStored: () -> Unit, private val loadStoredData: ((Set) -> Unit), - private val searchQueryCallback: ((Pair) -> Unit) + private val searchQueryCallback: ((Pair) -> Unit), + private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit ) : RecyclerView.ViewHolder(itemView) { private var previewAdapter: HomeScrollAdapter? = null private val previewViewpager: ViewPager2? = itemView.home_preview_viewpager @@ -602,11 +607,43 @@ class HomeParentItemAdapterPreview( fun updateResume(resumeWatching: List) { resumeHolder?.isVisible = resumeWatching.isNotEmpty() resumeAdapter?.updateList(resumeWatching) + + if (!isTvSettings()) { + itemView.home_watch_parent_item_title?.setOnClickListener { + moreInfoClickCallback.invoke( + HomeViewModel.ExpandableHomepageList( + HomePageList( + itemView.home_watch_parent_item_title?.text.toString(), + resumeWatching, + false + ), 1, false + ) + ) + } + } } fun updateBookmarks(data: Pair>) { bookmarkHolder?.isVisible = data.first bookmarkAdapter?.updateList(data.second) + if (!isTvSettings()) { + itemView.home_bookmark_parent_item_title?.setOnClickListener { + val items = toggleList.mapNotNull { it.first }.filter { it.isChecked } + if (items.isEmpty()) return@setOnClickListener // we don't want to show an empty dialog + val textSum = items + .mapNotNull { it.text }.joinToString() + + moreInfoClickCallback.invoke( + HomeViewModel.ExpandableHomepageList( + HomePageList( + textSum, + data.second, + false + ), 1, false + ) + ) + } + } } fun setAvailableWatchStatusTypes(availableWatchStatusTypes: Pair, Set>) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt new file mode 100644 index 00000000..d7c06c4e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -0,0 +1,395 @@ +package com.lagradost.cloudstream3.ui.library + +import android.app.Activity +import android.content.Context +import android.content.res.Configuration +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.AlphaAnimation +import androidx.annotation.StringRes +import androidx.appcompat.widget.SearchView +import androidx.core.view.isVisible +import androidx.fragment.app.activityViewModels +import com.google.android.material.tabs.TabLayoutMediator +import com.lagradost.cloudstream3.APIHolder +import com.lagradost.cloudstream3.APIHolder.allProviders +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.Resource +import com.lagradost.cloudstream3.mvvm.debugAssert +import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment +import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD +import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA +import com.lagradost.cloudstream3.utils.AppUtils.loadResult +import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog +import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar +import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount +import kotlinx.android.synthetic.main.fragment_library.* +import kotlin.math.abs + +const val LIBRARY_FOLDER = "library_folder" + + +enum class LibraryOpenerType(@StringRes val stringRes: Int) { + Default(R.string.default_subtitles), // TODO FIX AFTER MERGE + Provider(R.string.none), + Browser(R.string.browser), + Search(R.string.search), + None(R.string.none), +} + +/** Used to store how the user wants to open said poster */ +data class LibraryOpener( + val openType: LibraryOpenerType, + val providerData: ProviderLibraryData?, +) + +data class ProviderLibraryData( + val apiName: String +) + +class LibraryFragment : Fragment() { + companion object { + fun newInstance() = LibraryFragment() + + /** + * Store which page was last seen when exiting the fragment and returning + **/ + const val VIEWPAGER_ITEM_KEY = "viewpager_item" + } + + private val libraryViewModel: LibraryViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_library, container, false) + } + + override fun onSaveInstanceState(outState: Bundle) { + viewpager?.currentItem?.let { currentItem -> + outState.putInt(VIEWPAGER_ITEM_KEY, currentItem) + } + super.onSaveInstanceState(outState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + context?.fixPaddingStatusbar(search_status_bar_padding) + + sort_fab?.setOnClickListener { + val methods = libraryViewModel.sortingMethods.map { + txt(it.stringRes).asString(view.context) + } + + activity?.showBottomDialog(methods, + libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod), + txt(R.string.sort_by).asString(view.context), + false, + {}, + { + val method = libraryViewModel.sortingMethods[it] + libraryViewModel.sort(method) + }) + } + + main_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + libraryViewModel.sort(ListSorting.Query, query) + return true + } + + // This is required to prevent the first text change + // When this is attached it'll immediately send a onQueryTextChange("") + // Which we do not want + var hasInitialized = false + override fun onQueryTextChange(newText: String?): Boolean { + if (!hasInitialized) { + hasInitialized = true + return true + } + + libraryViewModel.sort(ListSorting.Query, newText) + return true + } + }) + + libraryViewModel.reloadPages(false) + + list_selector?.setOnClickListener { + val items = libraryViewModel.availableApiNames + val currentItem = libraryViewModel.currentApiName.value + + activity?.showBottomDialog(items, + items.indexOf(currentItem), + txt(R.string.select_library).asString(it.context), + false, + {}) { index -> + val selectedItem = items.getOrNull(index) ?: return@showBottomDialog + libraryViewModel.switchList(selectedItem) + } + } + + + /** + * Shows a plugin selection dialogue and saves the response + **/ + fun Activity.showPluginSelectionDialog( + key: String, + syncId: SyncIdName, + apiName: String? = null, + ) { + val availableProviders = allProviders.filter { + it.supportedSyncNames.contains(syncId) + }.map { it.name } + + // Add the api if it exists + (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } ?: emptyList()) + + val baseOptions = listOf( + LibraryOpenerType.Default, + LibraryOpenerType.None, + LibraryOpenerType.Browser, + LibraryOpenerType.Search + ) + + val items = baseOptions.map { txt(it.stringRes).asString(this) } + availableProviders + + val savedSelection = getKey(LIBRARY_FOLDER, key) + val selectedIndex = + when { + savedSelection == null -> 0 + // If provider + savedSelection.openType == LibraryOpenerType.Provider + && savedSelection.providerData?.apiName != null -> { + availableProviders.indexOf(savedSelection.providerData.apiName) + .takeIf { it != -1 } + ?.plus(baseOptions.size) ?: 0 + } + // Else base option + else -> baseOptions.indexOf(savedSelection.openType) + } + + this.showBottomDialog( + items, + selectedIndex, + txt(R.string.open_with).asString(this), + false, + {}, + ) { + val savedData = if (it < baseOptions.size) { + LibraryOpener( + baseOptions[it], + null + ) + } else { + LibraryOpener( + LibraryOpenerType.Provider, + ProviderLibraryData(items[it]) + ) + } + + setKey( + LIBRARY_FOLDER, + key, + savedData, + ) + } + } + + provider_selector?.setOnClickListener { + val syncName = libraryViewModel.currentSyncApi?.syncIdName ?: return@setOnClickListener + activity?.showPluginSelectionDialog(syncName.name, syncName) + } + + viewpager?.setPageTransformer(LibraryScrollTransformer()) + viewpager?.adapter = + viewpager.adapter ?: ViewpagerAdapter(mutableListOf(), { isScrollingDown: Boolean -> + if (isScrollingDown) { + sort_fab?.shrink() + } else { + sort_fab?.extend() + } + }) callback@{ searchClickCallback -> + // To prevent future accidents + debugAssert({ + searchClickCallback.card !is SyncAPI.LibraryItem + }, { + "searchClickCallback ${searchClickCallback.card} is not a LibraryItem" + }) + + val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId + val syncName = + libraryViewModel.currentSyncApi?.syncIdName ?: return@callback + + when (searchClickCallback.action) { + SEARCH_ACTION_SHOW_METADATA -> { + activity?.showPluginSelectionDialog( + syncId, + syncName, + searchClickCallback.card.apiName + ) + } + + SEARCH_ACTION_LOAD -> { + // This basically first selects the individual opener and if that is default then + // selects the whole list opener + val savedListSelection = + getKey(LIBRARY_FOLDER, syncName.name) + val savedSelection = getKey(LIBRARY_FOLDER, syncId).takeIf { + it?.openType != LibraryOpenerType.Default + } ?: savedListSelection + + when (savedSelection?.openType) { + null, LibraryOpenerType.Default -> { + // Prevents opening MAL/AniList as a provider + if (APIHolder.getApiFromNameNull(searchClickCallback.card.apiName) != null) { + activity?.loadSearchResult( + searchClickCallback.card + ) + } else { + // Search when no provider can open + QuickSearchFragment.pushSearch( + activity, + searchClickCallback.card.name + ) + } + } + LibraryOpenerType.None -> {} + LibraryOpenerType.Provider -> + savedSelection.providerData?.apiName?.let { apiName -> + activity?.loadResult( + searchClickCallback.card.url, + apiName, + ) + } + LibraryOpenerType.Browser -> + openBrowser(searchClickCallback.card.url) + LibraryOpenerType.Search -> { + QuickSearchFragment.pushSearch( + activity, + searchClickCallback.card.name + ) + } + } + } + } + } + + viewpager?.offscreenPageLimit = 2 + viewpager?.reduceDragSensitivity() + + val startLoading = Runnable { + gridview?.numColumns = context?.getSpanCount() ?: 3 + gridview?.adapter = + context?.let { LoadingPosterAdapter(it, 6 * 3) } + library_loading_overlay?.isVisible = true + library_loading_shimmer?.startShimmer() + empty_list_textview?.isVisible = false + } + + val stopLoading = Runnable { + gridview?.adapter = null + library_loading_overlay?.isVisible = false + library_loading_shimmer?.stopShimmer() + } + + val handler = Handler(Looper.getMainLooper()) + + observe(libraryViewModel.pages) { resource -> + when (resource) { + is Resource.Success -> { + handler.removeCallbacks(startLoading) + val pages = resource.value + val showNotice = pages.all { it.items.isEmpty() } + empty_list_textview?.isVisible = showNotice + if (showNotice) { + if (libraryViewModel.availableApiNames.size > 1) { + empty_list_textview?.setText(R.string.empty_library_logged_in_message) + } else { + empty_list_textview?.setText(R.string.empty_library_no_accounts_message) + } + } + + (viewpager.adapter as? ViewpagerAdapter)?.pages = pages + // Using notifyItemRangeChanged keeps the animations when sorting + viewpager.adapter?.notifyItemRangeChanged(0, viewpager.adapter?.itemCount ?: 0) + + // Only stop loading after 300ms to hide the fade effect the viewpager produces when updating + // Without this there would be a flashing effect: + // loading -> show old viewpager -> black screen -> show new viewpager + handler.postDelayed(stopLoading, 300) + + savedInstanceState?.getInt(VIEWPAGER_ITEM_KEY)?.let { currentPos -> + if (currentPos < 0) return@let + viewpager?.setCurrentItem(currentPos, false) + // Using remove() sets the key to 0 instead of removing it + savedInstanceState.putInt(VIEWPAGER_ITEM_KEY, -1) + } + + // Since the animation to scroll multiple items is so much its better to just hide + // the viewpager a bit while the fastest animation is running + fun hideViewpager(distance: Int) { + if (distance < 3) return + + val hideAnimation = AlphaAnimation(1f, 0f).apply { + duration = distance * 50L + fillAfter = true + } + val showAnimation = AlphaAnimation(0f, 1f).apply { + duration = distance * 50L + startOffset = distance * 100L + fillAfter = true + } + viewpager?.startAnimation(hideAnimation) + viewpager?.startAnimation(showAnimation) + } + + TabLayoutMediator( + library_tab_layout, + viewpager, + ) { tab, position -> + tab.text = pages.getOrNull(position)?.title?.asStringNull(context) + tab.view.setOnClickListener { + val currentItem = viewpager?.currentItem ?: return@setOnClickListener + val distance = abs(position - currentItem) + hideViewpager(distance) + } + }.attach() + } + is Resource.Loading -> { + // Only start loading after 200ms to prevent loading cached lists + handler.postDelayed(startLoading, 200) + } + is Resource.Failure -> { + stopLoading.run() + // No user indication it failed :( + // TODO + } + } + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + (viewpager.adapter as? ViewpagerAdapter)?.rebind() + super.onConfigurationChanged(newConfig) + } +} + +class MenuSearchView(context: Context) : SearchView(context) { + override fun onActionViewCollapsed() { + super.onActionViewCollapsed() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryScrollTransformer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryScrollTransformer.kt new file mode 100644 index 00000000..8aafbdd6 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryScrollTransformer.kt @@ -0,0 +1,17 @@ +package com.lagradost.cloudstream3.ui.library + +import android.view.View +import androidx.viewpager2.widget.ViewPager2 +import kotlinx.android.synthetic.main.library_viewpager_page.view.* +import kotlin.math.roundToInt + +class LibraryScrollTransformer : ViewPager2.PageTransformer { + override fun transformPage(page: View, position: Float) { + val padding = (-position * page.width).roundToInt() + page.page_recyclerview.setPadding( + padding, 0, + -padding, 0 + ) + } +} + diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt new file mode 100644 index 00000000..5f64880c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -0,0 +1,104 @@ +package com.lagradost.cloudstream3.ui.library + +import androidx.annotation.StringRes +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.Resource +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis +import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import kotlinx.coroutines.delay + +enum class ListSorting(@StringRes val stringRes: Int) { + Query(R.string.none), + RatingHigh(R.string.sort_rating_desc), + RatingLow(R.string.sort_rating_asc), + UpdatedNew(R.string.sort_updated_new), + UpdatedOld(R.string.sort_updated_old), + AlphabeticalA(R.string.sort_alphabetical_a), + AlphabeticalZ(R.string.sort_alphabetical_z), +} + +const val LAST_SYNC_API_KEY = "last_sync_api" + +class LibraryViewModel : ViewModel() { + private val _pages: MutableLiveData>> = MutableLiveData(null) + val pages: LiveData>> = _pages + + private val _currentApiName: MutableLiveData = MutableLiveData("") + val currentApiName: LiveData = _currentApiName + + private val availableSyncApis + get() = SyncApis.filter { it.hasAccount() } + + var currentSyncApi = availableSyncApis.let { allApis -> + val lastSelection = getKey(LAST_SYNC_API_KEY) + availableSyncApis.firstOrNull { it.name == lastSelection } ?: allApis.firstOrNull() + } + private set(value) { + field = value + setKey(LAST_SYNC_API_KEY, field?.name) + } + + val availableApiNames: List + get() = availableSyncApis.map { it.name } + + var sortingMethods = emptyList() + private set + + var currentSortingMethod: ListSorting? = sortingMethods.firstOrNull() + private set + + fun switchList(name: String) { + currentSyncApi = availableSyncApis[availableApiNames.indexOf(name)] + _currentApiName.postValue(currentSyncApi?.name) + reloadPages(true) + } + + fun sort(method: ListSorting, query: String? = null) { + val currentList = pages.value ?: return + currentSortingMethod = method + (currentList as? Resource.Success)?.value?.forEachIndexed { _, page -> + page.sort(method, query) + } + _pages.postValue(currentList) + } + + fun reloadPages(forceReload: Boolean) { + // Only skip loading if its not forced and pages is not empty + if (!forceReload && (pages.value as? Resource.Success)?.value?.isNotEmpty() == true && + currentSyncApi?.requireLibraryRefresh != true + ) return + + ioSafe { + currentSyncApi?.let { repo -> + _currentApiName.postValue(repo.name) + _pages.postValue(Resource.Loading()) + val libraryResource = repo.getPersonalLibrary() + if (libraryResource is Resource.Failure) { + _pages.postValue(libraryResource) + return@let + } + val library = (libraryResource as? Resource.Success)?.value ?: return@let + + sortingMethods = library.supportedListSorting.toList() + currentSortingMethod = null + + repo.requireLibraryRefresh = false + + val pages = library.allLibraryLists.map { + SyncAPI.Page( + it.name, + it.items + ) + } + + _pages.postValue(Resource.Success(pages)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LoadingPosterAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LoadingPosterAdapter.kt new file mode 100644 index 00000000..a637133b --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LoadingPosterAdapter.kt @@ -0,0 +1,37 @@ +package com.lagradost.cloudstream3.ui.library + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.ListPopupWindow.MATCH_PARENT +import android.widget.RelativeLayout +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.utils.UIHelper.toPx +import kotlinx.android.synthetic.main.loading_poster_dynamic.view.* +import kotlin.math.roundToInt +import kotlin.math.sqrt + +class LoadingPosterAdapter(context: Context, private val itemCount: Int) : + BaseAdapter() { + private val inflater: LayoutInflater = LayoutInflater.from(context) + + override fun getCount(): Int { + return itemCount + } + + override fun getItem(position: Int): Any? { + return null + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + return convertView ?: inflater.inflate(R.layout.loading_poster_dynamic, parent, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt new file mode 100644 index 00000000..2435f8be --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt @@ -0,0 +1,130 @@ +package com.lagradost.cloudstream3.ui.library + +import android.content.res.ColorStateList +import android.graphics.Color +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.AcraApplication +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.ui.AutofitRecyclerView +import com.lagradost.cloudstream3.ui.search.SearchClickCallback +import com.lagradost.cloudstream3.ui.search.SearchResultBuilder +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.UIHelper.toPx +import kotlinx.android.synthetic.main.search_result_grid_expanded.view.* +import kotlin.math.roundToInt + + +class PageAdapter( + override val items: MutableList, + private val resView: AutofitRecyclerView, + val clickCallback: (SearchClickCallback) -> Unit +) : + AppUtils.DiffAdapter(items) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return LibraryItemViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.search_result_grid_expanded, parent, false) + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is LibraryItemViewHolder -> { + holder.bind(items[position], position) + } + } + } + + private fun isDark(color: Int): Boolean { + return ColorUtils.calculateLuminance(color) < 0.5 + } + + fun getDifferentColor(color: Int, ratio: Float = 0.7f): Int { + return if (isDark(color)) { + ColorUtils.blendARGB(color, Color.WHITE, ratio) + } else { + ColorUtils.blendARGB(color, Color.BLACK, ratio) + } + } + + inner class LibraryItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val cardView: ImageView = itemView.imageView + + private val compactView = false//itemView.context.getGridIsCompact() + private val coverHeight: Int = + if (compactView) 80.toPx else (resView.itemWidth / 0.68).roundToInt() + + fun bind(item: SyncAPI.LibraryItem, position: Int) { + /** https://stackoverflow.com/questions/8817522/how-to-get-color-code-of-image-view */ + + SearchResultBuilder.bind( + this@PageAdapter.clickCallback, + item, + position, + itemView, + colorCallback = { palette -> + AcraApplication.context?.let { ctx -> + val defColor = ContextCompat.getColor(ctx, R.color.ratingColorBg) + var bg = palette.getDarkVibrantColor(defColor) + if (bg == defColor) { + bg = palette.getDarkMutedColor(defColor) + } + if (bg == defColor) { + bg = palette.getVibrantColor(defColor) + } + + val fg = + getDifferentColor(bg)//palette.getVibrantColor(ContextCompat.getColor(ctx,R.color.ratingColor)) + itemView.text_rating.apply { + setTextColor(ColorStateList.valueOf(fg)) + } + itemView.text_rating_holder?.backgroundTintList = ColorStateList.valueOf(bg) + itemView.watchProgress?.apply { + progressTintList = ColorStateList.valueOf(fg) + progressBackgroundTintList = ColorStateList.valueOf(bg) + } + } + } + ) + + // See searchAdaptor for this, it basically fixes the height + if (!compactView) { + cardView.apply { + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + coverHeight + ) + } + } + + val showProgress = item.episodesCompleted != null && item.episodesTotal != null + itemView.watchProgress.isVisible = showProgress + if (showProgress) { + itemView.watchProgress.max = item.episodesTotal!! + itemView.watchProgress.progress = item.episodesCompleted!! + } + + itemView.imageText.text = item.name + + val showRating = (item.personalRating ?: 0) != 0 + itemView.text_rating_holder.isVisible = showRating + if (showRating) { + // We want to show 8.5 but not 8.0 hence the replace + val rating = ((item.personalRating ?: 0).toDouble() / 10).toString() + .replace(".0", "") + + itemView.text_rating.text = "★ $rating" + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt new file mode 100644 index 00000000..33a40386 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -0,0 +1,90 @@ +package com.lagradost.cloudstream3.ui.library + +import android.os.Build +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.doOnAttach +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.OnFlingListener +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.ui.search.SearchClickCallback +import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount +import kotlinx.android.synthetic.main.library_viewpager_page.view.* + +class ViewpagerAdapter( + var pages: List, + val scrollCallback: (isScrollingDown: Boolean) -> Unit, + val clickCallback: (SearchClickCallback) -> Unit +) : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return PageViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.library_viewpager_page, parent, false) + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is PageViewHolder -> { + holder.bind(pages[position], unbound.remove(position)) + } + } + } + + private val unbound = mutableSetOf() + /** + * Used to mark all pages for re-binding and forces all items to be refreshed + * Without this the pages will still use the same adapters + **/ + fun rebind() { + unbound.addAll(0..pages.size) + this.notifyItemRangeChanged(0, pages.size) + } + + inner class PageViewHolder(private val itemViewTest: View) : + RecyclerView.ViewHolder(itemViewTest) { + fun bind(page: SyncAPI.Page, rebind: Boolean) { + itemView.page_recyclerview?.spanCount = + this@PageViewHolder.itemView.context.getSpanCount() ?: 3 + + if (itemViewTest.page_recyclerview?.adapter == null || rebind) { + // Only add the items after it has been attached since the items rely on ItemWidth + // Which is only determined after the recyclerview is attached. + // If this fails then item height becomes 0 when there is only one item + itemViewTest.page_recyclerview?.doOnAttach { + itemViewTest.page_recyclerview?.adapter = PageAdapter( + page.items.toMutableList(), + itemViewTest.page_recyclerview, + clickCallback + ) + } + } else { + (itemViewTest.page_recyclerview?.adapter as? PageAdapter)?.updateList(page.items) + itemViewTest.page_recyclerview?.scrollToPosition(0) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + itemViewTest.page_recyclerview.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY -> + val diff = scrollY - oldScrollY + if (diff == 0) return@setOnScrollChangeListener + + scrollCallback.invoke(diff > 0) + } + } else { + itemViewTest.page_recyclerview.onFlingListener = object : OnFlingListener() { + override fun onFling(velocityX: Int, velocityY: Int): Boolean { + scrollCallback.invoke(velocityY > 0) + return false + } + } + } + + } + } + + override fun getItemCount(): Int { + return pages.size + } +} \ No newline at end of file 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 c79cdd76..8d28fd9d 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 @@ -109,6 +109,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected var currentPrefQuality = Qualities.P2160.value // preferred maximum quality, used for ppl w bad internet or on cell protected var fastForwardTime = 10000L + protected var androidTVInterfaceOffSeekTime = 10000L; + protected var androidTVInterfaceOnSeekTime = 30000L; protected var swipeHorizontalEnabled = false protected var swipeVerticalEnabled = false protected var playBackSpeedEnabled = false @@ -605,7 +607,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player_top_holder?.isGone = isGone //player_episodes_button?.isVisible = !isGone && hasEpisodes player_video_title?.isGone = togglePlayerTitleGone - player_video_title_rez?.isGone = isGone +// player_video_title_rez?.isGone = isGone player_episode_filler?.isGone = isGone player_center_menu?.isGone = isGone player_lock?.isGone = !isShowing @@ -1051,19 +1053,19 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } KeyEvent.KEYCODE_DPAD_LEFT -> { if (!isShowing && !isLocked) { - player.seekTime(-10000L) + player.seekTime(-androidTVInterfaceOffSeekTime) return true } else if (player_pause_play?.isFocused == true) { - player.seekTime(-30000L) + player.seekTime(-androidTVInterfaceOnSeekTime) return true } } KeyEvent.KEYCODE_DPAD_RIGHT -> { if (!isShowing && !isLocked) { - player.seekTime(10000L) + player.seekTime(androidTVInterfaceOffSeekTime) return true } else if (player_pause_play?.isFocused == true) { - player.seekTime(30000L) + player.seekTime(androidTVInterfaceOnSeekTime) return true } } @@ -1207,6 +1209,13 @@ open class FullScreenPlayer : AbstractPlayerFragment() { settingsManager.getInt(ctx.getString(R.string.double_tap_seek_time_key), 10) .toLong() * 1000L + androidTVInterfaceOffSeekTime = + settingsManager.getInt(ctx.getString(R.string.android_tv_interface_off_seek_key), 10) + .toLong() * 1000L + androidTVInterfaceOnSeekTime = + settingsManager.getInt(ctx.getString(R.string.android_tv_interface_on_seek_key), 10) + .toLong() * 1000L + navigationBarHeight = ctx.getNavigationBarHeight() statusBarHeight = ctx.getStatusBarHeight() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index bf39edc7..67f58195 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -11,9 +11,7 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.inputmethod.EditorInfo import android.widget.* -import android.widget.TextView.OnEditorActionListener import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.core.animation.addListener @@ -528,7 +526,7 @@ class GeneratorPlayer : FullScreenPlayer() { } } - var selectSourceDialog: AlertDialog? = null + var selectSourceDialog: Dialog? = null // var selectTracksDialog: AlertDialog? = null override fun showMirrorsDialogue() { @@ -540,10 +538,8 @@ class GeneratorPlayer : FullScreenPlayer() { player.handleEvent(CSPlayerEvent.Pause) val currentSubtitles = sortSubs(currentSubs) - val sourceBuilder = AlertDialog.Builder(ctx, R.style.AlertDialogCustomBlack) - .setView(R.layout.player_select_source_and_subs) - - val sourceDialog = sourceBuilder.create() + val sourceDialog = Dialog(ctx, R.style.AlertDialogCustomBlack) + sourceDialog.setContentView(R.layout.player_select_source_and_subs) selectSourceDialog = sourceDialog @@ -1149,13 +1145,15 @@ class GeneratorPlayer : FullScreenPlayer() { val source = currentSelectedLink?.first?.name ?: currentSelectedLink?.second?.name ?: "NULL" - player_video_title_rez?.text = when (titleRez) { + val title = when (titleRez) { 0 -> "" 1 -> extra 2 -> source 3 -> "$source - $extra" else -> "" } + player_video_title_rez?.text = title + player_video_title_rez?.isVisible = title.isNotBlank() } override fun playerDimensionsLoaded(widthHeight: Pair) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt index ad3d9eb8..ba57d2de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -220,7 +220,7 @@ class QuickSearchFragment : Fragment() { when (it) { is Resource.Success -> { it.value.let { data -> - (quick_search_autofit_results?.adapter as? SearchAdapter?)?.updateList( + (quick_search_autofit_results?.adapter as? SearchAdapter)?.updateList( context?.filterSearchResultByFilmQuality(data) ?: data ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 68a57b7f..68dd1c0e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -277,7 +277,7 @@ open class ResultFragment : ResultTrailerPlayer() { private var downloadButton: EasyDownloadButton? = null override fun onDestroyView() { updateUIListener = null - (result_episodes?.adapter as EpisodeAdapter?)?.killAdapter() + (result_episodes?.adapter as? EpisodeAdapter)?.killAdapter() downloadButton?.dispose() super.onDestroyView() @@ -458,7 +458,7 @@ open class ResultFragment : ResultTrailerPlayer() { temporary_no_focus?.requestFocus() } - (result_episodes?.adapter as? EpisodeAdapter?)?.updateList(episodes.value) + (result_episodes?.adapter as? EpisodeAdapter)?.updateList(episodes.value) if (isTv && hasEpisodes) main { delay(500) @@ -687,7 +687,7 @@ open class ResultFragment : ResultTrailerPlayer() { val newList = list.filter { it.isSynced && it.hasAccount } result_mini_sync?.isVisible = newList.isNotEmpty() - (result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.mapNotNull { it.icon }) + (result_mini_sync?.adapter as? ImageAdapter)?.updateList(newList.mapNotNull { it.icon }) } var currentSyncProgress = 0 @@ -850,6 +850,7 @@ open class ResultFragment : ResultTrailerPlayer() { } observe(viewModel.page) { data -> + if(data == null) return@observe when (data) { is Resource.Success -> { val d = data.value @@ -899,7 +900,7 @@ open class ResultFragment : ResultTrailerPlayer() { result_cast_items?.isVisible = d.actors != null - (result_cast_items?.adapter as ActorAdaptor?)?.apply { + (result_cast_items?.adapter as? ActorAdaptor)?.apply { updateList(d.actors ?: emptyList()) } @@ -973,6 +974,7 @@ open class ResultFragment : ResultTrailerPlayer() { chip.isCheckable = false chip.isFocusable = false chip.isClickable = false + chip.setTextColor(context.colorFromAttribute(R.attr.textColor)) addView(chip) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index 9bae8753..b38e1765 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -485,7 +485,7 @@ class ResultFragmentPhone : ResultFragment() { result_recommendations?.post { rec?.let { list -> - (result_recommendations?.adapter as SearchAdapter?)?.updateList(list.filter { it.apiName == matchAgainst }) + (result_recommendations?.adapter as? SearchAdapter)?.updateList(list.filter { it.apiName == matchAgainst }) } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index d5cab1a6..2bd8ff0f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -107,7 +107,7 @@ class ResultFragmentTv : ResultFragment() { result_recommendations?.isGone = isInvalid result_recommendations_holder?.isGone = isInvalid val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName - (result_recommendations?.adapter as SearchAdapter?)?.updateList(rec?.filter { it.apiName == matchAgainst } + (result_recommendations?.adapter as? SearchAdapter)?.updateList(rec?.filter { it.apiName == matchAgainst } ?: emptyList()) rec?.map { it.apiName }?.distinct()?.let { apiNames -> 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 4bc7bc48..afaaeef9 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 @@ -13,6 +13,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.AcraApplication.Companion.setKey @@ -24,6 +25,7 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie import com.lagradost.cloudstream3.metaproviders.SyncRedirector import com.lagradost.cloudstream3.mvvm.* +import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.APIRepository @@ -55,7 +57,6 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason import com.lagradost.cloudstream3.utils.UIHelper.navigate import kotlinx.coroutines.* import java.io.File -import java.lang.Math.abs import java.util.concurrent.TimeUnit @@ -314,6 +315,11 @@ data class ExtractedTrailerData( class ResultViewModel2 : ViewModel() { private var currentResponse: LoadResponse? = null + fun clear() { + currentResponse = null + _page.postValue(null) + } + data class EpisodeIndexer( val dubStatus: DubStatus, val season: Int, @@ -340,9 +346,9 @@ class ResultViewModel2 : ViewModel() { //private val currentHeaderName get() = currentResponse?.name - private val _page: MutableLiveData> = - MutableLiveData(Resource.Loading()) - val page: LiveData> = _page + private val _page: MutableLiveData?> = + MutableLiveData(null) + val page: LiveData?> = _page private val _episodes: MutableLiveData>> = MutableLiveData(ResourceSome.Loading()) @@ -398,7 +404,6 @@ class ResultViewModel2 : ViewModel() { private val _selectedDubStatusIndex: MutableLiveData = MutableLiveData(-1) val selectedDubStatusIndex: LiveData = _selectedDubStatusIndex - private val _loadedLinks: MutableLiveData> = MutableLiveData(Some.None) val loadedLinks: LiveData> = _loadedLinks @@ -421,7 +426,6 @@ class ResultViewModel2 : ViewModel() { fun updateWatchStatus(currentResponse: LoadResponse, status: WatchType) { val currentId = currentResponse.getId() - val resultPage = currentResponse DataStoreHelper.setResultWatchState(currentId, status.internalId) val current = DataStoreHelper.getBookmarkedData(currentId) @@ -432,12 +436,12 @@ class ResultViewModel2 : ViewModel() { currentId, current?.bookmarkedTime ?: currentTime, currentTime, - resultPage.name, - resultPage.url, - resultPage.apiName, - resultPage.type, - resultPage.posterUrl, - resultPage.year + currentResponse.name, + currentResponse.url, + currentResponse.apiName, + currentResponse.type, + currentResponse.posterUrl, + currentResponse.year ) ) } @@ -1419,79 +1423,127 @@ class ResultViewModel2 : ViewModel() { meta: SyncAPI.SyncResult?, syncs: Map? = null ): Pair { - if (meta == null) return resp to false + //if (meta == null) return resp to false var updateEpisodes = false val out = resp.apply { Log.i(TAG, "applyMeta") - duration = duration ?: meta.duration - rating = rating ?: meta.publicScore - tags = tags ?: meta.genres - plot = if (plot.isNullOrBlank()) meta.synopsis else plot - posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl - actors = actors ?: meta.actors + if (meta != null) { + duration = duration ?: meta.duration + rating = rating ?: meta.publicScore + tags = tags ?: meta.genres + plot = if (plot.isNullOrBlank()) meta.synopsis else plot + posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl + actors = actors ?: meta.actors - if (this is EpisodeResponse) { - nextAiring = nextAiring ?: meta.nextAiring + if (this is EpisodeResponse) { + nextAiring = nextAiring ?: meta.nextAiring + } + + val realRecommendations = ArrayList() + val apiNames = apis.filter { + it.name.contains("gogoanime", true) || + it.name.contains("9anime", true) + }.map { + it.name + } + + meta.recommendations?.forEach { rec -> + apiNames.forEach { name -> + realRecommendations.add(rec.copy(apiName = name)) + } + } + + recommendations = recommendations?.union(realRecommendations)?.toList() + ?: realRecommendations } for ((k, v) in syncs ?: emptyMap()) { syncData[k] = v } - val realRecommendations = ArrayList() - // TODO: fix - //val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name) - // meta.recommendations?.forEach { rec -> - // apiNames.forEach { name -> - // realRecommendations.add(rec.copy(apiName = name)) - // } - // } + argamap( + { + if (this !is AnimeLoadResponse) return@argamap + // already exist, no need to run getTracker + if (this.getAniListId() != null && this.getMalId() != null) return@argamap - recommendations = recommendations?.union(realRecommendations)?.toList() - ?: realRecommendations - - argamap({ - addTrailer(meta.trailers) - }, { - if (this !is AnimeLoadResponse) return@argamap - val map = - Kitsu.getEpisodesDetails( - getMalId(), - getAniListId(), - isResponseRequired = false + val res = APIHolder.getTracker( + listOfNotNull( + this.engName, + this.name, + this.japName + ).filter { it.length > 2 }.distinct(), // the reason why we filter is due to not wanting smth like " " or "?" + TrackerType.getTypes(this.type), + this.year ) - if (map.isNullOrEmpty()) return@argamap - updateEpisodes = DubStatus.values().map { dubStatus -> - val current = - this.episodes[dubStatus]?.mapIndexed { index, episode -> - episode.apply { - this.episode = this.episode ?: (index + 1) - } - }?.sortedBy { it.episode ?: 0 }?.toMutableList() - if (current.isNullOrEmpty()) return@map false - val episodeNumbers = current.map { ep -> ep.episode!! } - var updateCount = 0 - map.forEach { (episode, node) -> - episodeNumbers.binarySearch(episode).let { index -> - current.getOrNull(index)?.let { currentEp -> - current[index] = currentEp.apply { - updateCount++ - val currentBack = this - this.description = this.description ?: node.description?.en - this.name = this.name ?: node.titles?.canonical - this.episode = - this.episode ?: node.num ?: episodeNumbers[index] - this.posterUrl = - this.posterUrl ?: node.thumbnail?.original?.url + + val ids = arrayOf( + AccountManager.malApi.idPrefix to res?.malId?.toString(), + AccountManager.aniListApi.idPrefix to res?.aniId + ) + + if (ids.any { (id, new) -> + val current = syncData[id] + new != null && current != null && current != new + } + ) { + // getTracker fucked up as it conflicts with current implementation + return@argamap + } + + // set all the new data, prioritise old correct data + ids.forEach { (id, new) -> + new?.let { + syncData[id] = syncData[id] ?: it + } + } + + // set posters, might fuck up due to headers idk + posterUrl = posterUrl ?: res?.image + backgroundPosterUrl = backgroundPosterUrl ?: res?.cover + }, + { + if (meta == null) return@argamap + addTrailer(meta.trailers) + }, { + if (this !is AnimeLoadResponse) return@argamap + val map = + Kitsu.getEpisodesDetails( + getMalId(), + getAniListId(), + isResponseRequired = false + ) + if (map.isNullOrEmpty()) return@argamap + updateEpisodes = DubStatus.values().map { dubStatus -> + val current = + this.episodes[dubStatus]?.mapIndexed { index, episode -> + episode.apply { + this.episode = this.episode ?: (index + 1) + } + }?.sortedBy { it.episode ?: 0 }?.toMutableList() + if (current.isNullOrEmpty()) return@map false + val episodeNumbers = current.map { ep -> ep.episode!! } + var updateCount = 0 + map.forEach { (episode, node) -> + episodeNumbers.binarySearch(episode).let { index -> + current.getOrNull(index)?.let { currentEp -> + current[index] = currentEp.apply { + updateCount++ + this.description = this.description ?: node.description?.en + this.name = this.name ?: node.titles?.canonical + this.episode = + this.episode ?: node.num ?: episodeNumbers[index] + this.posterUrl = + this.posterUrl ?: node.thumbnail?.original?.url + } } } } - } - this.episodes[dubStatus] = current - updateCount > 0 - }.any { it } - }) + this.episodes[dubStatus] = current + updateCount > 0 + }.any { it } + }) } return out to updateEpisodes } @@ -1628,10 +1680,11 @@ class ResultViewModel2 : ViewModel() { if (ranges?.contains(range) != true) { // if the current ranges does not include the range then select the range with the closest matching start episode // this usually happends when dub has less episodes then sub -> the range does not exist - ranges?.minByOrNull { abs(it.startEpisode - range.startEpisode) }?.let { r -> - postEpisodeRange(indexer, r) - return - } + ranges?.minByOrNull { kotlin.math.abs(it.startEpisode - range.startEpisode) } + ?.let { r -> + postEpisodeRange(indexer, r) + return + } } val isMovie = currentResponse?.isMovie() == true @@ -2112,8 +2165,9 @@ class ResultViewModel2 : ViewModel() { showFillers: Boolean, dubStatus: DubStatus, autostart: AutoResume?, + loadTrailers: Boolean = true, ) = - viewModelScope.launchSafe { + ioSafe { _page.postValue(Resource.Loading(url)) _episodes.postValue(ResourceSome.Loading()) @@ -2131,7 +2185,7 @@ class ResultViewModel2 : ViewModel() { "This provider does not exist" ) ) - return@launchSafe + return@ioSafe } @@ -2139,24 +2193,18 @@ class ResultViewModel2 : ViewModel() { val validUrlResource = safeApiCall { SyncRedirector.redirect( url, - api.mainUrl + api ) } - // TODO: fix - // val validUrlResource = safeApiCall { - // SyncRedirector.redirect( - // url, - // api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime") - // .replace(GogoanimeProvider().mainUrl, "gogoanime") - // ) - // } + if (validUrlResource !is Resource.Success) { if (validUrlResource is Resource.Failure) { _page.postValue(validUrlResource) } - return@launchSafe + return@ioSafe } + val validUrl = validUrlResource.value val repo = APIRepository(api) currentRepo = repo @@ -2166,11 +2214,11 @@ class ResultViewModel2 : ViewModel() { _page.postValue(data) } is Resource.Success -> { - if (!isActive) return@launchSafe + if (!isActive) return@ioSafe val loadResponse = ioWork { applyMeta(data.value, currentMeta, currentSync).first } - if (!isActive) return@launchSafe + if (!isActive) return@ioSafe val mainId = loadResponse.getId() preferDubStatus = getDub(mainId) ?: preferDubStatus @@ -2190,15 +2238,15 @@ class ResultViewModel2 : ViewModel() { System.currentTimeMillis(), ) ) - - loadTrailers(data.value) + if (loadTrailers) + loadTrailers(data.value) postSuccessful( data.value, updateEpisodes = true, updateFillers = showFillers, apiRepository = repo ) - if (!isActive) return@launchSafe + if (!isActive) return@ioSafe handleAutoStart(activity, autostart) } is Resource.Loading -> { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt index ddf559fc..649641c8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt @@ -10,12 +10,16 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.ui.AutofitRecyclerView +import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx import kotlinx.android.synthetic.main.search_result_compact.view.* import kotlin.math.roundToInt +/** Click */ const val SEARCH_ACTION_LOAD = 0 + +/** Long press */ const val SEARCH_ACTION_SHOW_METADATA = 1 const val SEARCH_ACTION_PLAY_FILE = 2 const val SEARCH_ACTION_FOCUSED = 4 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 1da30691..b4a38216 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 @@ -45,6 +45,7 @@ import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.updateChips import com.lagradost.cloudstream3.ui.home.ParentItemAdapter import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.utils.AppUtils.ownHide import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.main @@ -121,6 +122,7 @@ class SearchFragment : Fragment() { override fun onDestroyView() { hideKeyboard() + bottomSheetDialog?.ownHide() super.onDestroyView() } @@ -418,7 +420,7 @@ class SearchFragment : Fragment() { is Resource.Success -> { it.value.let { data -> if (data.isNotEmpty()) { - (search_autofit_results?.adapter as SearchAdapter?)?.updateList(data) + (search_autofit_results?.adapter as? SearchAdapter)?.updateList(data) } } searchExitIcon.alpha = 1f diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt index 1de89809..45336d5b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt @@ -3,11 +3,13 @@ package com.lagradost.cloudstream3.ui.search import android.app.Activity import android.widget.Toast import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_PLAY_FILE import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.ui.result.START_ACTION_LOAD_EP +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper @@ -54,7 +56,15 @@ object SearchHelper { } } SEARCH_ACTION_SHOW_METADATA -> { - showToast(activity, callback.card.name, Toast.LENGTH_SHORT) + if(!isTvSettings()) { // we only want this on phone as UI is not done yet on tv + (activity as? MainActivity?)?.apply { + loadPopup(callback.card) + } ?: kotlin.run { + showToast(activity, callback.card.name, Toast.LENGTH_SHORT) + } + } else { + showToast(activity, callback.card.name, Toast.LENGTH_SHORT) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt index 3afbb8c0..3447ee32 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt @@ -1,12 +1,14 @@ package com.lagradost.cloudstream3.ui.search import android.content.Context +import android.graphics.drawable.Drawable import android.view.View import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.cardview.widget.CardView import androidx.core.view.isVisible +import androidx.palette.graphics.Palette import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings @@ -41,6 +43,7 @@ object SearchResultBuilder { nextFocusBehavior: Boolean? = null, nextFocusUp: Int? = null, nextFocusDown: Int? = null, + colorCallback : ((Palette) -> Unit)? = null ) { val cardView: ImageView = itemView.imageView val cardText: TextView? = itemView.imageText @@ -100,7 +103,7 @@ object SearchResultBuilder { cardText?.isVisible = showTitle cardView.isVisible = true - if (!cardView.setImage(card.posterUrl, card.posterHeaders)) { + if (!cardView.setImage(card.posterUrl, card.posterHeaders, colorCallback = colorCallback)) { cardView.setImageResource(R.drawable.default_cover) } 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 54c5bdb5..2e249948 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 @@ -56,46 +56,48 @@ fun getCurrentLocale(context: Context): String { // https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes leave blank for auto val appLanguages = arrayListOf( /* begin language list */ - Triple("", "Arabic", "ar"), - Triple("", "Bulgarian", "bg"), - Triple("", "Bengali", "bn"), - Triple("\uD83C\uDDE7\uD83C\uDDF7", "Brazilian Portuguese", "bp"), - Triple("", "Czech", "cs"), - Triple("", "German", "de"), - Triple("", "Greek", "el"), + Triple("", "العربية", "ar"), + Triple("", "български", "bg"), + Triple("", "বাংলা", "bn"), + Triple("\uD83C\uDDE7\uD83C\uDDF7", "português brasileiro", "bp"), + Triple("", "čeština", "cs"), + Triple("", "Deutsch", "de"), + Triple("", "Ελληνικά", "el"), Triple("", "English", "en"), Triple("", "Esperanto", "eo"), - Triple("", "Spanish", "es"), - Triple("", "Farsi", "fa"), - Triple("", "French", "fr"), - Triple("", "Hindi", "hi"), - Triple("", "Croatian", "hr"), - Triple("", "Hungarian", "hu"), - Triple("\uD83C\uDDEE\uD83C\uDDE9", "Indonesian", "in"), - Triple("", "Italian", "it"), - Triple("\uD83C\uDDEE\uD83C\uDDF1", "Hebrew", "iw"), - Triple("", "Kannada", "kn"), - Triple("", "Macedonian", "mk"), - Triple("", "Malayalam", "ml"), - Triple("", "Moldavian", "mo"), - Triple("", "Dutch", "nl"), - Triple("", "Norwegian Nynorsk", "nn"), - Triple("", "Norwegian", "no"), - Triple("", "Polish", "pl"), - Triple("\uD83C\uDDF5\uD83C\uDDF9", "Portuguese", "pt"), - Triple("", "Romanian", "ro"), - Triple("", "Russian", "ru"), - Triple("", "Swedish", "sv"), - Triple("", "Tamil", "ta"), + 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("", "ಕನ್ನಡ", "kn"), + Triple("", "македонски", "mk"), + Triple("", "മലയാളം", "ml"), + Triple("", "Nederlands", "nl"), + Triple("", "norsk nynorsk", "nn"), + Triple("", "norsk bokmål", "no"), + Triple("", "polski", "pl"), + Triple("\uD83C\uDDF5\uD83C\uDDF9", "português", "pt"), + Triple("🦍", "mmmm... monke", "qt"), + Triple("", "română", "ro"), + Triple("", "русский", "ru"), + Triple("", "slovenčina", "sk"), + Triple("", "Soomaaliga", "so"), + Triple("", "svenska", "sv"), + Triple("", "தமிழ்", "ta"), Triple("", "Tagalog", "tl"), - Triple("", "Turkish", "tr"), - Triple("", "Ukrainian", "uk"), - Triple("", "Urdu", "ur"), - Triple("", "Viet Nam", "vi"), - Triple("", "Chinese Simplified", "zh"), - Triple("\uD83C\uDDF9\uD83C\uDDFC", "Chinese Traditional", "zh-rTW"), + Triple("", "Türkçe", "tr"), + Triple("", "українська", "uk"), + Triple("", "اردو", "ur"), + Triple("", "Tiếng Việt", "vi"), + Triple("", "中文", "zh"), + Triple("\uD83C\uDDF9\uD83C\uDDFC", "文言", "zh-rTW"), /* end language list */ -).sortedBy { it.second } //ye, we go alphabetical, so ppl don't put their lang on top +).sortedBy { it.second?.toLowerCase() } //ye, we go alphabetical, so ppl don't put their lang on top class SettingsGeneral : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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 bd44a058..d328d226 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 @@ -143,7 +143,7 @@ class PluginsFragment : Fragment() { } observe(pluginViewModel.filteredPlugins) { (scrollToTop, list) -> - (plugin_recycler_view?.adapter as? PluginAdapter?)?.updateList(list) + (plugin_recycler_view?.adapter as? PluginAdapter)?.updateList(list) if (scrollToTop) plugin_recycler_view?.scrollToPosition(0) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt index bc9bfb1f..50fb37d6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt @@ -67,7 +67,7 @@ class SetupFragmentLayout : Fragment() { crash_reporting_text?.text = getText(text) } - val enableCrashReporting = !settingsManager.getBoolean(ACRA.PREF_DISABLE_ACRA, false) + val enableCrashReporting = !settingsManager.getBoolean(ACRA.PREF_DISABLE_ACRA, true) acra_switch.isChecked = enableCrashReporting crash_reporting_text.text = getText( diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index d563bffa..00dee9b2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -28,10 +28,12 @@ import androidx.core.text.toSpanned import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.tvprovider.media.tv.* import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor +import androidx.viewpager2.widget.ViewPager2 import com.fasterxml.jackson.module.kotlin.readValue import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState @@ -65,6 +67,7 @@ import okhttp3.Cache import java.io.* import java.net.URL import java.net.URLDecoder +import kotlin.system.measureTimeMillis object AppUtils { fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) { @@ -164,6 +167,18 @@ object AppUtils { return builder.build() } + // https://stackoverflow.com/a/67441735/13746422 + fun ViewPager2.reduceDragSensitivity(f: Int = 4) { + val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView") + recyclerViewField.isAccessible = true + val recyclerView = recyclerViewField.get(this) as RecyclerView + + val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop") + touchSlopField.isAccessible = true + val touchSlop = touchSlopField.get(recyclerView) as Int + touchSlopField.set(recyclerView, touchSlop * f) // "8" was obtained experimentally + } + @SuppressLint("RestrictedApi") fun getAllWatchNextPrograms(context: Context): Set { val COLUMN_WATCH_NEXT_ID_INDEX = 0 @@ -329,6 +344,46 @@ object AppUtils { } } + abstract class DiffAdapter( + open val items: MutableList, + val comparison: (first: T, second: T) -> Boolean = { first, second -> + first.hashCode() == second.hashCode() + } + ) : + RecyclerView.Adapter() { + override fun getItemCount(): Int { + return items.size + } + + fun updateList(newList: List) { + val diffResult = DiffUtil.calculateDiff( + GenericDiffCallback(this.items, newList) + ) + + items.clear() + items.addAll(newList) + + diffResult.dispatchUpdatesTo(this) + } + + inner class GenericDiffCallback( + private val oldList: List, + private val newList: List + ) : + DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + comparison(oldList[oldItemPosition], newList[newItemPosition]) + + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition] == newList[newItemPosition] + } + } + + fun Activity.downloadAllPluginsDialog(repositoryUrl: String, repositoryName: String) { runOnUiThread { val context = this diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index 80e5d64a..2318fda6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -9,6 +9,7 @@ import android.provider.MediaStore import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.WorkerThread import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.module.kotlin.readValue @@ -18,17 +19,17 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.plugins.PLUGINS_KEY import com.lagradost.cloudstream3.plugins.PLUGINS_KEY_LOCAL import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_CACHED_LIST -import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_SHOULD_UPDATE_LIST import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_TOKEN_KEY import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_UNIXTIME_KEY import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_USER_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_CACHED_LIST import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_REFRESH_TOKEN_KEY -import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_SHOULD_UPDATE_LIST import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_TOKEN_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi.Companion.OPEN_SUBTITLES_USER_KEY +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs import com.lagradost.cloudstream3.utils.DataStore.mapper @@ -52,12 +53,10 @@ object BackupUtils { // When sharing backup we do not want to transfer what is essentially the password ANILIST_TOKEN_KEY, ANILIST_CACHED_LIST, - ANILIST_SHOULD_UPDATE_LIST, ANILIST_UNIXTIME_KEY, ANILIST_USER_KEY, MAL_TOKEN_KEY, MAL_REFRESH_TOKEN_KEY, - MAL_SHOULD_UPDATE_LIST, MAL_CACHED_LIST, MAL_UNIXTIME_KEY, MAL_USER_KEY, @@ -121,6 +120,7 @@ object BackupUtils { ) } + @WorkerThread fun Context.restore( backupFile: BackupFile, restoreSettings: Boolean, @@ -223,31 +223,29 @@ object BackupUtils { try { restoreFileSelector = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? -> - this.let { activity -> - uri?.let { - try { - val input = - activity.contentResolver.openInputStream(uri) - ?: return@registerForActivityResult + if (uri == null) return@registerForActivityResult + val activity = this + ioSafe { + try { + val input = activity.contentResolver.openInputStream(uri) + ?: return@ioSafe - val restoredValue = - mapper.readValue(input) - activity.restore( - restoredValue, - restoreSettings = true, - restoreDataStore = true + val restoredValue = + mapper.readValue(input) + + activity.restore( + restoredValue, + restoreSettings = true, + restoreDataStore = true + ) + activity.runOnUiThread { activity.recreate() } + } catch (e: Exception) { + logError(e) + main { // smth can fail in .format + showToast( + activity, + getString(R.string.restore_failed_format).format(e.toString()) ) - activity.recreate() - } catch (e: Exception) { - logError(e) - try { // smth can fail in .format - showToast( - activity, - getString(R.string.restore_failed_format).format(e.toString()) - ) - } catch (e: Exception) { - logError(e) - } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 9174c481..281c9c44 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.utils import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.APIHolder.capitalize import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey @@ -10,6 +11,8 @@ 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 import com.lagradost.cloudstream3.ui.result.VideoWatchState @@ -51,7 +54,20 @@ object DataStoreHelper { @JsonProperty("year") val year: Int?, @JsonProperty("quality") override var quality: SearchQuality? = null, @JsonProperty("posterHeaders") override var posterHeaders: Map? = null, - ) : SearchResponse + ) : SearchResponse { + fun toLibraryItem(id: String): SyncAPI.LibraryItem { + return SyncAPI.LibraryItem( + name, + url, + id, + null, + null, + null, + null, + apiName, type, posterUrl, posterHeaders, quality, this.id + ) + } + } data class ResumeWatchingResult( @JsonProperty("name") override val name: String, @@ -71,6 +87,9 @@ object DataStoreHelper { @JsonProperty("posterHeaders") override var posterHeaders: Map? = null, ) : SearchResponse + /** + * A datastore wide account for future implementations of a multiple account system + **/ private var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION fun getAllWatchStateIds(): List? { @@ -177,6 +196,7 @@ object DataStoreHelper { fun setBookmarkedData(id: Int?, data: BookmarkedData) { if (id == null) return setKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString(), data) + AccountManager.localListApi.requireLibraryRefresh = true } fun getBookmarkedData(id: Int?): BookmarkedData? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index d209a0d4..1ad3639b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -265,8 +265,6 @@ val extractorApis: MutableList = arrayListOf( OkRu(), OkRuHttps(), Okrulink(), - Sendvid(), - SendvidHttps(), // dood extractors DoodCxExtractor(), @@ -293,6 +291,7 @@ val extractorApis: MutableList = arrayListOf( Supervideo(), GuardareStream(), CineGrabber(), + Vanfem(), // StreamSB.kt works // SBPlay(), @@ -323,6 +322,7 @@ val extractorApis: MutableList = arrayListOf( DesuDrive(), Filesim(), + FileMoon(), Linkbox(), Acefile(), SpeedoStream(), @@ -363,6 +363,7 @@ val extractorApis: MutableList = arrayListOf( Cda(), Dailymotion(), + ByteShare(), ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt index 6b6d3928..bc81a5b9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt @@ -91,7 +91,7 @@ class ApkInstaller(private val service: PackageInstallerService) { session.openWrite(context.packageName, 0, size) .use { outputStream -> - val buffer = ByteArray(1024) + val buffer = ByteArray(4 * 1024) var bytesRead = inputStream.read(buffer) while (bytesRead >= 0) { @@ -100,6 +100,7 @@ class ApkInstaller(private val service: PackageInstallerService) { installProgress.invoke(bytesRead) } + session.fsync(outputStream) inputStream.close() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index 7dda3e18..e5f2f2dc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -4,6 +4,7 @@ package com.lagradost.cloudstream3.utils import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.APIHolder.apis //import com.lagradost.cloudstream3.animeproviders.AniflixProvider import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError @@ -78,17 +79,21 @@ object SyncUtil { return null } - suspend fun getUrlsFromId(id: String, type: String = "anilist") : List { - return arrayListOf() - // val url = - // "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json" - // val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).parsed() - // val pages = response.pages ?: return emptyList() - // val current = pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values).mapNotNull { it.url }.toMutableList() - // if(type == "anilist") { // TODO MAKE BETTER - // current.add("${AniflixProvider().mainUrl}/anime/$id") - // } - // return current + suspend fun getUrlsFromId(id: String, type: String = "anilist"): List { + val url = + "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json" + val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).parsed() + val pages = response.pages ?: return emptyList() + val current = + pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values) + .mapNotNull { it.url }.toMutableList() + + if (type == "anilist") { // TODO MAKE BETTER + apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { + current.add("${it.mainUrl}/anime/$id") + } + } + return current } data class SyncPage( diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index 63b3623d..c300d615 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -9,7 +9,9 @@ import android.content.Context import android.content.pm.PackageManager import android.content.res.Configuration import android.content.res.Resources +import android.graphics.Bitmap import android.graphics.Color +import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle import android.view.* @@ -28,15 +30,21 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.graphics.alpha import androidx.core.graphics.blue +import androidx.core.graphics.drawable.toBitmapOrNull import androidx.core.graphics.green import androidx.core.graphics.red import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.NavHostFragment +import androidx.palette.graphics.Palette import androidx.preference.PreferenceManager +import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings @@ -105,7 +113,7 @@ object UIHelper { listView.requestLayout() } - fun Activity?.getSpanCount(): Int? { + fun Context?.getSpanCount(): Int? { val compactView = false val spanCountLandscape = if (compactView) 2 else 6 val spanCountPortrait = if (compactView) 1 else 3 @@ -158,12 +166,27 @@ object UIHelper { return color } + var createPaletteAsyncCache: HashMap = hashMapOf() + fun createPaletteAsync(url: String, bitmap: Bitmap, callback: (Palette) -> Unit) { + createPaletteAsyncCache[url]?.let { palette -> + callback.invoke(palette) + return + } + Palette.from(bitmap).generate { paletteNull -> + paletteNull?.let { palette -> + createPaletteAsyncCache[url] = palette + callback(palette) + } + } + } + fun ImageView?.setImage( url: String?, headers: Map? = null, @DrawableRes errorImageDrawable: Int? = null, - fadeIn: Boolean = true + fadeIn: Boolean = true, + colorCallback: ((Palette) -> Unit)? = null ): Boolean { if (this == null || url.isNullOrBlank()) return false @@ -177,6 +200,33 @@ object UIHelper { else req } + if (colorCallback != null) { + builder.listener(object : RequestListener { + @SuppressLint("CheckResult") + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + resource?.toBitmapOrNull() + ?.let { bitmap -> createPaletteAsync(url, bitmap, colorCallback) } + return false + } + + @SuppressLint("CheckResult") + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + return false + } + }) + } + val res = if (errorImageDrawable != null) builder.error(errorImageDrawable).into(this) else diff --git a/app/src/main/res/color/item_select_color.xml b/app/src/main/res/color/item_select_color.xml index 0d2834dd..3d69c540 100644 --- a/app/src/main/res/color/item_select_color.xml +++ b/app/src/main/res/color/item_select_color.xml @@ -1,5 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_collections_bookmark_24.xml b/app/src/main/res/drawable/ic_baseline_collections_bookmark_24.xml new file mode 100644 index 00000000..fc90e300 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_collections_bookmark_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_sort_24.xml b/app/src/main/res/drawable/ic_baseline_sort_24.xml new file mode 100644 index 00000000..96d46231 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_sort_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_star_24.xml b/app/src/main/res/drawable/ic_baseline_star_24.xml index ab099425..2dcadb7f 100644 --- a/app/src/main/res/drawable/ic_baseline_star_24.xml +++ b/app/src/main/res/drawable/ic_baseline_star_24.xml @@ -1,5 +1,5 @@ - + android:width="12dp" xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/app/src/main/res/drawable/ic_outline_account_circle_24.xml b/app/src/main/res/drawable/ic_outline_account_circle_24.xml new file mode 100644 index 00000000..cc564471 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_account_circle_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/indicator_background.xml b/app/src/main/res/drawable/indicator_background.xml new file mode 100644 index 00000000..ef44fb7c --- /dev/null +++ b/app/src/main/res/drawable/indicator_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/rating_bg_color.xml b/app/src/main/res/drawable/rating_bg_color.xml new file mode 100644 index 00000000..60e62bab --- /dev/null +++ b/app/src/main/res/drawable/rating_bg_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ad29d22a..b6290865 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -35,9 +35,9 @@ --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_downloads.xml b/app/src/main/res/layout/fragment_downloads.xml index f88e39d0..65f36209 100644 --- a/app/src/main/res/layout/fragment_downloads.xml +++ b/app/src/main/res/layout/fragment_downloads.xml @@ -5,7 +5,6 @@ android:id="@+id/download_root" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/primaryGrayBackground" android:orientation="vertical" tools:context=".ui.download.DownloadFragment"> @@ -132,7 +131,8 @@ - + android:orientation="vertical"> @@ -80,7 +79,6 @@ --> + android:text="@string/continue_watching" + app:drawableRightCompat="@drawable/ic_baseline_arrow_forward_24" + app:drawableTint="?attr/white" + android:background="?android:attr/selectableItemBackground" + android:contentDescription="@string/home_more_info"/> - - android:foreground="?android:attr/selectableItemBackgroundBorderless" - android:nextFocusLeft="@id/nav_rail_view" - android:nextFocusUp="@id/home_watch_child_recyclerview" - android:nextFocusForward="@id/home_bookmarked_child_recyclerview" - android:paddingStart="12dp" - android:paddingTop="5dp" - android:paddingEnd="12dp" - - android:paddingBottom="5dp" - android:requiresFadingEdge="horizontal"> - - + android:layout_marginEnd="50dp" + android:fadingEdge="horizontal" - + + - android:nextFocusLeft="@id/nav_rail_view" - android:nextFocusRight="@id/home_plan_to_watch_btt" - android:text="@string/type_watching" /> + - android:nextFocusLeft="@id/home_type_watching_btt" - android:nextFocusRight="@id/home_type_on_hold_btt" - android:text="@string/type_plan_to_watch" /> + - android:nextFocusLeft="@id/home_plan_to_watch_btt" - android:nextFocusRight="@id/home_type_dropped_btt" - android:text="@string/type_on_hold" /> + - android:nextFocusLeft="@id/home_type_on_hold_btt" - android:nextFocusRight="@id/home_type_completed_btt" - android:text="@string/type_dropped" /> + - android:layout_height="wrap_content" - android:nextFocusLeft="@id/home_type_dropped_btt" - android:text="@string/type_completed" /> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 8b9b9e36..afbf735d 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -129,9 +129,9 @@ + android:paddingBottom="100dp" + android:clipToPadding="false" + android:layout_height="wrap_content"> - - - - - - + + + + + diff --git a/app/src/main/res/layout/loading_list.xml b/app/src/main/res/layout/loading_list.xml index ccd4a8d6..1ed01c8e 100644 --- a/app/src/main/res/layout/loading_list.xml +++ b/app/src/main/res/layout/loading_list.xml @@ -1,59 +1,59 @@ + android:layout_width="match_parent" + android:layout_height="200dp" + android:orientation="vertical" + android:paddingTop="@dimen/loading_margin" + android:paddingBottom="@dimen/loading_margin"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + android:layout_width="@dimen/loading_margin" + android:layout_height="wrap_content" /> + android:layout_width="@dimen/loading_margin" + android:layout_height="wrap_content" /> + android:layout_width="@dimen/loading_margin" + android:layout_height="wrap_content" /> + android:layout_width="@dimen/loading_margin" + android:layout_height="wrap_content" /> + android:layout_width="@dimen/loading_margin" + android:layout_height="wrap_content" /> + android:layout_width="@dimen/loading_margin" + android:layout_height="match_parent" /> + android:layout_width="@dimen/loading_margin" + android:layout_height="wrap_content" /> diff --git a/app/src/main/res/layout/loading_poster_dynamic.xml b/app/src/main/res/layout/loading_poster_dynamic.xml new file mode 100644 index 00000000..11855acb --- /dev/null +++ b/app/src/main/res/layout/loading_poster_dynamic.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 691795d3..683a1077 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -96,33 +96,36 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + app:layout_constraintTop_toTopOf="parent"> - + + + + @@ -319,23 +322,23 @@ + tools:text="Skip Opening" + tools:visibility="visible" /> @@ -117,6 +118,7 @@ android:nextFocusLeft="@id/sort_providers" android:nextFocusRight="@id/cancel_btt" android:requiresFadingEdge="vertical" + tools:layout_height="200dp" tools:listfooter="@layout/sort_bottom_footer_add_choice" tools:listitem="@layout/sort_bottom_single_choice" /> diff --git a/app/src/main/res/layout/search_result_grid_expanded.xml b/app/src/main/res/layout/search_result_grid_expanded.xml index 25b3f67d..cf6ab3b2 100644 --- a/app/src/main/res/layout/search_result_grid_expanded.xml +++ b/app/src/main/res/layout/search_result_grid_expanded.xml @@ -5,62 +5,109 @@ android:id="@+id/search_result_root" android:layout_width="match_parent" android:layout_height="wrap_content" - android:clickable="true" android:focusable="true" android:foreground="@drawable/outline_drawable" android:orientation="vertical"> - + android:layout_height="wrap_content"> - - android:layout_height="match_parent" - android:contentDescription="@string/search_poster_img_des" - android:duplicateParentState="true" - android:foreground="?android:attr/selectableItemBackgroundBorderless" - android:scaleType="centerCrop" - tools:src="@drawable/example_poster" /> - - - - + + android:id="@+id/text_quality" + style="@style/TypeButton" /> - - - + + + + + + + + + + + + + + + + - - + + - \ No newline at end of file + diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index 3a5e0929..cb620bb8 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -1,20 +1,23 @@ - + android:id="@+id/navigation_home" + android:icon="@drawable/home_alt" + android:title="@string/title_home" /> + android:id="@+id/navigation_search" + android:icon="@drawable/search_icon" + android:title="@string/title_search" /> + android:id="@+id/navigation_library" + android:icon="@drawable/ic_outline_account_circle_24" + android:title="@string/library" /> + android:id="@+id/navigation_downloads" + android:icon="@drawable/netflix_download" + android:title="@string/title_downloads" /> + \ No newline at end of file diff --git a/app/src/main/res/menu/library_menu.xml b/app/src/main/res/menu/library_menu.xml new file mode 100644 index 00000000..f21d998d --- /dev/null +++ b/app/src/main/res/menu/library_menu.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 14d750a0..d71eeb06 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -144,6 +144,15 @@ app:popEnterAnim="@anim/enter_anim" app:popExitAnim="@anim/exit_anim" /> + + حذف مزيد من المعلومات قد تكون هناك حاجة إلى VPN لكي يعمل هذا المزود بشكل صحيح - هذا المزود هو تورنت ، يوصى باستخدام شبكة ظاهرية خاصة + هذا المزود هو تورنت ، يوصى باستخدام شبكة ظاهرية خاصة لا يتم توفير البيانات الوصفية بواسطة الموقع ، وسيفشل تحميل الفيديو إذا لم يكن موجودًا في الموقع. الوصف لم يتم العثور على وصف @@ -184,8 +184,10 @@ إستئناف -٣٠ +٣٠ - سوف يتم الحذف نهائيا %s\nهل أنت متأكد? - %dm\nمتبقية + سوف يتم الحذف نهائيا %s +\nهل أنت متأكد\? + %dm +\nمتبقية جاري التنفيذ اكتمل الحالة @@ -233,7 +235,7 @@ مرآة كروم كاست تشغيل في التطبيق %s تشغيل في - تشغيل في الويب + تشغيل في الويب نسخ الرابط التحميل التلقائي تحميل بجودات مختلفة @@ -279,22 +281,7 @@ تكبير إخلاء مسؤولية 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. - + 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. عام زر العشوائي إظهار زر العشوائي على الصفحة الرئيسية @@ -345,7 +332,7 @@ مزامنة تقييم %d / 10 - /?? + /\?\? /%d %s تم التوثيق تعذر تسجيل الدخول في %s @@ -355,7 +342,7 @@ الكل الحد الاقصي الحد الأدنى - @string/none + \@string/none الخطوط المحيطة النمط المنخفض ظل @@ -452,7 +439,7 @@ عرض مستودعات المجتمع قائمة عامة جميع الترجمات حروف كبيرة - تحميل جميع الإضافات من هذا المستودع? + تحميل جميع الإضافات من هذا المستودع\? %s (معطل) المسارات مسار الصوت @@ -533,4 +520,27 @@ سيتم تحديث التطبيق عند الخروج بدأ التحديث تم تنزيل الإضافة + إزالة من المشاهدة + الترتيب الأبجدي (من الألف إلى الياء) + اختر المكتبة + المتصفح + محدث (من الأحدث إلى الأقدم) + يبدو أن هذه القائمة فارغة ، حاول التبديل إلى قائمة أخرى + التقييم (من الأعلى إلى الأدنى) + التقييم (من الأدنى إلى الأعلى) + الترتيب الأبجدي (من ي إلى أ) + يبدو أن مكتبتك فارغة :( +\nتسجيل الدخول إلى حساب مكتبة أو إضافة عروض إلى مكتبتك المحلية + محدث (من القديم إلى الجديد) + فرز حسب + افرز + فتح بواسطة + المكتبة + تم العثور على ملف الوضع الآمن! +\nلا يتم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف. + مدة التقديم عنما يكون المشغل مخفيا + مدة التقديم - المشغل مخفي + تلفزيون أندرويد + مدة التقديم عنما يكون المشغل مرئيا + مدة التقديم- المشغل المرئي \ No newline at end of file diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 0fdda385..f1f512a1 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -8,7 +8,6 @@ %dh %dm %dm Poster - \@string/result_poster_img_des Episode Poster Main Poster Следващ произволен @@ -17,7 +16,8 @@ Визуализация на фона Скорост (%.2fx) Оценка: %.1f - Намерена е нова актуализация!\n%s -> %s + Намерена е нова актуализация! +\n%s -> %s Шаблон %d мин CloudStream @@ -105,7 +105,7 @@ Продължете да гледате Премахване Повече информация - @string/home_play + \@string/home_play Може да е необходим VPN, за да работи правилно този доставчик Този доставчик е торент, препоръчва се VPN Метаданните не се предоставят от сайта, зареждането на видео ще бъде неуспешно, ако не съществува на сайта. @@ -135,8 +135,7 @@ Докоснете два пъти от дясната или лявата страна, за да превъртите напред или назад Докоснете в средата, за да направите пауза Използвайте яркостта на системата - Използвайте системна яркост в плейъра на приложението вместо тъмно - наслагване + Използвайте системна яркост в плейъра на приложението вместо тъмно наслагване Актуализирайте прогреса на гледане Автоматично синхронизирайте прогреса на текущия си епизод Възстановете данните от архив @@ -175,8 +174,7 @@ Връзката е копирана в клипборда Пусни епизода Възстановяване на стойността по подразбиране - За съжаление приложението се срина. Анонимен доклад за грешка ще бъде изпратен до - разработчиците + За съжаление приложението се срина. Анонимен доклад за грешка ще бъде изпратен до разработчиците Сезон %s %d%s Без сезон @@ -193,8 +191,10 @@ Продължи -30 30 - Това ще изтрие за постоянно %s\nСигурни ли сте? - %dm\nостава + Това ще изтрие за постоянно %s +\nСигурни ли сте\? + %dm +\nостава Продължава Завършен Статус @@ -223,8 +223,8 @@ Филм Серия Анимационен филм - @string/anime - @string/ova + \@string/anime + \@string/ova Торент Документален филм Азиатска драма @@ -320,7 +320,7 @@ Синхронизиране Оценен %d / 10 - /?? + /\?\? /%d %s удостоверен Не можах да вляза в %s @@ -426,7 +426,7 @@ Вижте хранилищата на общността Публичен списък Всички субтитри с главни букви - Изтегляне на всички добавки от това хранилище? + Изтегляне на всички добавки от това хранилище\? %s (Деактивиран) Потоци Аудио потоци @@ -496,4 +496,5 @@ Приставката е изтеглена Приложението ще се актуализира при изход от него Започна Актуализация + Премахване от гледани \ No newline at end of file diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index ebc3ecd2..2c2e1303 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -10,7 +10,7 @@ %dm Poster - @string/result_poster_img_des + \@string/result_poster_img_des Episode Poster Main Poster Next Random @@ -20,7 +20,8 @@ Velocidade (%.2fx) Nota: %.1f - Nova atualização encontrada!\n%s -> %s + Nova atualização encontrada! +\n%s -> %s Filler %d min CloudStream @@ -107,7 +108,7 @@ Continue Assistindo Remover Mais Info - @string/home_play + \@string/home_play Uma VPN pode ser necessária para esse fornecedor funcionar corretamente Esse fornecedor é um torrent, uma VPN é recomendada Metadados não são oferecidas pelo site, o carregamento do video pode falhar se ele não existir no site. @@ -174,9 +175,7 @@ Link copiado para área de transferência Assistir Episódio Restaurar para o padrão - Desculpe, a aplicação travou. Um relatório de erro anônimo será enviado para os - desenvolvedores - + Desculpe, a aplicação travou. Um relatório de erro anônimo será enviado para os desenvolvedores Temporada Nenhuma Temporada Episódio @@ -190,8 +189,10 @@ Retomar -30 +30 - Isso apagará %s permanentemente\nVocê tem certeza? - %dm\nsobrando + Isso apagará %s permanentemente +\nVocê tem certeza\? + %dm +\nsobrando Em andamento Concluído Estado @@ -221,8 +222,8 @@ Filme Série Desenho Animado - @string/anime - @string/ova + \@string/anime + \@string/ova Torrent Documentário Drama Asiático @@ -277,22 +278,7 @@ Esticar Zoom Aviso Legal - 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. - + 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. Geral Botão Aleatório Mostra o botão Aleatório na página inicial @@ -337,7 +323,7 @@ Sincronizar Nota %d / 10 - /?? + /\?\? /%d %s autenticado Falha em autenticar para %s @@ -436,7 +422,7 @@ Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas - Transferir todos os plugins deste repositório? + Transferir todos os plugins deste repositório\? %s (Desativado) Reproduzir automaticamente próximo episódio Começa o próximo episódio quando o atual termina diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 5a2d44d4..a78da8a4 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -6,7 +6,7 @@ Hrají: %s Plakát - @string/result_poster_img_des + Plakát Episode Poster Main Poster Next Random @@ -16,7 +16,8 @@ Rychlost (%.2fx) Hodnocení: %.1f - Nalezena nová aktualizace!\n%s -> %s + Nalezena nová aktualizace! +\n%s -> %s Výplň %d min CloudStream @@ -102,7 +103,7 @@ Pokračovat ve sledování Odebrat Další informace - @string/home_play + \@string/home_play Aby tento poskytovatel fungoval správně, budete možná potřebovat VPN Tento poskytovatel je torrent, je doporučená VPN Web neposkytnul žádná metadata, načítání videa selže, pokud na webu neexistuje. @@ -127,21 +128,18 @@ Dvojité klepnutí pro posun Dvojité klepnutí pro pozastavení Množství času k posunu - Klepněte dvakrát vpravo nebo vlevo pro posun vpřed nebo vzad - + Klepněte dvakrát vpravo nebo vlevo pro posun vpřed nebo vzad Klepněte doprostřed pro pozastavení Použít systémový jas - V přehrávači použít systémov - překrytí - + V přehrávači použít systémov překrytí Aktualizovat postup sledování Automaticky synchronizovat postup sledování současné epizody Obnovit data ze zálohy Zálohovat data Načten soubor zálohy Nepodařilo se obnovit data ze soubory %s - Data úspěšně uložena - Chybí oprávnění k úložišti, zkuste to prosím znovu + Data uložena + Chybí oprávnění k úložišti. Zkuste to prosím znovu. Chyba při zálohování %s Search Účty @@ -168,9 +166,7 @@ Odkaz zkopírován do schránky Přehrát epizodu Obnovit na výchozí hodnoty - Omlouváme se, aplikace spadla. Vývojářům bude odesláno anonymní hlášení - o pádu - + Omlouváme se, aplikace spadla. Vývojářům bude odesláno anonymní hlášení o pádu Sezóna Žádná sezóna Epizoda @@ -184,8 +180,10 @@ Pokračovat -30 +30 - Toto nevratně smaže %s\nJste si jisti? - %dm\nzbývá + Toto nevratně smaže %s +\nJste si jisti\? + %dm +\nzbývá Probíhající Dokončena Stav @@ -213,8 +211,8 @@ Film Seriál Animovaný - @string/anime - @string/ova + \@string/anime + \@string/ova Torrent Dokument Asijské drama @@ -254,33 +252,18 @@ Délka vyrovnávací paměti videa Mezipaměť videa na disku Vymazat mezipamět videí a obrázků - Při nastavení příliš vysoké hodnoty způsobí náhodné pády. Neměňte, pokud máte málo paměti RAM, například u televize s Androidem nebo starého telefonu. - Pokud ji nastavíte příliš vysoko, může způsobit problémy v systémech s malým úložným prostorem, například v zařízeních Android TV. + Při nastavení příliš vysoké hodnoty na zařízeních s malou pamětí, jako je například Android TV, může způsobit pády. + Při nastavení příliš vysoké hodnoty na zařízeních s malým dostupným úložištěm, jako je například Android TV, může způsobit pády. DNS přes HTTPS Užitečné pro obcházení blokací ISP Cesta stahování - URL serveru Nginx + URL serveru NGINX Zobrazit dabované anime/anime s titulky Vyplnit na obrazovku Roztáhnout Přiblížit Odmítnutí odpovědnosti - Jakékoli právní otázky týkající se obsahu této aplikace - je třeba řešit se samotnými hostiteli a poskytovateli souborů, protože s nimi nejsme nijak spojeni. - - V případě porušení autorských práv se obraťte přímo na odpovědné strany nebo na webové stránky, na kterých se streamování odehrává. - - Aplikace je určena výhradně pro vzdělávací a osobní účely. - - CloudStream 3 v aplikaci nehostuje žádný obsah a nemá žádnou kontrolu nad tím, jaká média jsou v aplikaci umístěna nebo odstraněna. - CloudStream 3 funguje jako jakýkoli jiný vyhledávač, například Google. Služba CloudStream 3 nehostuje, nenahrává ani - nespravuje žádná videa, filmy ani obsah. Pouze vyhledává, agreguje a zobrazuje odkazy v pohodlném, - uživatelsky přívětivém rozhraní. - - Pouze shromažďuje webové stránky třetích stran, které jsou veřejně přístupné prostřednictvím jakéhokoli běžného webového prohlížeče. Je - odpovědností uživatele, aby se vyvaroval jakýchkoli akcí, které by mohly porušovat zákony platné v jeho lokalitě. Použijte - CloudStream 3 na vlastní nebezpečí. - + Jakékoli právní otázky týkající se obsahu této aplikace je třeba řešit se samotnými hostiteli a poskytovateli souborů, protože s nimi nejsme nijak spojeni. V případě porušení autorských práv se obraťte přímo na odpovědné strany nebo na webové stránky, na kterých se streamování odehrává. Aplikace je určena výhradně pro vzdělávací a osobní účely. CloudStream 3 v aplikaci nehostuje žádný obsah a nemá žádnou kontrolu nad tím, jaká média jsou v aplikaci umístěna nebo odstraněna. CloudStream 3 funguje jako jakýkoli jiný vyhledávač, například Google. Služba CloudStream 3 nehostuje, nenahrává ani nespravuje žádná videa, filmy ani obsah. Pouze vyhledává, agreguje a zobrazuje odkazy v pohodlném, uživatelsky přívětivém rozhraní. Pouze shromažďuje webové stránky třetích stran, které jsou veřejně přístupné prostřednictvím jakéhokoli běžného webového prohlížeče. Je odpovědností uživatele, aby se vyvaroval jakýchkoli akcí, které by mohly porušovat zákony platné v jeho lokalitě. Použijte CloudStream 3 na vlastní nebezpečí. Obecné Náhodné tlačítko Zobrazit na domovské stránce náhodné tlačítko @@ -322,7 +305,7 @@ Synchronizovat Hodnoceno %d / 10 - /?? + /\?\? /%d Přihlášeno k %s Nepodařilo se přihlásit k %s @@ -337,10 +320,10 @@ Stín Vyvýšené Synch. titulky - 1000ms + 1000 ms Zpoždění titulků - Toto použijte, pokud jsou titulky zobrazeny o %dms dříve - Toto použijte, pokud jsou titulky zobrazeny o %dms později + Toto použijte, pokud jsou titulky zobrazeny o %d ms dříve + Toto použijte, pokud jsou titulky zobrazeny o %d ms později Žádné zpoždění titulků रफ्तार (%.2fx) - नया अपडेट आया है!\n%s -> %s + नया अपडेट आया है! +\n%s -> %s होम खोजें डाउनलोड @@ -69,8 +70,7 @@ प्लेयर में वीडियो की रफ्तार धिमी या तेज़ करता है दाएं या बाएं तरफ स्वाइप करने से वीडियो को आगे पीछे करता है दाएं तरफ या बाएं तरफ स्वाइप करने से रोशिनी और आवाज़ को ऊपर नीचे करता है - दो बार दाएं या बाएं तरफ दबाने से वीडियो को आगे या पीछे करा जा सकता है - + दो बार दाएं या बाएं तरफ दबाने से वीडियो को आगे या पीछे करा जा सकता है खोजें जानकारी नतीजों को सूत्रों के हिसाब से बांटकर दिखता है @@ -88,13 +88,13 @@ कोई लिंक नही मिले लिंक को क्लिपबोर्ड पे कॉपी करदिया गया चलाये - असुविधा के लिए खेद है, यह एप्प क्रैश हो गया है । एक गुमनाम रिपोर्ट निर्माताओं को भेज दी गयी है । - + असुविधा के लिए खेद है, यह एप्प क्रैश हो गया है । एक गुमनाम रिपोर्ट निर्माताओं को भेज दी गयी है । फ़ाइल डिलीट करें डिलीट रोके वापिस चलाये - एज इस चीज़ को हमेशा के लिए नष्ट कर देगा %s\nक्या आपका निर्णय निश्चित है ? + एज इस चीज़ को हमेशा के लिए नष्ट कर देगा %s +\nक्या आपका निर्णय निश्चित है \? अभी चालू है खत्म हो गया है स्तिथि @@ -132,8 +132,8 @@ ISP ब्लॉक से छुटकारा पाएं सूत्र की भाषाएं ऐप का लेआउट - पसंदीदा मीडिया - डाउनलोड करने का मार्ग + पसंदीदा मीडिया + डाउनलोड करने का मार्ग Dubbed या Subbed Anime दिखाये टीवी लेआउट फ़ोन लेआउट diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index accb625d..0f3e36bc 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -19,7 +19,7 @@ %dm Poster - @string/result_poster_img_des + Poster Episode Poster Main Poster Next Random @@ -29,7 +29,8 @@ Brzina (%.2fx) Ocijenjeno: %.1f - Pronađeno novo ažuriranje!\n%s -> %s + Pronađeno novo ažuriranje! +\n%s -> %s Umetak %d min CloudStream @@ -118,7 +119,7 @@ Nastavite s gledanjem Makni Više informacija - @string/home_play + \@string/home_play Za ispravan rad ovog pružatelja usluga može biti potreban VPN Ovaj pružatelj usluga je torrent, preporučuje se VPN Stranica ne daje metapodatke, učitavanje videozapisa neće uspjeti ako ne postoji na stranici. @@ -145,13 +146,10 @@ Dodirni dvaput za traženje Dodirni dvaput za pauziranje Iznos preskakanja u playeru - Dvaput dodirni desnu ili lijevu stranu ekrana za pomicanje naprijed ili natrag - + Dvaput dodirni desnu ili lijevu stranu ekrana za pomicanje naprijed ili natrag Dodirni u sredinu zaslona za pauziranje Koristi svijetlinu u sustavu - Koristi svjetlinu sustava u playeru aplikacija umjesto tamnog - preklopa - + Koristi svjetlinu sustava u playeru aplikacija umjesto tamnog preklopa Ažuriraj napredak gledanja Automatski sinkroniziraj svoj trenutni napredak u epizodi Vraćanje podataka iz sigurnosne kopije @@ -190,8 +188,7 @@ Veza je kopirana u međuspremnik Pokreni epizodu Vrati na zadanu vrijednost - Nažalost, aplikacija se srušila. Anonimno izvješće o bugu bit će poslano developerima - + Nažalost, aplikacija se srušila. Anonimno izvješće o bugu bit će poslano developerima Sezona Nema sezone Epizoda @@ -207,8 +204,10 @@ Nastavi -30 +30 - Ovo će trajno izbrisati %s\nJeste li sigurni? - %dm\npreostalo + Ovo će trajno izbrisati %s +\nJeste li sigurni\? + %dm +\npreostalo U tijeku Završeno Status @@ -239,8 +238,8 @@ Film Serija Crtić - @string/anime - @string/ova + \@string/anime + \@string/ova Torrent Dokumentarac Azijska drama @@ -297,22 +296,7 @@ Rastegni Zoom Obavijest - 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. - + 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. Općenito Random gumb Prikaži random gumb na početnoj stranici @@ -351,7 +335,7 @@ Sinkroniziraj Ocijenjeno %d / 10 - /?? + /\?\? /%d Ovjereno%s Nije moguće prijaviti se na %s @@ -457,7 +441,7 @@ Pregledajte repozitorije zajednice Javni popis Svi titlovi pisani velikim slovima - Preuzeti sve dodatke iz ovog repozitorija? + Preuzeti sve dodatke iz ovog repozitorija\? %s (Onemogućeno) Zapis Audio zapis @@ -529,4 +513,22 @@ Aktualiziranje započeto Program če se aktualizirati tijekom zatvaranja programa Dodatak preuzet + Ukloni iz pogledanog + Preglednik + Biblioteka + Ocjena (Veća do manje) + Sortiraj prema + Sortiraj + Ocjena (Manja do veće) + Ažurirano (Od novog do starog) + Ažurirano (Od starog do novog) + Abecedno (A do Ž) + Abecedno (Ž do A) + Odaberite biblioteku + Otvori sa + Čini se da vam je biblioteka prazna :( +\nPrijavite se na račun biblioteke ili dodajte serije u svoju lokalnu biblioteku + Čini se da je ova lista prazna, pokušajte se prebaciti na drugu + Pronađena datoteka sigurnog načina rada! +\nNe učitavaju se ekstenzije pri pokretanju dok se datoteka ne ukloni. \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 5e515c56..d54e4fa9 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -5,7 +5,7 @@ Pemeran: %s Poster - @string/result_poster_img_des + Poster Episode Poster Main Poster Next Random @@ -15,7 +15,8 @@ Kecepatan (%.2fx) Dinilai: %.1f - Update baru ditemukan!\n%s -> %s + Update baru ditemukan! +\n%s -> %s Filler %d mnt CloudStream @@ -100,7 +101,7 @@ Lanjutkan Menonton Hapus Info lebih lanjut - @string/home_play + \@string/home_play Sebuah VPN mungkin diperlukan agar provider ini bisa bekerja dengan benar Provider ini adalah sebuah torrent, VPN direkomendasikan Metadata tidak disediakan oleh situs, loading video akan gagal jika tidak ada di situs. @@ -125,12 +126,10 @@ Tekan dua kali untuk mengubah waktu Tekan dua kali untuk menjeda Jumlah pengubah waktu pemutar - Tekan dua kali di sisi kanan atau kiri untuk mengubah waktu ke depan atau ke belakang - + Tekan dua kali di sisi kanan atau kiri untuk mengubah waktu ke depan atau ke belakang Tekan di tengah untuk menjeda Gunakan pencerahan sistem - Gunakan pencerahan sistem di pemutar aplikasi dari pada hamparan gelap - + Gunakan pencerahan sistem di pemutar aplikasi dari pada hamparan gelap Update progres tontonan Sinkronisasikan progres episode saat ini secara otomatis Pulihkan data dari cadangan @@ -165,8 +164,7 @@ Tautan disalin ke papan klip Putar Episode Ulang ke pengaturan default - Maaf, aplikasi ngecrash. Laporan bug anonim akan dikirim ke developer - + Maaf, aplikasi ngecrash. Laporan bug anonim akan dikirim ke developer Season Tidak Ada Season Episode @@ -180,8 +178,10 @@ Lanjutkan -30 +30 - Ini akan secara permanen menghapus %s\nApakah anda yakin? - %dm\ntersisa + Ini akan secara permanen menghapus %s +\nApakah anda yakin\? + %dm +\ntersisa Masih Berlanjut Tamat Status @@ -209,8 +209,8 @@ Movie Seri Kartun - @string/anime - @string/ova + \@string/anime + \@string/ova Torrent Film Dokumenter Drama Asia @@ -261,22 +261,7 @@ Regang Zoom Peringatan - 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. - + 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. Umum Tombol Acak Tampilkan tombol acak di Beranda @@ -312,7 +297,7 @@ Sinkronisasi Skor %d / 10 - /?? + /\?\? /%d %s terautentikasi Gagal masuk ke %s @@ -468,7 +453,7 @@ Semua fitur tambahkan dimatikan karena crash, untuk memudahkanmu mencari penyebab crash. Kode bahasa (en) Ambil dari internet - Putar vidio di bahasa ini + Putar video di bahasa ini Tambah Repositori Pilih ini untuk menghapus semua repositori plugin Lewati pengaturan @@ -498,7 +483,7 @@ Gerakan Beberapa perangkat tidak mendukung penginstal paket mode baru. Coba mode lama jika pembaruan tidak dapat diinstal. Aksi - Sumber + Referer Ya Pasang dulu fitur tambahan Semua Bahasa @@ -526,4 +511,22 @@ Plugin Terunduh Aplikasi akan diperbaharui pada saat keluar Pembaharuan Dimulai + Hapus dari tontonan + Browser + Pilih pustaka + Yahh daftar pustaka kamu kosong :( +\nMasuk ke akun pustaka atau tambah perlihatkan ke lokal pustaka kamu + Pustaka + Urutkan berdasar + Urutkan + Peringkat (Rendah ke Tinggi) + Update (Lama ke Terbaru) + Peringkat (Tinggi ke Rendah) + Update (Terbaru ke Lama) + Abjad (A ke Z) + Abjad (Z ke A) + Buka dengan + Yahh daftar ini kosong, coba ganti ke yang lain + Mode aman file ditemukan! +\nTidak memuat ekstensi pada startup sampai berkas dihapus. \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f27f7c13..9dbc627f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -9,7 +9,7 @@ %d min Poster - @string/result_poster_img_des + Poster Poster episodio Poster principale Prossimo casuale @@ -19,7 +19,8 @@ Velocità (%.2fx) Valutato: %.1f - Nuovo aggiornamento trovato!\n%s -> %s + Nuovo aggiornamento trovato! +\n%s -> %s Filler %d min @@ -107,7 +108,7 @@ Continua a guardare Rimuovi Più info - @string/home_play + \@string/home_play Potrebbe essere necessaria una VPN per far funzionare correttamente questo provider Questo provider è un torrent, si raccomanda una VPN I metadati non sono forniti dal sito, il caricamento del video fallirà se non esiste sul sito. @@ -193,8 +194,10 @@ Riprendi -30 +30 - Stai per eliminare permanentemente %s\nSei sicuro? - %dm\nrimanenti + Stai per eliminare permanentemente %s +\nSei sicuro\? + %dm +\nrimanenti In corso Completato Stato @@ -321,7 +324,7 @@ Sincronizza Valutato %d / 10 - /?? + /\?\? /%d %s autenticato Impossibile autenticarsi a %s @@ -428,7 +431,7 @@ Vedi le repository della community Lista pubblica Tutti i sottotitoli in maiuscolo - Scaricare tutti i plugin da questa repository? + Scaricare tutti i plugin da questa repository\? %s (Disabilitato) Tracce Traccia audio @@ -507,4 +510,22 @@ L\'app verrà aggiornata all\'uscita Aggiornamento avviato Plugin scaricato + Rimuovi dai già visti + Browser + Ordina per + Punteggio (Decrescente) + Punteggio (Crescente) + Aggiornato (Da nuovo a vecchio) + Aggiornato (Da vecchio a nuovo) + Alfabetico (A - Z) + Alfabetico (Z - A) + Sembra che la tua libreria sia vuota :( +\nAccedi a un account di libreria o aggiungi degli show alla tua libreria locale + Seleziona libreria + Apri con + Libreria + Ordina + Sembra che questa lista sia vuota, prova a passare a un\'altra + File \"safe mode\" trovato! +\nAll\'avvio non sarà caricata alcuna estensione finchè il file non verrà rimosso. \ No newline at end of file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index cb8d5945..11cf77ce 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -34,13 +34,13 @@ הושלם בתכנון לצפייה צופה מחדש - נגן סרט - נגן קדימון - נגן שידור חי + הפעל סרט + הפעל טריילר + הפעל שידור חי מקורות נסיון חדש לחיבור… - נעצר - שידור טורנט + הפסיק + שדר טורנט חזרה נגן פרק הורד @@ -75,7 +75,7 @@ נגן עם קלאודסטרים פרק הבא טוען… - צופה ב + צופה כתוביות בהמתנה ללא @@ -102,11 +102,11 @@ הספק הזה הוא טורנט, שימוש ב-VPN הוא מומלץ תיאור עלילה לא נמצאה - העלילה לא נמצאה + התיאור לא נמצאה מצב תמונה בתמונה כפתור שינוי גודל הנגן הסר את הגבולות השחורים - %d ניתן למפתחים + %d בנינים שניתנו למפתחים הגדרות כתוביות הנגן הגדרות כתוביות כרומקאסט גופן @@ -129,4 +129,381 @@ אין הכל כותרת + העדכון התחיל + מידע + התקן אוטומטית את כל התוספים שטרם הותקנו ממאגרים שנוספו. + חפש אוטומטית עדכונים חדשים בפתיחת האפליקציה + הצג עדכונים לאפליקציה + בצע מחדש את תהליך ההגדרה + עדכן למהדורות מוקדמות + חפש עדכוני מהדרות מוקדמות במקום מהדורות מלאות בלבד + פרקים + %d-%d + פרק + %d %s + עו + פר + לא נמצאו פרקים + מחק קובץ + מחק + עצור + המשך + -30 + +30 + %dדקות +\nנותרו + ‬פעולה זאת תמחק לצמיתות את %s +\nהאם אתם בטוחים\? + מתמשך + משך זמן + דירוג + שנה + ללא כתוביות + ברירת מחדל + חינם + משומש + סדרת טלוויזיה + סדרות/סרטים מצוירים + \@string/אנימה + \@string/אנימציית וידאו מקורית + דרמה אסייתית + כרומקאסט את הפרק + כרומקאסט את המראה + נגן בדפדפן + תווית כתוביות + החלף רכיבי ממשק משתמש בפוסטר + דלג על הפתיח + אל תראה שוב + דלג על עדכון זה + עדכון + איכות צפייה מועדפת + נגן וידאו כותרת מקסימום תווים + רזולוציית נגן וידאו + הוסף מעקב + צור חשבון + הבא + ספריה + מטא-דאטה לא מסופק על ידי האתר, טעינת הסרטון תיכשל אם הוא לא קיים באתר. + החלק על הצד השמאלי או הימני כדי לשנות את הבהירות או עוצמת הקול + שחזור הנתונים מהקובץ נכשל %s + עדכונים וגיבויים + נותן לך את תוצאות החיפוש מופרדות לפי ספק + הצג פרק פילר (לא הכרחי לעלילה) לאנימה + הורדה אוטומטית של תוספים + עדכוני תוספים אוטומטיים + כמות בנינים שניתנו + עונה + מצטערים, האפליקציה קרסה. דוח באג אנונימי יישלח למפתחים + טורנט + NSFW + שגיאת מעבד + הורד מראה + הורד כתוביות + הצג כפתור אקראי בדף הבית + תווית דיבוב + גורם לקריסות אם מוגדר גבוה מדי במכשירים עם זיכרון נמוך, כגון Android TV. + מקור + ספקים + הסר אתר + נתיב הורדה + נראה + אוטומטי + פריסת טלוויזיה + %s %s + סנן לפי שפת מדיה מועדפת + נושא אפליקציה + מיקום כותרת פוסטר + מה אתם רוצים לראות + איכות גבוהה + HD + SDR + תוסף טעון + DVD + עדכוני אפליקציה + 4K + WP + דלג על ההגדרה + תכונות נגן + עדכן התקדמות צפייה + DNS מעל HTTPS + לחץ באמצע כדי לעצור + החלק שמאלה או ימינה כדי לשלוט על זמן בנגן הסרטונים + נגן אוטומטית את הפרק הבא + התחל את הפרק הבא כאשר הפרק הנוכחי נגמר + לחץ פעמיים על צד ימין או שמאל כדי להציץ קדימה או אחורה + השתמש בבהירות המערכת + גבה נתונים + סנכרן את הכתוביות + עיכוב כתוביות + אין עיכוב בכתוביות + מומלץ + נטען %s + השתמש בזה אם הכתוביות מוצגות %d מילישניות מוקדם יותר + אקראי + רזולוציה + איכות מצלמה + כתובת אתר לא מזוהה + קישור לשידור + המפנה + TC + SD + UHD + כתובת אתר למאגר + מדיה מועדפת + /%d + נגן את הפרק + לא נמצאו קישורים + קוד שפה (אנגלית) + שגיאה + שפות ספק + קישורים + פריסת אפליקציה + חיפוש מתקדם + 18+ + פוסטר + איכות מצלמה + החלק כדי להציץ + גיבוי + כמות הצצת הנגן + סרטון + גיטהאב + מצלמה + החלק לשינוי ההגדרות + ‪דפדפן + צבע חלון + הצג לוג + הוסף אפשרות מהירות בנגן + לחץ פעמיים כדי להציץ + לחץ פעמיים כדי לעצור + התשתמש בבהירות המערכת בנגן האפליקציה במקום שכבת-על כהה + סנכרן אוטומטית את התקדמות הפרק הנוכחית שלך + שחזר נתונים מגיבוי + קובץ הגיבוי נטען + נתונים מאוחסנים + חסרות הרשאות אחסון. אנא נסה שוב. + שגיאה בגיבוי %s + חיפוש + חשבונות + לא ניתנו בנינים + מצב איגנגראבי + שולח נתונים רק בקריסות + לא שולח נתונים + הצג טריילרים + הצג פוסטרים מKitsu + הסתר את איכות הסרטון שנבחרה בתוצאות החיפוש + מתקין APK + חלק מהטלפונים אינם תומכים במתקין החבילות החדש. נסו את האפשרות מדור קודם אם העדכונים לא מתקינים. + אפליקצית רומנים קלים על ידי אותו מפתחים + אפליקצית אנימה על ידי אותו מפתחים + הצטרפות לדיסקורד + שפת האפליקציה + לספק זה אין תמיכה בכרומקאסט + הקישור הועתק ללוח + אפס לערך ברירת המחדל + אין עונה + הושלם + אתר + תקציר + בתור + לתת בניני למפתחים + %s %d%s + אפליקציה + סרטים + אנימה + סרטים תיעודיים + אנימציית וידאו מקורית + דרמות אסייתיות + שידורי חי + אחר + סרט + סדרה + סדרה/סרט מצויר + טורנט + דוקומנטרי + שידור חי + NSFW + שגיאת מקור + שגיאה מרחוק + שגיאת נגן לא צפויה + שגיאת הורדה, בדוק הרשאות אחסון + נגן באפליקציה + נגן ב %s + העתק קישור + הורדה אוטומטית + טען מחדש קישורים + תווית איכות + לא נמצא עדכון + בדוק אם יש עדכון + נעל + שינוי גודל + גודל מאגר וידאו + אורך מאגר וידאו + מטמון וידאו בכונן + נקה מטמון וידאו ותמונה + גורם לקריסות אם מוגדר גבוה מדי במכשירים עם מקום נמוך, כגון Android TV. + שימושי עבור עקיפת מחסומי ספק שירותי אינטרנט + אתר העתק + הוסף העתק של אתר קיים, עם כתובת אתר אחרת + NGINX שרת כתובת אתר + הצג אנימות מדובבות/מתורגמות + התאם למסך + מתוח + זום + כתב ויתור + הרחבות + פעולות + מטמון + מחוות + כתוביות + פריסה + ברירות מחדל + תכונות + כללי + כפתור אקראי + הרשה NSFW בספקים נתמכים + קידוד כתוביות + פריסה + פריסת טלפון + פריסת אמולטור + צבע ראשוני + שים את הכותרת מתחת לפוסטר + סיסמה123 + שםהמשתמשהמגניבשלי + hello@world.com + 127.0.0.1 + האתרהמגניבשלי + דוגמה.com + חשבון + החלף חשבון + הוסף חשבון + נוסף %s + סנכרן + מדורג + %d / 10 + /\?\? + %s מאומת + לא ניתן להתחבר ב %s + רגיל + מקסימום + מינימום + מתאר + מדוכא + צל + הורם + 1000 מילישניות + השתמש בזה אם הכתוביות מוצגות %d מילישניות מאוחר יותר + השועל החום המהיר קופץ מעל הכלב העצלן + טען מקובץ + טען מהאינטרנט + קובץ שהורד + ראשי + משנה + רקע + מקור + בקרוב… + TS + Blu-ray + HDR + Web + תמונת פוסטר + נגן + רזולוציה וכותרת + לא מזוהה ID + נתונים לא מזוהים + הסר כיתובים סגורים מהכתוביות + הסר נפיחות מכתוביות + תוספות + טריילר + צפה בסרטונים בשפות אלה + קודם + שנה את מראה האפליקציה כך שיתאים למכשירך + דיווח על קריסה + סוים + הרחבות + הוסף מאגר + שם מאגר + תוסף הורד + התוסף נמחק + לא יכול לטעון %s + התחיל להוריד %d %s… + הוריד %d %s + הצג חלונות קופצים של דילוג בשביל פתיח/שיר סיום + מחק מאגר + פתיח מעורב + כל %s כבר הורד + מחברים + שפה + MPV + קרדיטים + מיין + בחר ספרייה + נראה שהספרייה שלכם ריקה :( +\nהתחברו לחשבון ספריה או הוסף סדרות לספרייה המקומית שלך + קובץ מצב בטוח נמצא! +\nלא טוען שום תוספות בהפעלה עד להסרת הקובץ. + לא ניתן להתקין את הגרסה החדשה של האפליקציה + הורדת אצווה + תוסף + הורד את כל התוספים ממאגר זה\? + רצועות שמע + מסלולים + Web Video Cast + דפדפן אינטרנט + כל התוספים נכבו עקב התרסקות כדי לעזור לך למצוא את האחד הגורם לצרות. + מוריד החבילות + עודכן %d תוספים + תוספים + פעולה זו תמחק גם את כל תוספי המאגר + הורד את רשימת האתרים שבהם ברצונך להשתמש + הורד: %d + מוגבל: %d + לא מורד: %d + לקלאודסטרים אין אתרים מותקנים כברירת מחדל. עליכם להתקין את האתרים ממאגרים. +\n +\nבגלל הסרת DMCA על ידי Sky UK LImited 🤮 אנחנו לא יכולים לקשר את אתר המאגרים באפליקציה. +\n +\nתצטרפו לדיסקורד שלנו או תחפשו באינטרנט. + הצג מאגרים קהילתיים + רשימה ציבורית + לשים את הכתוביות באותיות רישיות + %s (מוגבל) + רצועות וידאו + החל על הפעלה מחדש + מצב בטוח מופעל + הצג מידע על ההתרסקות + דירוג: %s + תיאור + גרסה + מצב + גודל + תומך + התקן תחילה את התוסף + פלייליסט HLS + נגן וידאו מועדף + נגן פנימי + VLC + האפליקציה לא נמצאה + כל השפות + דלג %s + פתיח + שיר סיום + סיכום + שיר סיום מעורב + מבוא + נקה היסטוריה + יותר מדי טקסט. לא ניתן לשמור ללוח. + הסר מצפייה + האם אתם בטוחים שאתם רוצים לצאת\? + מוריד עדכון אפליקציה… + מתקין עדכון אפליקציה… + האפליקציה תעודכן עם היציאה + מיין לפי + דירוג (גבוה לנמוך) + דירוג (נמוך לגבוה) + מעודכן (חדש לישן) + מעודכן (ישן לחדש) + אלפביתי (א \'עד ת\') + אלפביתי (ת\' עד א\') + פתח עם + נראה שהרשימה הזו ריקה, נסו לעבור לרשימה אחרת \ No newline at end of file diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index a6b3daec..efe0a1d8 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -1,2 +1,3 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 19d1bc6e..7251d0d7 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -3,7 +3,8 @@ Брзина (%.2fx) Оценето: %.1f - Пронајдена нова верзија на апликацијата!\n%s -> %s + Пронајдена нова верзија на апликацијата! +\n%s -> %s Филтер CloudStream Дома @@ -134,7 +135,8 @@ Избриши Паузирај Продолжи - Ова трајно ќе го избрише %s\nДали си сигурен? + Ова трајно ќе го избрише %s +\nДали си сигурен\? Во тек Завршени Статус diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index b60aadae..b6ad3a80 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -3,7 +3,8 @@ വേഗം (%.2fx) റേറ്റിംഗ്: %.1f - പുതിയ അപ്ഡേറ്റ്\n%s -> %s + പുതിയ അപ്ഡേറ്റ് +\n%s -> %s CloudStream ഹോം തിരയുക @@ -66,14 +67,14 @@ സ്രോതസ് അടിസ്ഥാനത്തിൽ തിരയുക തരം അടിസ്ഥാനത്തിൽ തിരയുക %d പഴം കൊടുത്തു - പഴം കൊടുത്തിട്ടില്ല + പഴം കൊടുത്തിട്ടില്ല റീസെറ് ചെയ്യാൻ അമർത്തിപ്പിടിക്കുക തുടർന്നു കാണുക നീക്കം ചെയ്യുക കൂടുതൽ വിവരം - ഈ സ്രോതസ് പ്രവൃത്തിക്കാൻ VPN ഉപയോഗിക്കേണ്ടിവന്നേക്കാം + ഈ സ്രോതസ് പ്രവൃത്തിക്കാൻ VPN ഉപയോഗിക്കേണ്ടിവന്നേക്കാം ഈ ടോറന്റ് സ്രോതസ് ഉപയോഗിക്കാൻ VPN ശുപാർശചെയ്യുന്നു വിവരണം വിവരണം ലഭ്യമല്ല @@ -109,7 +110,7 @@ പഴം കൊടുക്കു പഴം കൊടുത്ത എണ്ണം ആപ്പിന്റെ ഭാഷ - ഈ സ്രോതസ് ക്രോംകാസ്റ് അനുവദിക്കുന്നില്ല + ഈ സ്രോതസ് ക്രോംകാസ്റ് അനുവദിക്കുന്നില്ല ലിങ്കുകൾ ലഭ്യമല്ല ലിങ്ക് പകർത്തിയിരിക്കുന്നു എപ്പിസോഡ് പ്ലേയ് ചെയ്യുക @@ -122,7 +123,8 @@ ഡിലീറ്റ് നിർത്തുക തുടരുക - സ്ഥിരമായി %sനെ ഡിലീറ്റ് ചെയ്യുക\nഉറപ്പാണോ? + സ്ഥിരമായി %sനെ ഡിലീറ്റ് ചെയ്യുക +\nഉറപ്പാണോ\? തുടരുന്നു പൂർത്തിയായി അവസ്ഥ @@ -162,7 +164,7 @@ വലുപ്പം മാറ്റുക സ്രോതസ് OP ഒഴിവാക്കു - ഇനിയും കാണിക്കരുത് + ഇനിയും കാണിക്കരുത് അപ്ഡേറ്റ് ഔചിത്യ വീഡിയോ ക്വാളിറ്റി ചരിത്രം diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 0e69f9fc..c2561914 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -9,7 +9,7 @@ %dm Poster - @string/result_poster_img_des + \@string/result_poster_img_des Aflevering Poster Hoofdposter Volgende willekeurig @@ -19,7 +19,8 @@ Snelheid (%.2fx) Beoordeeld: %.1fAls - Nieuwe update gevonden!\n%s -> %s + Nieuwe update gevonden! +\n%s -> %s Filler %d min CloudStream @@ -108,7 +109,7 @@ Doorgaan met kijken Verwijder Meer Info - @string/home_play + \@string/home_play Een VPN kan nodig zijn om deze provider correct te laten werken Deze provider is een torrent, een VPN wordt aanbevolen Metadata wordt niet geleverd door de site, het laden van video\'s zal mislukken als deze niet op de site bestaat. @@ -143,8 +144,8 @@ Back-up gegevens Geladen back-up bestand Kan gegevens uit bestand niet herstellen %s - Gegevens succesvol opgeslagen - Opslagrechten ontbreken, probeer het opnieuw + De gegevens zijn opgeslagen + Opslagrechten ontbreken. Probeer het opnieuw. Fout bij het maken van een back-up %s Zoeken Accounts @@ -156,7 +157,7 @@ Verstuurt geen gegevens Toon filler episode voor anime Toon trailers - Toon posters van kitsu + Toon posters van Kitsu App-updates tonen Automatisch zoeken naar nieuwe updates bij het opstarten Update naar pre-releases @@ -173,7 +174,7 @@ Link gekopieerd naar klembord Aflevering afspelen Reset naar standaardwaarde - Sorry, de applicatie is gecrasht. Er wordt een anoniem bugrapport naar de ontwikkelaars gestuurd + Sorry, de applicatie is gecrasht. Er wordt een anoniem bugrapport naar de ontwikkelaars gestuurd Seizoen Geen seizoen Aflevering @@ -189,8 +190,10 @@ Hervatten -30 +30 - Dit wordt zeker permanent verwijderd %s\nWeet u het zeker? - %dm\nremaining + Dit wordt zeker permanent verwijderd %s +\nWeet u het zeker\? + %dm +\nremaining Voortdurende Voltooid Status @@ -219,8 +222,8 @@ Film Serie Tekenfilm - @string/anime - @string/ova + \@string/anime + \@string/ova Torrent Documentaire Aziatisch drama @@ -256,7 +259,7 @@ Update Gewenste kijkwaliteit Maximaal aantal tekens voor titel van videospeler - Videospeler Resolutie + Videospeler Resolutie Grootte videobuffer Lengte videobuffer Video cache op schijf @@ -319,7 +322,7 @@ Sync gewaardeerd %d / 10 - /?? + /\?\? /%d Geauthenticeerd %s Mislukt om te verifiëren aan %s @@ -394,9 +397,12 @@ Instelling overslaan Pas het uiterlijk van de app aan uw apparaat aan Crashrapportage - Wat wil je zien? + Wat wil je zien\? Klaar Markeer als bekeken Geschiedenis Speel Trailer + Start de volgende episode wanneer deze afgelopen is + Volgende episode automatisch afspelen + De update is gestart \ No newline at end of file diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index e7ebc98f..41bf704d 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -2,7 +2,7 @@ Plakat - @string/result_poster_img_des + \@string/result_poster_img_des Episode Plakat Main Plakat Neste tilfeldig @@ -12,7 +12,8 @@ Avspillingshastighet (%.2fx) Vurdert: %.1f - Ny oppdatering funnet!\n%s -> %s + Ny oppdatering funnet! +\n%s -> %s CloudStream Hjem Søk @@ -142,7 +143,8 @@ Slett Stopp Gjenoppta - Dette vil slette %s\nEr du sikker? + Dette vil slette %s +\nEr du sikker\? Pågående Fullført Posisjon @@ -201,22 +203,7 @@ App Tema Foretrukket Videoinnhold Disclaimer - 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. - + 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. Besetning: %s %dm Tøm @@ -503,4 +490,6 @@ APK-installatør Noen enheter støtter ikke den nye pakkeinstallatøren. Prøv gammeldags alternativ hvis ting ikke installeres. Oppdatering startet + Programtillegg nedlastet + Programmet vil oppgraderes når du avslutter det \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index c1999697..c709f124 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -2,16 +2,17 @@ Prędkość (%.2fx) Ocena: %.1f - Znaleziono nową aktualizację!\n%s -> %s + Znaleziono nową aktualizację! +\n%s -> %s Filler %d min %s Odc. %d - Plakat - Plakat odcinka - Główny plakat + Obrazek + Obrazek odcinka + Główny obrazek Następny losowy Wstecz - Zmień dostawcę + Zmień źródło Pogląd tła CloudStream Główna @@ -23,7 +24,7 @@ Brak danych Więcej opcji Następny odcinek - Odcinek %d będzie udostępniony + Odcinek %d wyjdzie za Gatunki Udostępnij Otwórz w przeglądarce @@ -52,11 +53,11 @@ Błąd przy pobieraniu Anulowano pobieranie Zakończono pobieranie - Stream + Odtwórz Błąd przy ładowaniu linków Pamięć wewnętrzna Dub - Sub + Nap Usuń plik Odtwórz plik Wznów pobieranie @@ -86,7 +87,7 @@ Wzniesienie napisów Czcionka Rozmiar czcionki - Szukaj według dostawców + Szukaj według źródeł Szukaj według typów Dano %d bananów Brak bananów @@ -94,12 +95,12 @@ Pobieranie języków Język napisów Przytrzymaj, aby zresetować - Importuj czcionki umieszczając je w %s + Importuj czcionki, umieszczając je w %s Kontyntynuj oglądanie Usuń Więcej informacji Połączenie przez VPN może być wymagane - Ten dostawca jest torrentem, wskazane jest użycie połączenia VPN + To źródło jest torrentem, wskazane jest użycie połączenia VPN Metadane nie są dostarczane przez witrynę, ładowanie filmu nie powiedzie się, jeśli nie ma ich w witrynie. Opis Nie znaleziono opisu @@ -107,25 +108,25 @@ Pokaż Logcat 🐈 Obraz-w-obrazie Oglądaj w małym, pływającym okienku - Przycisk zmiany rozmiaru + Zmień rozmiar odtwarzacza Usuwanie czarnych ramek Napisy Ustawienia napisów Napisy Chromecast Ustawienia napisów Chromecast - Tryb Eigengrau + Tryb Eigengravy Ustawienia prędkości - Przesuwaj aby przeglądać - Przesuwaj w lewo lub prawo aby kontrolować czas - Przesuwaj aby zmienić ustawienia - Przesuwaj po lewej lub prawej stronie aby zmienić jasność i głośność + Przesuń aby przewinąć + Przesuń w lewo lub prawo aby kontrolować czas + Przesuń aby zmienić ustawienia + Przesuń góra-dół z lewej lub prawej aby zmienić jasność i głośność Autoodtwarzanie następnego odcinka - Rozpocznij następny odcinek po zakończeniu bieżącego - Wielkość skoku przy podwójnym stuknięciu - Podwójne stuknięcie aby przeglądać - Stuknij 2 razy z prawej lub lewej strony aby przeglądać - Stuknij dwukrotnie, aby wstrzymać - Stuknij na środku, aby wstrzymać + Rozpocznij następny odcinek po skończeniu bieżącego + Czas przewinięcia przy podwójnym kliknięciu + Podwójne kliknięcie aby przewinąć + Kliknij 2 razy z prawej lub lewej strony aby przewinąć + Kliknij dwukrotnie aby wstrzymać + Kliknij na środku, aby zatrzymać wideo Użyj jasności systemowej Użyj jasności systemowej w odtwarzaczu aplikacji zamiast ciemnej nakładki Aktualizuj postęp oglądania @@ -142,23 +143,23 @@ Aktualizacje i kopia zapasowa Informacje Zaawansowane wyszukiwanie - Szukaj z podziałem na dostawców + Szukaj z podziałem na źródła Wysyłaj dane tylko przy awariach Nie wysyłaj żadnych danych - Pokaż odcinek wypełniający dla anime + Pokaż fillery dla anime Pokaż zwiastuny - Pokaż plakaty z Kitsu + Pokaż obrazki z Kitsu Ukryj wybraną jakość wideo w wynikach wyszukiwania Automatyczne aktualizacje rozszerzeń Automatyczne pobieranie rozszerzeń Pokazuj aktualizacje - Automatycznie wyszukuj aktualizacji przy starcie + Automatycznie wyszukuj aktualizacje przy starcie Aktualizuj do wersji beta - Wyszukuj wersji beta, zamiast pełnych wydań + Wyszukuj wersji beta, zamiast oficjalnych wydań Github - Aplikacja do noweli - Aplikacja do anime - Discord + Aplikacja do light novel od nas + Aplikacja do anime od nas + Serwer Discord Daj banana programistom Dano banana Język aplikacji @@ -167,7 +168,7 @@ Skopiowano do schowka Odtwórz odcinek Zresetowano - Awaria aplikacji. Anonimowe zgłoszenie błedu zostanie wysłane programistom + Awaria aplikacji. Anonimowe zgłoszenie błędu zostanie wysłane programistom Sezon %s %d%s Brak sezonu @@ -184,9 +185,11 @@ Odtwórz -30 +30 - Spowoduje to trwałe usunięcie %s\nCzy jesteś pewien? - %dm\npozostało - Bierzący + Spowoduje to trwałe usunięcie %s +\nCzy jesteś pewien\? + %dm +\npozostało + Bieżący Zakończone Status Rok @@ -202,13 +205,13 @@ Aplikacja Filmy - Serial telewizyjny + Seriale telewizyjne Kreskówki Anime Torrenty Filmy dokumentalne OVA - Filmy azjatyckie + Dramy azjatyckie Transmisje na żywo NSFW Inne @@ -218,7 +221,7 @@ Kreskówka Torrent Film dokumentalny - Film azjatycki + Drama azjatycka Transmisja na żywo NSFW Inne @@ -226,48 +229,48 @@ Zdalny błąd Błąd renderowania Nieoczekiwany błąd odtwarzacza - Błąd pobierania, sprawdź uprawnienia - Chromecast odcinka - Chromecast mirroru + Błąd pobierania, sprawdź uprawnienia aplikacji + odcinek Chromecast + mirror dla Chromecast Odtwórz w aplikacji Odtwórz w %s Odtwórz w przeglądarce Kopiuj link Automatyczne pobieranie Pobierz mirror - Odświerz linki + Odśwież linki Pobierz napisy Etykieta jakości Etykieta dubbingu Etykieta napisów Tytuł - Włącz elementy interfejsu na plakatach + Pokaż elementy interfejsu na obrazkach Nie znaleziono aktualizacji - Sprawdź czy jest aktualizacja + Sprawdź, czy jest aktualizacja Zablokuj - Rozmiar - Źródlo + Zmień rozmiar + Źródło Pomiń OP Nie pokazuj ponownie Pomiń tę aktualizację Aktualizacja Domyślna jakość - Maksymalna ilość znaków tytułu w odtwarzaczu - Zawartość tytułu w odtwarzaczu + Maksymalna ilość znaków w tytule odtwarzacza + Rozdzielczość odtwarzacza wideo Rozmiar bufora wideo Długość bufora wideo Pamięć podręczna wideo na dysku Wyczyść pamięć podręczną wideo i obrazów - Ustawienie zbyt wysokiej wartości może powodować problemy w systemach z małą ilością pamięci RAM, takich jak urządzenia Android TV lub stare telefony. - Zbyt wysokie ustawienie może powodować problemy w systemach z małą ilością miejsca w pamięci, takich jak urządzenia Android TV. - DNS przez HTTPS + Ustawienie zbyt wysokiej wartości może powodować problemy na urządzeniach z małą ilością pamięci RAM, takich jak urządzenia Android TV lub stare telefony. + Zbyt wysokie ustawienie może powodować problemy na urządzeniach z małą ilością dostęponej pamięci, takich jak urządzenia Android TV. + DNS over HTTPS Przydatne w pomijaniu blokad dostawców internetu Sklonuj stronę Usuń stronę Dodaj klona istniejącej strony z innym adresem URL Ścieżka pobierania URL serwera Nginx - Wyświetlanie Anime z dubbingiem/subbingiem + Wyświetlanie Anime z dubbingiem/napisami Dopasuj do ekranu Rozciągnij Powiększ @@ -275,12 +278,12 @@ Ogólne Przycisk do losowania Pokaż przycisk do losowania na stronie głównej - Języki dostawców + Języki źródeł Układ aplikacji Preferowane media - Włącz NSFW u obsługiwanych dostawców + Włącz NSFW w obsługiwanych źródłach Kodowanie napisów - Dostawcy + Źródła Układ interfejsu Automatyczny Układ dla telewizorów @@ -288,8 +291,8 @@ Układ dla emulatorów Kolor podstawowy Motyw aplikacji - Pozycja tytułu względem plakatu - Ustaw tytuł pod plakatem + Pozycja tytułu względem obrazka + Ustaw tytuł pod obrazkiem Kod języka (pl) %s %s konto @@ -301,7 +304,7 @@ Dodaj synchronizację Dodano %s Synchronizacja - Ocenione + Oceniono %d na 10 Uwierzytelniono %s Nie udało się uwierzytelnić %s @@ -312,9 +315,9 @@ Maks Min Kontur - Wciśnięte + Obniżone Cień - Zniesione + Wzniesione Synchronizacja napisów 1000 ms Opóźnienie napisów @@ -327,7 +330,7 @@ Wczytaj z pliku Wczytaj z Internetu Pobrano plik - Główna + Główny Drugoplanowy Pomocniczy Źródło @@ -349,7 +352,7 @@ HDR SDR Web - Obraz plakatu + Obrazek Odtwarzacz Rozdzielczość i tytuł Tytuł @@ -358,26 +361,26 @@ Niepoprawne dane Niepoprawny URL Błąd - Usuń informacje dla niesłyszących z napisów + Usuń CC z napisów Usuń nadmiarowe informacje z napisów - Filtrowanie wg preferowanego języka mediów + Filtrowanie wg preferowanego języka Dodatki Zwiastun Odsyłacz - Dalej - Wyświetlaj filmy w wybranych językach - Cofnij - Pomiń + Następny + Wyświetlaj filmy w tych językach + Poprzedni + Pomiń setup Dostosuj wygląd aplikacji do urządzenia - Przekazywanie błędów - Preferowany rodzaj filmów + Zgłaszanie błędów + Co chciałbyś obejrzeć\? Gotowe - Dodatki + Rozszerzenia Dodaj repozytorium Nazwa repozytorium Adres URL repozytorium - Rozszerzenie załadowane - Rozszerzenie usunięte + Załadowano rozszerzenie + Usunięto rozszerzenie Błąd ładowania %s 18+ Rozpoczęto pobieranie %d %s… @@ -395,24 +398,24 @@ Zaaktualizowano %d rozszerzeń CloudStream nie ma domyślnie zainstalowanych żadnych witryn. Musisz zainstalować witryny z repozytoriów. \n -\nZ powodu bezmyślnego usunięcia DMCA przez Sky UK Limited 🤮 nie możemy zamieścić linku dowitryny z repozytoriami. +\nZ powodu bezmyślnego usunięcia DMCA przez Sky UK Limited 🤮 nie możemy zamieścić linku do witryny z repozytoriami. \n -\nDołącz do naszego Discorda lub szukaj online. +\nDołącz do naszego Discorda lub poszukaj online. Zobacz repozytoria społeczności Publiczna lista Wszystkie napisy wielką literą - Pobrać wszystkie rozszerzenia z tego repozytorium? + Pobrać wszystkie rozszerzenia z tego repozytorium\? %s (Wyłączone) Ścieżki Ścieżki audio Ścieżki wideo Zastosuj po ponownym uruchomieniu Tryb bezpieczny włączony - Wszystkie rozszerzenia zostały wyłączone z powodu awarii, aby pomóc Ci znaleźć ten, który powoduje problemy. - Wyświetl informacje o awarii + Z powodu wystąpienia błędu wszystkie rozszerzenia zostały wyłączone, aby ułatwić wykrycie tego wadliwego. + Wyświetl informacje o błędzie Ocena: %s Opis - Versja + Wersja Status Rozmiar Autorzy @@ -431,7 +434,7 @@ Wyczyść historię Historia Za dużo tekstu. Nie można skopiować do schowka. - Link do transmisji + Link do odtwarzania Odtwórz w CloudStream Pomiń %s %dh %dm @@ -446,11 +449,11 @@ Pobieranie aktualizacji aplikacji… /%d Obsada: %s - Automatycznie instaluj wszystkie jeszcze nie zainstalowane wtyczki z dodanych repozytoriów. - \@string/result_poster_img_des + Automatycznie instaluj wszystkie jeszcze niezainstalowane wtyczki z dodanych repozytoriów. + Poster Podsumowanie Instalator APK - Niektóre telefony nie obsługują nowego instalatora pakietów. Wypróbuj tryb kompatybilności, jeśli aktualizacje nie zostaną zainstalowane. + Niektóre telefony nie obsługują nowego instalatora pakietów. Wypróbuj tryb legacy, jeśli aktualizacje nie zostaną zainstalowane. password123 \@string/ova MojaFajnaWitryna @@ -470,12 +473,12 @@ Intro Mixed ending Pokaż wyskakujące okienka pomijania dla niektórych segmentów - Dodatki + Rozszerzenia Działania Pamięć podręczna Powtórz proces konfiguracji Linki - Aktualizacje aplikacji + Aktualizacje Kopia zapasowa Napisy Gesty @@ -486,6 +489,24 @@ Wygląd Odtwórz zwiastun Aplikacja zostanie zaktualizowana po wyjściu - Aktualizacja rozpoczęta - Wtyczka pobrana + Rozpoczęto aktualizację + Pobrano rozszerzenie + Usuń z obejrzanych + Przeglądarka + Data aktualizacji (od nowego do starego) + Sortuj według + Sortuj + Otwórz za pomocą + Ocena (od najwyższej do najniższej) + Ocena (od najniższej do najwyższej) + Data aktualizacji (od starego do nowego) + Alfabetycznie (od A do Z) + Alfabetycznie (od Z do A) + Wybierz bibliotekę + Biblioteka + Wygląda na to, że twoja biblioteka jest pusta :( +\nZaloguj się na swoje konto lub dodaj programy do swojej lokalnej biblioteki + Wygląda na to, że ta lista jest pusta, spróbuj przełączyć się na inną + Znaleziono plik trybu bezpiecznego. +\nRozszerzenia nie zostaną wczytane, dopóki plik nie zostanie usunięty. \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings-pt.xml b/app/src/main/res/values-pt/strings.xml similarity index 76% rename from app/src/main/res/values-pt/strings-pt.xml rename to app/src/main/res/values-pt/strings.xml index 375c3193..9353664e 100644 --- a/app/src/main/res/values-pt/strings-pt.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,32 +1,34 @@ - - - - + + + %s Ep %d + %dh %dm + %dm + Episódio %d será lançado em Poster - @string/result_poster_img_des - Episode Poster - Main Poster - Next Random - Go back - Change Provider + Capa do Episódio + \@string/result_poster_img_des + Capa Principal + Próximo Aleatório + Voltar + Trocar Provedor + %dd %dh %dm + Fonte + Resolução + Extras Preview Background - - Velocidade (%.2fx) Classificado: %.1f - Nova atualização encontrada!\n%s -> %s + Nova atualização encontrada! +\n%s -> %s Enchimento - CloudStream - Reproduzir com CloudStream + Reproduzir com CloudStream Início Pesquisa Transferências Opções - Procurar… - Procurar em %s… - + Procurar em %s… Sem Dados Mais Opções Próximo episódio @@ -35,24 +37,21 @@ Abrir no Navegador Saltar Carga Carregando… - Assistindo Em Espera Concluído Abandonado Planeio Assistir - Nenhuma - Assistindo de Novo - + Nenhuma + Assistindo de Novo Reproduzir Filme - Reproduzir Livestream + Reproduzir Livestream Transmitir Torrent Fontes Legendas Voltar a tentar ligação… Voltar Atrás Reproduzir Episódio - Transferir Transferido A Transferir @@ -61,19 +60,15 @@ Transferência Falhou Transferência Cancelada Transferência Completa - Stream - + Stream Erro a Carregar Links Armazenamento Interno - Dob Leg - Eliminar Ficheiro Reproduzir Ficheiro Retomar Transferência Pausar Transferência - Desativar relatório automático de erros Mais info Esconder @@ -84,13 +79,11 @@ Remover Aplicar Cancelar - Copiar + Copiar Fechar Limpar Guardar - Velocidade de Reprodução - Configurações de Legendas Cor do Texto Cor do Delineado @@ -100,89 +93,70 @@ Elevação da Legenda Fonte Tamanho da Fonte - Procurar usando fornecedores Procurar usando tipos - %d Benenes dadas aos devs Nenhum Benene dada - Auto-seleção de Idioma Transferir Idiomas - Idioma da Legenda + Idioma da Legenda Segure para retornar para o padrão - Importar fontes colocando em %s + Importar fontes colocando em %s Continuar a Assistir - Remover Mais info - Uma VPN pode ser necessária para que este fornecedor funcione corretamente Este fornecedor é um torrent, uma VPN é recomendada - - Metadados não são oferecidos pelo site, o carregamento do vídeo irá falhar se ele não existir no site. - - Descrição + Metadados não são oferecidos pelo site, o carregamento do vídeo irá falhar se ele não existir no site. + Descrição Nenhum Enredo Encontrado Nenhuma Descrição Encontrada - - Mostrar logcat 🐈 - + Mostrar logcat 🐈 Picture-in-picture Continua a reprodução num player em miniatura em cima de outras apps Botão de redimensionamento do player Remover as bordas negras Legendas Configurações de legendas do player - Legendas do Chromecast + Legendas do Chromecast Configurações de legendas do Chromecast - Modo Eigengravy Acrescenta uma opção de velocidade no player Deslize para andar Deslize para a esq. ou dir. para controlar o tempo no player Deslize para mudar as configurações Deslize do lado esq. ou dir. para ajustar brilho ou volume - -Reproduzir automaticamente próximo episódio + Reproduzir automaticamente próximo episódio Começa o próximo episódio quando o atual termina - Toque duplo para avançar - Toque duplo para pôr em pausa - Segundos avançados no player + Toque duplo para pôr em pausa + Segundos avançados no player Toque duplo no lado esq. ou dir. para andar para trás ou para a frente Toque no meio para pôr em pausa Usar brilho da sistema Usar brilho do sistema no player em vez de uma sobreposição escura - - Atualizar progresso + Atualizar progresso Sincronizar automaticamente o progresso do seu episódio atual - Restaurar dados a partir de backup - Fazer backup Arquivo de backup carregado Falha ao restaurar dados do ficheiro %s Dados guardados com sucesso Permissões de armazenamento em falta, por favor tente de novo Erro no backup de %s - Procurar - Contas + Contas Atualizações e backup - Info Procura Avançada Mostra resultados separados por fornecedor Só envia dados sobre falhas Não envia nenhum dado Mostrar episódios de enchimento para anime - Mostrar trailers + Mostrar trailers Mostrar posters do kitsu - - Esconder qualidades de vídeo selecionadas nos resultados da Pesquisa - Atualizações de plugin automáticas - + Esconder qualidades de vídeo selecionadas nos resultados da Pesquisa + Atualizações de plugin automáticas Mostrar atualizações da app Procurar novas atualizações automaticamente ao iniciar Atualizar para pré-lançamentos @@ -192,18 +166,13 @@ Junte-se ao Discord Dar um benene aos devs Benene dada - Idioma da App - Este fornecedor não tem suporte para Chromecast Nenhum Link Encontrado Link copiado para a área de transferência Reproduzir episódio Restaurar para o padrão - Desculpe, a aplicação falhou. Um relatório de erro anónimo será enviado para os - desenvolvedores - - + Desculpe, a aplicação falhou. Um relatório de erro anónimo será enviado para os desenvolvedores Temporada Nenhuma Temporada Episódio @@ -211,14 +180,14 @@ T E Nenhum Episódio encontrado - Eliminar Ficheiro Eliminar Pôr em Pausa Retomar - Isto apagará %s permanentemente\nTem a certeza? - %dm\nem falta - + Isto apagará %s permanentemente +\nTem a certeza\? + %dm +\nem falta Em Curso Concluído Estado @@ -227,44 +196,36 @@ Duração Site Sinopse - Na fila Sem Legendas Padrão - Livre Usado App - - Filmes Séries Desenhos Animados Anime Torrents - Documentários - OVA + Documentários + OVA Dramas Asiáticos Transmissões em Direto NSFW Outros - - - Filme - Série - Desenho Animado - Torrent - Documentário - Drama Asiático - Transmissão em Direto + Filme + Série + Desenho Animado + Torrent + Documentário + Drama Asiático + Transmissão em Direto NSFW - Erro de fonte Erro remoto Erro de renderização Erro inesperado do player Erro de transferência, verifique permissões de armazenamento - Episódio pelo Chromecast Alternativa pelo Chromecast Reproduzir na app @@ -274,118 +235,82 @@ Transferência Automática Transferir por servidor alternativo Recarregar links - Transferir legendas - - Etiqueta de qualidade + Transferir legendas + Etiqueta de qualidade Etiqueta Dub Etiqueta Sub Título Alternar elementos da interface no póster - Nenhuma Atualização Encontrada Procurar Atualização - Fixar Mudar Tamanho Fonte Saltar OP - Não mostrar de novo - Saltar esta Atualização + Saltar esta Atualização Atualizar Qualidade Preferida - Máximo de caracteres do título de vídeos + Máximo de caracteres do título de vídeos Resolução do player de vídeo - - Tamanho do buffer do vídeo + Tamanho do buffer do vídeo Comprimento do buffer do vídeo Cache do vídeo em disco Limpar cache de vídeo e imagem - Causará travamentos aleatórios se definido muito alto. Não mude se tiver pouca memória RAM, como um Android TV ou um telefone antigo Pode causar problemas em sistemas com pouco espaço de armazenamento se definido muito alto, como em dispositivos Android TV - DNS sobre HTTPS Útil para contornar bloqueios do fornecedor de internet - - Clonar site + Clonar site Remover site Adiciona um clone de um site existente, com um url diferente - - Caminho de transferência - + Caminho de transferência Url do servidor Nginx - Mostrar Anime Dobrado/Legendado - Ajustar para a Tela Esticar Aumentar - - Aviso Legal - Quaisquer questões legais relativas ao conteúdo desta aplicação - devem ser levados em conta com os próprios anfitriões e fornecedores de arquivos, pois não somos afiliados a eles. - - Em caso de violação de direitos autorais, favor contatar diretamente as partes responsáveis ou os sites de streaming. - - O aplicativo é puramente para uso educacional e pessoal. - - O CloudStream 3 não hospeda nenhum conteúdo no aplicativo, e não tem controle sobre qual mídia é colocada ou retirada. - O CloudStream 3 funciona como qualquer outro mecanismo de busca, como o Google. O CloudStream 3 não hospeda, não faz upload ou - gerenciar qualquer vídeo, filme ou conteúdo. Ele simplesmente rasteja, agrega e exibe links em um conveniente, - interface amigável para o usuário. - - Ela apenas raspa sites de terceiros que podem ser acessados publicamente através de qualquer navegador web normal. É o - responsabilidade do usuário de evitar qualquer ação que possa violar as leis que regem sua localidade. Utilizar - CloudStream 3 por sua própria conta e risco. - - Geral - Botão Aleatório + Aviso Legal + Geral + Botão Aleatório Mostra o botão Aleatório na página inicial Idioma dos fornecedores Layout da App Mídia preferida -Ativar NSFW em fornecedores compatíveis -Fornecedores - Codificação das legendas + Ativar NSFW em fornecedores compatíveis + Codificação das legendas + Fornecedores Layout - Auto Layout de TV Layout de telemóvel - Layout de emulador - + Layout de emulador Cor Primária Tema do App - Local do título do poster + Local do título do poster Coloca o título debaixo do poster - - - senha123 + senha123 MeuNomeFixe ola@mundo.com 127.0.0.1 MeuSiteFixe examplo.com Codigo da Língua (pt) - Conta Sair Entrar Mudar de conta Adicionar conta - Criar conta + Criar conta Adicionar sincronização %s adicionado Sincronizar Nota %d / 10 - /?? + /\?\? /%d %s autenticado Falha em autenticar para %s - - Nenhuma Normal Tudo @@ -395,20 +320,13 @@ Deprimido Sombreado Em Relevo - Sincronizar legendas - Atraso de legenda + Sincronizar legendas + Atraso de legenda Use isto se as legendas forem mostradas %dms adiantadas Use isto se as legendas forem mostradas %dms atrasadas Sem atraso de legenda - Luís argüia à Júlia que «brações, fé, chá, óxido, pôr, zângão» eram palavras do português - - Recomendada + Recomendada %s carregada Carregar de arquivo Carregar da Internet @@ -416,34 +334,24 @@ Protagonista Coadjuvante Figurante - - Fonte Aleatório - Em breve… - - Imagem de Poster + Imagem de Poster Player Resolução e título Título - Resolução Id inválida Dado inválido - -URL inválido + URL inválido Erro Remover legendas ocultas(CC) das legendas Remover bloat das legendas - -Filtrar por linguagem preferida - Extras + Filtrar por linguagem preferida Trailer - Próximo Ver vídeos nestas linguagens Anterior Saltar setup - Change the look of the app to suit your device Crash reporting What do you want to see @@ -455,7 +363,6 @@ Plugin Carregado Plugin Apagado Falha ao carregar %s - Iniciada a transferência %d %s Transferido %d %s com sucesso Tudo %s já transferido @@ -472,7 +379,6 @@ Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas - - Transferir todos os plugins deste repositório? + Transferir todos os plugins deste repositório\? %s (Desativado) - + \ No newline at end of file diff --git a/app/src/main/res/values-mo/string.xml b/app/src/main/res/values-qt/strings.xml similarity index 86% rename from app/src/main/res/values-mo/string.xml rename to app/src/main/res/values-qt/strings.xml index 361aaf56..b36f3b16 100644 --- a/app/src/main/res/values-mo/string.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -4,12 +4,12 @@ aauugghhaauuh ooh oouuh - ouuhhhooh ooh + ouuhhuhooh ooh aaaghhoh aauugghh - haaooh + haaooh ooo aaaaaah oooohhoouuh - aaaaaoouuhahhh ahh + aaaaoouuuhahhh ahh oooohh aauuh ahhhaaaghh aaaaa ahaauugghh @@ -122,7 +122,7 @@ A ooha ohahaaahoooa ahahooo oooooah - aahhaaaaaahooo aauuh aaahhuaooo-ahahoooohh aoouuhoohoohooo-ahah + aahhaaaaaahooo aauuh aaahhuaooo-ahahoooohh aoouuhoohoohooo-ahah %s aaaghhaaaaa aauuhaauuh ahhhaaaaa @@ -145,7 +145,7 @@ aauugghhooo-ahah ohaaauugghh aoohaaahhu ahouuhhh ooo-ahahaauuh aaahhu - ooo-ahah ohaauuh + ooo-ahah ohaauuh %s ahoha ooo-ahahohoohah oooohh aauugghhahhaauugghh aaaghhoooohh aaahhu ahooo @@ -195,5 +195,27 @@ u ooah uo ahauao huhuu hauu h a ou oh ouhuouhoaaha aaooohhouhhha hauauuu - aaaaaaa uuuuuu\n%s -> %s + aaaaaaa uuuuuu\n%s -> %s + %s aaou %d + oouaaahh %s + aaaaaaugh ouh %d uuoogahaaah ooua-h-ha + %daaa %duuu %dhhhg + %daaaaaaauuhh %doouuahaaha + %dmmmm... + aaaaaaaaaaaaahh (%.2foouo) + aghaaaooo-ough %.1f + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaooooohhuhuhuhuhuhhuhuhhhaaagaha-agagaoooo + %d ooaaaugha + oooooh aaah aauuuggghauuh + aaoohhu %s aoouu + oooaaaauhgaaaa + aaaaaah-ooooooooouuagh + auugggguuuuuuhhh uuuu hhhhhhhhuhggghggg + ggaaaahhhhhhh gaauuuuuuuaaaau + aaauuuuggggguu + ooo aagg hhhh + ooo aagg hhhh + uuuuhhhoouuooog ooaaahhhh + uuu ugggg + ooo guggg ooh \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 9087f8d9..982546bc 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -9,7 +9,7 @@ %dm Poster - @string/result_poster_img_des + \@string/result_poster_img_des Poster Episod Poster Principal Următorul la Întâmplare @@ -19,7 +19,8 @@ Viteză (%.2fx) Evaluare: %.1f - Actualizare nouă găsită!\n%s -> %s + Actualizare nouă găsită! +\n%s -> %s Filler %d min @@ -105,7 +106,7 @@ Continuați să urmăriți Eliminați Mai multe informații - @string/home_play + \@string/home_play Există probabilitatea necesitații unui VPN pentru ca acest furnizor să funcționeze corespunzător Acest furnizor este un torrent, se recomandă un VPN Metadatele nu sunt furnizate de către site, există posibilitatea ca încărcarea videoclipului să eșueze. @@ -140,7 +141,7 @@ Copie de rezervă a datelor Fișier de rezervă încărcat Imposibilitatea de a restaura datele din %s - Datele au fost salvate cu succes + Date stocate Permisiuni de arhivare lipsă, vă rugăm să încercați din nou Eroare de backup %s Căutare @@ -184,8 +185,10 @@ Continuă -30 +30 - Sunteți pe cale să ștergeți definitiv %s\nSunteți sigur? - %dm\nrămas + Sunteți pe cale să ștergeți definitiv %s +\nSunteți sigur\? + %dm +\nrămas În curs de desfășurare Finalizat Status @@ -268,22 +271,7 @@ Întindere Mărire Aviz juridic (declinarea responsabilității și drepturi de autor) - Orice probleme legale privind conținutul acestei aplicații ar trebui - să fie rezolvate cu furnizorii și gazdele actuale de fișiere, întrucât noi nu suntem afiliați cu aceștia. - - În caz de încălcare a drepturilor de autor, vă rugăm să contactați direct părțile responsabile sau site-urile de streaming. - - Aplicația este destinată exclusiv utilizării educaționale și personale. - - CloudStream 3 nu găzduiește niciun fel de conținut în aplicație și nu are niciun control asupra conținutului media care este pus sau retras. - CloudStream 3 funcționează ca orice alt motor de căutare, cum ar fi Google. CloudStream 3 nu găzduiește, nu încarcă și - nu gestionează niciun videoclip, film sau conținut. Pur și simplu navighează, adună și afișează linkuri într-o interfață convenabilă - și ușor de utilizat. - - Pur și simplu, acesta extrage paginile web ale unor terțe părți care sunt accesibile publicului prin intermediul oricărui browser web obișnuit. Este - responsabilitatea utilizatorului de a evita orice acțiune care ar putea încălca legile care guvernează locația sa. Utilizați - CloudStream 3 pe propria răspundere. - + Orice probleme legale privind conținutul acestei aplicații ar trebui să fie rezolvate cu furnizorii și gazdele actuale de fișiere, întrucât noi nu suntem afiliați cu aceștia. În caz de încălcare a drepturilor de autor, vă rugăm să contactați direct părțile responsabile sau site-urile de streaming. Aplicația este destinată exclusiv utilizării educaționale și personale. CloudStream 3 nu găzduiește niciun fel de conținut în aplicație și nu are niciun control asupra conținutului media care este pus sau retras. CloudStream 3 funcționează ca orice alt motor de căutare, cum ar fi Google. CloudStream 3 nu găzduiește, nu încarcă și nu gestionează niciun videoclip, film sau conținut. Pur și simplu navighează, adună și afișează linkuri într-o interfață convenabilă și ușor de utilizat. Pur și simplu, acesta extrage paginile web ale unor terțe părți care sunt accesibile publicului prin intermediul oricărui browser web obișnuit. Este responsabilitatea utilizatorului de a evita orice acțiune care ar putea încălca legile care guvernează locația sa. Utilizați CloudStream 3 pe propria răspundere. General Aleatoriu Afișați butonul Aleatoriu pe pagina de start @@ -320,7 +308,7 @@ Sincronizare Recenzie %d / 10 - /?? + /\?\? /%d %s autentificat Imposibil de autentificat la %s @@ -388,4 +376,12 @@ Trailer Istoric Marcare ca vizionat + Redă automat următorul episod + CloudStream + Vizionează trailerul + Actualizarea a început + Actualizați progresul ceasului + Începe următorul episod când se termină episodul curent + Ascundeți calitatea video selectată în rezultatele căutării + Redare Livestream \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 49ed0fb3..6e9fb394 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -4,19 +4,19 @@ Нет Да Треки - в очереди + В очереди Приблизить - Скачать + Загрузить Поиск Заполнить Скачать неудачный Подогнать Удалить - Всё + Все Пауза Актёрский состав: %s Название источника - Вход + Войти Нет Название Загрузка… @@ -24,13 +24,13 @@ Выйти Серия %d будет выпущен в Плакат - \@нить/результат_плокат_картинка_ - Серия плакат + Плакат + Плакат эпизода Главный плакат Следующий случайный Вернуться - Сменить поставщика - Фон предпросмотр + Изменить поставщика + Предпросмотр фона Скорость (%.2fx) Оценили: %.1f Новое обновление найдено! @@ -39,7 +39,7 @@ CloudStream Убирать %s Ep %d - Проиграть с CloudStream + Смотреть с CloudStream Главная Поиск Загрузки @@ -48,49 +48,49 @@ Поиск %s… Нет данных Дополнительные опции - Следующий серия + Следующий эпизод Жанры - Делиться - Открыть в браузер - Скип загрузка - Смотрю - Приостановленный + Поделиться + Открыть в браузере + Пропустить загрузку + Просмотр + Приостановленно Завершено Брошенный - План по смотреть - Никто + План посмотреть + Нет Пересмотрю - Проиграть фильм - Проиграть трейлер - Проиграть Livestream + Смотреть фильм + Смотреть трейлер + Смотреть Livestream Источники Субтитры - Проиграть серия + Смотреть эпизод Повторная попытка подключение… Вернуться - Скачали + Скачано Скачивание - Скачать приостановленный + Скачать остановлена Скачать начатый - Скачать отменено + Скачать отменённый Скачать выполнено Инфо Обновление началось Прятать - Проиграть - Подробнее инфо + Смотреть + Подробнее Фильтр закладки Закладки - Наносить - Прервать + Применить + Отмена Копия Закрыть Очистить Сохранить Скорость проигрыватель - Проиграть Серия + Воспроизвести Эпизод %dд %dч %dм - %d мин + %d мин. Dub Sub Установите смотреть состояние @@ -101,28 +101,28 @@ Цвет окна Тип края Субтитр подъём - Поиск с использованием поставщики + Поиск с использованием поставщиков Поиск с использованием типов %d Бенены данность на разрабы Бенены не дают Автовыбор языка Скачать языки Язык субтитров - Удерживайте чтобы сбросить по умолчанию + Удерживайте, чтобы сбросить по умолчанию Ошибка загрузки ссылок Поток Шрифт Размер шрифта Удалить файл - Проиграть файл + Воспроизвести файл Внутренняя память - Скачать резюме - Приостановить скачать + Продолжить Скачать + Остановить скачивание Отключить автоматическое информирование об ошибках Импортируйте шрифты поместив их в %s - Продолжать смотрю + Продолжить смотреть Удалите - Дополнительная инфо + Подробнее Для корректной работы этого поставщика может потребоваться VPN Этот поставщика - торрент, рекомендуется VPN Метаданные не предоставляются сайтом, загрузка видео будет неудачной, если они не существуют на сайте. @@ -151,7 +151,7 @@ Не удалось восстановить данные из %s Отсутствует разрешение на хранение. Пожалуйста попробуйте снова. Аккаунты - Обновления и резервное копирование + Обновления и резервное Информация Расширенный поиск Показывать трейлеры @@ -166,20 +166,20 @@ Язык приложения Ссылок не найдено Ссылка скопирована в буфер обмена - Восстановить к изначальному значению - Извините, приложение прекратило работу. Анонимный отчет об ошибке был отправлен разработчикам + Восстановить по умолчанию + Извините, приложение прекратило работу. Анонимный отчет об ошибке будет отправлен разработчикам Эпизод - Эпизоды + Эпизодов С Э Эпизоды не найдены Удалить файл - Возобновить + Продолжить -30 +30 Это будет удалено безвозвратно%s \nВы уверены\? - %dм + %d мин. \nосталось Завершено Год @@ -193,11 +193,317 @@ Другое Ошибка загрузки, проверьте разрешения хранилища Копировать ссылку - Автоматическая загрузка + Автоскачивание Загрузка. Зеркало Сезон Аниме приложение от тех же разработчиков Автоматически загружать еще не установленные плагины из добавленных репозиториев. Присоединится в Discord Бесплатно + %d мин. + %d ч. %d мин. + Фильмы + Мультфильм + Сериалы + Азиатские драмы + Видео + Мультфильмы + Документальные фильмы + OVA + NSFW + NSFW + Фильм + Сериал + Торрент + Документальный + Азиатская драма + Общие + Провайдеры + Макет + Расширения + Плеер + Резервное копирование данных + Отправлять данные только при вылетах + Использовано + Двойное нажатие для паузы + Коснитесь дважды правой или левой стороны для поиска вперед или назад + Нажмите в центре для паузы + Использовать системную яркость + Автоматически синхронизировать текущий прогресс эпизода + Ошибка резервного копирования %s + Восстановить данные из резервной копии + Показывать Филлер эпизоды для аниме + Показывать афиши из Kitsu + Некоторые телефоны не поддерживают новые установщики. Попробуйте использовать legacy (старый) вариант, если обновления не устанавливаются. + Выходит + Блокировка + Обновление не найдено + Изменить размер + Источник + Проверить обновления + Клон сайта + DNS через HTTPS + Удалить сайт + Оговорка + Синхронизация субтитров + Добавить клон существующего сайта с другим URL-адресом + Используется для обхода блокировок интернет провайдера + Путь скачивания + Давал бенен + Обновить + Основной цвет + Языки поставщиков + Название репозитория + Очистить историю + Referer + Дайте бенен разрабам + Ссылки + Макет + Макет приложения + Тема приложения + Добавить репозиторий + Убрать отметку + Вы уверены, что хотите выйти\? + Плагин скачан + Плагин удалён + Описание + Версия + Статус + Размер + Авторы + Поддерживается + VLC + MPV + Пропустить %s + Концовка + Используйте яркость системы в проигрывателе приложения вместо темного наложения + Обновить состояние хода просмотра + Данные сохранены + Показывает результаты поиска, разделенные по провайдеру + Поиск предварительных обновлений вместо полных выпусков + Повторить процесс настройки + Этот провайдер не поддерживает Chromecast + %s %d%s + Нет сезона + %d-%d + %d %s + Прямые трансляции + Прямая трансляция + Ошибка источника + Ошибка пульта + Ошибка рендера + Неожиданная ошибка плеера + Эпизод Chromecast + Воспроизведение на %s + Воспроизвести в браузере + Скачать субтитры + Знак качества + Переключение элементов интерфейса на плакате + Пропустить OP + Больше не показывать + Пропустить это обновление + URL сервера NGINX + Создать учётную запись + Добавить слежение + Добавлено %s + Синхронизировать + Оценено + %d из 10 + Посмотреть информацию о сбое + Предпочитаемый видеоплеер + Веб-браузер + Приложение не найдено + Все языки + Вступление + Титры + Отметить как просмотренное + Разрешение видеоплеера + Желаемое качество видео + Максимум символов + Длинна буфера + Кеш видео на диске + Размер буфера + Отчистить кеш видео и изображений + Вызывает сбои, если установлено слишком высокое значение на устройствах с небольшим объемом памяти, таких как Android TV. + Вызывает проблемы, если установлено слишком высокое значение на устройствах с небольшим объемом памяти, таких как Android TV. + Легкая новелла от тех же разработчиков + Язык + Плейлист HLS + Сначала установить расширение + Внутренний проигрыватель + Синопсис + Интро + hello@world.com + Зеркало Chromecast + Воспроизвести в приложении + По умолчанию + Возможности плеера + Субтитры + Расположение названия плаката + ТV (телевидение) + Телефон + Эмулятор + Под плакатом + parol123 + МоёИмяПользователя + Сменить учётную запись + Добавить учётную запись + МойКрутойСайт + example.com + Код языка (ru) + учётная запись + Автоматически + 127.0.0.1 + Обновления приложения + Резервная копия + Действия + Кэш + Жесты + Готово + Расширения + URL репозитория + Плагин загружен + \@string/home_play + Перемотка двойным нажатием + /\?\? + /%d + 18+ + Не удалось загрузить %s + плагины + SDR + TS + Удалить репозиторий + SD + 4K + Web + UHD + плагин + Пропустить настройку + Ошибка + Cam + Cam + HDR + DVD + WP + TC + HD + Blu-ray + Cam + HQ + Отключено: %d + %s %s + %s аутентифицировано + Не удается логин на %s + Макс + Мин + Контурный + С тенью + 1000 мс + Задержка субтитров + Без задержки субтитров + Используйте, если субтитры отображаются на %d мс слишком рано + Используйте, если субтитры отображаются %d мс слишком поздно + Нормально + Поднятые + Съешь ещё этих мягких французских булок, да выпей же чаю + Рекомендуется + Загружено %s + \@нить/аниме + \@нить/ova + Этикетка Dub + Сайт + Функции + Главное + Источник + Случайный + Скоро… + Этикетка Sub + Фон + Oтoбpaжeниe + Трейлер + %s (отключено) + Следующий + В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев. +\n +\nИз-за безмозглой DMCA-атаки со стороны Sky UK Limited 🤮 мы не можем привязать сайт репозитория в приложении. +\n +\nПрисоединяйтесь к нашему Discord или ищите в интернете. + Недопустимые данные + Разрешение и название + Предыдущий + Разрешение + Браузер + Библиотека + Обновленный (старый - новый) + Алфавитный (А до Я) + Алфавитный (Я до А) + Выбрать библиотеку + Открыть с + Похоже, ваша библиотека пуста :( +\nВойдите в аккаунт с библиотекой или добавьте сериалы в локальную библиотеку + Сортировка + Открытый список + Рейтинг (высокий - низкий) + Рейтинг (низкий - высокий) + Обновленный (новый - старый) + Сортировать по + PackageInstaller + Кодировка субтитров + Загрузить из файла + Рейтинг: %s + Скачано %d %s + Все %s уже скачаны + Начата загрузка %d %s… + Не скачано: %d + Скачать все плагины из этого репозитория\? + Включен безопасный режим + Скачано: %d + Обновлено %d плагинов + Загрузить из интернета + Загрузка обновления приложения… + Недопустимый URL + Применить при перезапуске + Отчеты ошибках + Что вы хотите увидеть + Смотрите видео на этих языках + Скачано файл + Изображение постера + Пакетная загрузка + Скачайте список сайтов, который вы хотите использовать + Отображать Аниме с Дубляжом/Субтитрами + Включить NSFW на поддерживаемых провайдерах + Удалять скрытые субтитры из субтитров + Дополнительно + Изменить вид интерфейса, чтобы соответствовать устройству + Аудио дорожки + Это также удалит все плагины репозитория + Просмотреть репозитории сообщества + Видео дорожки + Все расширения были отключены из-за сбоя, чтобы помочь вам найти то, которое вызывает проблемы. + Повтор + Слишком много текста. Не удалось сохранить в буфер обмена. + Установка обновления приложения… + Не удалось установить новую версию приложения + Файл безопасного режима найден! +\nНе загружаются никакие расширения при запуске, пока файл не будет удален. + Приложение будет обновлено после выхода + Похоже, этот список пуст, попробуйте переключиться на другой + Все субтитры заглавными + Показывать всплывающие окна для пропуска вступления/заключения + Фильтровать по предпочитаемому языку медиа + Неверный ID + Ссылка на стрим + Отображать рандомную кнопку на Главной странице + Рандомная кнопка + Legacy (старый) + Веб видеокаст + Не отправляет данные + Перезагрузить ссылки + Предпочтительные медиа + Опущенные + Объем перемотки плеера + Объем перемотка, используемый, когда плеер виден + Плеер показан - Перемотки объем + Плеер спрятан - Перемотки объем + Удалять лишнее из субтитров \ No newline at end of file diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml new file mode 100644 index 00000000..97039233 --- /dev/null +++ b/app/src/main/res/values-sk/strings.xml @@ -0,0 +1,107 @@ + + + Našla sa nová aktualizácia! +\n%s -> %s + Výplň + %dh %dm + Epizóda %d bude vydaná za + %s Ep %d + Ďalšia epizóda + Žánre + Zdielať + Otvoriť v prehliadači + Preskočiť načítavanie + Hrajú: %s + CloudStream + Plagát + %dd %dh %dm + %dm + %d min + \@string/result_poster_img_des + Plagát epizódy + Hlavný plagát + Prehrať s CloudStream + Nastavenia + Hľadať %s… + Pokračovať v sťahovaní + Hodnotenie: %.1f + Ísť späť + Rýchlosť (%.2fx) + Zmeniť poskytovateľa + Domov + Hľadať + Hľadať… + Sťahovanie + Žiadne dáta + Zrušiť + Kopírovať + Zavrieť + Uložiť + Stiahnuť + Stiahnuté + Ďalšie možnosti + Zdroje + Ísť späť + Sťahovanie zlyhalo + Sťahovanie pozastavené + Sťahovanie dokončené + Chyba pri načítavaní odkazov + Aktualizácia spustená + Interné úložisko + Načítavanie… + Dokončené + Plánujem pozerať + Zakázať automatické nahlasovanie chýb + Viac informácií + Záložky + Prehrať film + Prehrať upútavku + Sťahovanie + Sťahovanie zrušené + Dab + Zmazať súbor + Žiadny + Tit + Opätovné sledovanie + Prehrať súbor + Info + Prehrať živý prenos + Titulky + Prehrať epizódu + Pozastaviť sťahovanie + Skryť + Filtrovať záložky + Odstrániť + Použiť + Sťahovanie spustené + Vyčistiť + Prehrať + Nastaviť stav sledovania + Rýchlosť prehrávania + Farba obrysu + Farba okna + Typ hrany + Nastavenia titulkov + Farba pozadia + Farba textu + Vyvýšenie titulkov + Hľadať pomocou poskytovateľov + Písmo + Hľadať pomocou typov + Automaticky vybrať jazyk + Jazyk titulkov + Veľkosť písma + Nedarovali ste žiadne benény + Podržaním obnovíte predvolené nastavenia + %d benénov darovaných vývojárom + Stiahnuť jazyky + Odstrániť + Tento poskytovateľ je torrent, odporúča sa VPN + Importovať písma ich umiestnením do %s + Viac informácií + \@string/home_play + Pokračovať v sledovaní + Na správne fungovanie tohto poskytovateľa môže byť potrebná VPN + Stránka neposkytla žiadne metadáta, načítanie videa zlyhá, ak na stránke neexistuje. + Popis + \ No newline at end of file diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml new file mode 100644 index 00000000..b944b6b3 --- /dev/null +++ b/app/src/main/res/values-so/strings.xml @@ -0,0 +1,490 @@ + + + Metalaya: %s + %dm %ds %dd + %ds %dd + %dd + %s Xlq %d + Xalqadda %d waxa lasoo deyn doonaa + Daji + Dejintii ma guulaysan + Raadi + Tirtir + Haki + Horran + Le-ekaysii shaashadda + Kala jiid + Soo dhawee + Dhammaan + Muusik + Ku daawo CloudStream + Habaynta + Buuxiye + Beddel qaybiyaha + Cloudstream + Xawaaraha (%.2fx) + %d dqq + Kadinka + Qiimaysan: %.1f + App cusub baa soo baxay! +\n%s -> %s + Soo dejinta + Raadi + Dookhyo kale + Xog ma leh + Xalqadda xigta + Daawanaysid + Raadi %s… + Caynadda + Faafi + Ku fur barawsarka + Is dhaafi soo kicitaanka + Soo kacaya… + Raadi… + Kor u hay + Dib u daawasho + Dhamaystirmay + Soo dhacay + Ku talo jira + Midna + Daaro filinka + Daar goos-gooska + Midabka daaqadda + Saar faylka + Daar faylka + Si toos u daawo + Daalaco toorentiga + Qrl-hoosaadka + Dib u bilow dejinta + Jooji + Midabka dambeedka + Xigashooyinka + Dib u xidhiidhinaya… + Dib u noqo + Xalqadda daaro + Waad hakisey la degista + Way dhamaatay dejintu + Kaydka gudaha + Trj + Way bilaabmatay soo dejintu + Daalaco + Jooji cillad gudbinta iskeed ah + Waa la joojiyey dejintan + Way bilaabantay cuaboonaysiintu + Soo raridda lifaaqyadu way fashilantay + Qrl + Qari + Warbixin + Warbixin dheeraad ah + Habaynta Qoraal-hoosaadka + Midabka lakabka sare + Daar + Shaandhee caalamad-sanayaasha + Calaamad-sanayaasha + Xagee marinaysaa + Kaydi + Xawaaraha daaraha + Xajmiga farta + Haki dejinta + Ka dhig + Midabka farta + Xeji si aad asalkiisa ugu soo celiso + Koobi/ka reeb + Xidh + Tirtir + Nooca cidhifyada + Isticmaal xaddiga iftiinka ee moobilkaaga halkii aad adigu habayn lahayd ugu horreynba + Gurmadka iyo cusbooneysiinta + Waa la kaydiyet xogta gurmadka + Raadinta waxay kuugu kala qaybinaysaa mid kasta iyo qaybiyihiisa + Ma jirto xog la dirayo + Way fashilantay in xogta laga soo celiyo faylka gurmadka%s + Moobillada qaar ayaa awoodin iney Rakibaha cusub een isticmaalnay, kan ku day haddii cusbooneysiintu kuu shaqayn weydo. + +30 + Xogta waa la kaydiyey + Heerka + Ogolaanshaha lagu kaydinayo ma hayno. Fadlan isku day mar kale. + Koontooyinka + Xog + Si iskii ah usoo deji sidkanaha + Qari tayada muqaalka aad dooratay natiijada waxa aad raadiso + Raadin heer-sare ah + Keliya xogta waxaa ka dirayaa marka appku duqeeyo + Xalqad buuxis ah tusi anemiga + Dhiseyaasha appka sii beniin + Way fashilantay dejintu, hubi ogolaanshaha isticmaalka kaydka + Xalqadda koromakaastiga + Soo dejiso Qoraal-hoosaadyada + Summadda Tayada + Humaagga-bixinta koromokaastiga + Asalkiisa + Isla appkan ku daawo + Ku daawo %s + Ku daawo barawsarka + Dejinta iskeed ah + Deji toorentiga(mirror) + Cusbooneysii lifaaqyada + Eeg iney jirto cusbooneysiin + Summadda codeynta + Sumadda Qrl-hoosaadka + Ciwaanka + Maxaa ka muuqan doona boodhka filinka + Ma jiro wax ku cusub Appka + Quful + Cabbirka + isha + Is dhaafi + Mar dambe ha i tusin + Is dhaafi + Cusbooneysii + Tayada aad doorbiddo + Qaabka + Qaab emulator + Waxa uu ku tusayaa badhin aad si xukasho la\'aan ah ugu dooran kartid + Kor-u-baxa Qrl-hoosaadka + Farta + Muuq dambeedka + Ku raadi qaybiyeyaasha + Ku raadi noocyada + %d oo beniin ayaad siisey dhiseyaasha + Wax beniin ah maad bixin + Iskeed-u-doorashada luuqadda + Luuqadda dejinta + Luuqadda Qoraal-hoosaadka + Xogta muqaalkan ma bixinno, ma shaqayn doonto haddii aannu weyno xogta muqaalka. + Dhig nooca farta aad dooneyso %s + Sii wado daawashada + Ka saar + Xog dheeri ah + Tusi Boodhka Kitsu + Waxa laa raadinayaa haddii ay wax jiraan baahin-horaad ah, halkii la sugi lahaa baahin buuxda + Si iskii ah u cusbooneysii sidkanaha + Si iskii ah usoo deji dhamaan sidkanayaasha aan weli lasoo dejin ee ku jira kaydka aad keentay. + Marka ugu horreysa ee aad appka furto ayaad arkaysaa haddii wax cusub yihiin + Light novel app, isla dhiseyaasha appkan baa leh + Tus wixii appka ku cusub + Dib uso bilow fadhiisinta appka + %d %s + Ku cusbooneysii baahinta ugu horreysa + Rakibaha APKga + Socda + Github + Filin + Anime app, isla dhiseyaashan appkan baa leh + Kal ma leh + Discord naga soo qabo + Beeniinyada aad bixisey + Luuqadda + Bixiyahan kuma shaqeeyo koromakaastiga + Asalkiisii hore kusoo celi + Dulucuda + Raalliahow, wuu duqeeyey appku, waxa warbixin cillad-saarka ka caawisa loo direy dhiseyaasha + Kalka + %s %d%s + Xalqad + Xalqado + %d illaa %d + K + X + Kartoon + Daraamada eeshiyaanka + Tirtir faylka + Sii wado + -30 + Dhamaantii waa la saari doona %s +\nSow ma hubtid\? + Fashil ka yimi xigashada + %ddq +\nAyaa hadhsan + Dhamaystirmay + Sannadka + Qiimaynta + Muddada + La isticmaalay + Qrl-hoosaad ma leh + Sidiisaa + Bilaash + Appka + Kartoob + Anemi + Daaraha ayaa ku fashilmay + OVA + Toorenti + Tooska + OVA + Daraamada Eeshiyaanka + Daaraha korkiisa + kaydka kumeelgaadhka + Ka reebo fadhigan + Kuwa kale + Taxane + Anemi + Dhokumentari + Toorentiyo + Dhokumentariyo + Toos + CEEB + Kuwa kale + Fashil fog + DNS ka horreysi HTTPS + Maxaa ku cusub appka + Gurmad + Kordhiyeyaal + Qoraal-hoosaadyada + Muqaal + Jiid si aad u habaysato + Laga yaabaa inaad u baahato VPN si qaybiyahan kuugu shaqeeyo + Qaybiyahan waa toorenti, inaad VPN isticmaasho ayaa wanaagsan + Ka sii wado daawashada daaqad yar oo appska kale dul istaagi doonta + Jiid si aad u dhaafiso + Eeg xogaha hoose 🐈 + Badhinka xajmiga daaraha + Fashil ka yimi dhiibaha + Koobu garee lifaaqa + Sharraxaad ma leh + Sharraxaadda + Duluc ma leh + Sawir-daaqadlaha + Habaynta Qrl-hoosaadka daaraha + Qrl-hoosaadka koromakaastiga + Hareeraha madow ka saar markaad daawanayso + Qoraal-hoosaadka + Habaynta Qrl-hoosaadka Koromakaastiga + Habka Xawaare-kordhinta + Waxaad kordhin karta xawaaraha aad ku daawanayso + Midig iyo bidix u jiid si aad marba dhinac ugu dhaafiso muqaalka + Laba jeer taabo midig ama bidix si aad u dhaafiso ama ku ceshato + Laba jeer taabo si aad u dhaafiso + Inta aad is dhaafinayso + Laba jeer taabo si aad u qallajiso + Dhexda taabo si aad u qallajisato + Gurmad u samee xogta + Iftiinka moobilka istcimaal + Cusbooneysii marba halkaad marayso daawashada + Tus goos-gooska + Waa fashilmay kaydinta gurmadku %s + Kasoo celi Xogta gurmadka aad kaydsatay + Dhererka K. kumeelgaadhka ah + Noocyada muqaal eed doorbiddo + Qaab TV + Heerka Kayd-yaraha + Masax kayd yaraha sawirrada/muqaalka + Laga yaaba inuu appku duqeeyo haddii aad badiso. + Laga yaabaa inuu wax xumeeyo haddii aad badiso, weliba aaladaha qaadka yar, sida Androyd tiifiiga. + Wuxuu muhiim u yahay inaad dhaafto Xannibaadaha ISPga + Ka saar fadhigan + Ku dar ka-reeb fadhi hore u jirey, laakiin hayb cusub wata + Aaraar + Haybta Seerfarka NGINX + Tusi Anemaha Turjuman/Leh Qrl-hoosaad + Lifaaqyada + Barta kaydka + Astaamaha + Badhinka X. la\'aanta + Qaybiyeyaasha + Iskii + Qaab moobil + Falal + Kayd-yare + Ishaarooyinka + Astaamaha daaraha + Guud + Tusi CEEBta qaybiyayaasha ku shaqeeya + Qaabka + Midab asaaska + Horidda Qoraal-hoosaadka + Nashqadda Appka + Goobta Cinwaanka boodhka + La degtay + Soo degaya + Ka Saar + Kor ama hoos u jiid labada dhinaca ee daaraha si aad u maamusho codka ama iftiinka + Iskii-u-daarka xalqadda xigta + Si iskii ah ayuu u daarayaa xalqadda xigta marka aad dhamaysato kaad daawanayso + Si iskii ah ha ula socdo appku hadba xalqadda aad marayso + Lifaaq lagama helin + Waad koobiyeysay lifaaqa + Daari xalqaddan + Xalqado ma leh + Wabsaydka + Filimaan + Taxanayaal + CEEB + Tirada inta ka muuqanaysa magaca filinka + Luuqadda bixiyaha + Qaabka Appka + Ka saar kuwa la daawaday + Eeg Kuwa Dadka kale Rakibeen + Codad kale + Far weyn ka dhig Qrl-hoosaadka + Sharraxaad + Xaaladda + Xajmiga + Safe mode ayaa daaran + Dhammaan Kordhiyeyaasha waa la wada demiyey cillad timi aawadeed si aad u ogaato ka cillada sababaya. + %s (La xayiray) + Marka dambe eed gasho isticmaal + Eeg warbixinra Cilladda + Qiimeynta: %s + Lahaanshaha + Qaybtii + HLS playlist + Ku shaqeeya + Luuqadda + Rakib kordhiyahan ugu horreyn + Muqaal daaraha aad doorbiddo + Masax taariikhda + Taariikhda + Furitaanka + Qoraalku wuu dheeryahay. Ma koobi garayn kartid. + Haa + Tusi badhin aad iska dhaafin karto bilowga/dhamaadka + Ku caalamadi in la daawaday + Dejinaya cusbooneysiinta appka… + Ma hubtaa inaad ka baxdid\? + May + PackageInstaller + Rakibaya cusbooneysiinta… + Waa la rakibi kari waayay qaybtan cusub ee appka + Lagama-maarmaanka + Marka aad ka baxdo baa ka cusbooneysiin doona appka + Dhig ciwaanka boodhka hoostiisa + Akoonka + Soo gal + Akoon geli + %s xaqiijisan + iiwarran@sxb.com + 127.0.0.1 + MagacKaYaabAh + Ka bax + Tusaale.com + Waxa lagu daray %s + Midna + Websaydkayga.com + Koodhka Luuqadda (en) + %s %s + Akoon samee + Talantaalli + Qiimaysan + Beddel akoonka + Ku dar raad-raace + /%d + %d / 10 + /\?\? + Waa lagu guuldarraystey inaad ku gasho %s + Caadi + Ugu weyn + Ugu yar + Hoos u degsan + Lakabka sare + Hagaaji Qrl-hosaadka + Isticmaal intan haddii Qrl-hoosaadku ay %d ms soo habsaanayaan + 4K + Dib u noqo + Hadhka + Kor-u-qaadan + Isticmaal intan haddii Qrl-hoosaadku ay %d ms soo hordhacayaan + 1000 ms + Ka guud + La jilaya + Sheekheenna wiilkiisa curad, gabadhiisu foox la\'aan ma heesi karto + HQ + Lagugula taliyey + Waxa laso kiciyey %s + Faylashaada ka keen + Internetka ka keen + Waa ka dejiyey faylka + Wajhadda dambe + Cam + HD + TS + SDR + Daaraha + Ciwaanka + Ka saar Qrl-hoosaadka asalka ah + Ka saar bararsanaanta Qrl-hoosaadka + Goor dhow… + Xigashada + Xulasho la\'aan + Cam + Cam + TC + Blu-ray + SD + WP + UHD + HDR + DVD + Sawirka Boodhka + Web + Hayb aan sax ahayn + Tayada iyo ciwaanka + Tayada + Aqoonsi aan sax ahayn + Xog ana sax ahayn + Xumaaday + Noocyada Muqaalka + Waxba kamaad bedelin daahitaanka Qrl-hoosaadka + U kala hor sida luuqadaha aad doorbidday + X.la\'aanta xigta + Sidkane + Sidkanayaasha + Saar kayd-weynaha + Dejiso liiska websaydyada aad dooneyso + Waxa la dejiyey: %d + Dejin xidhmeed + Waxa la saarayaa dhammaan sidkanayaasha kayd-weynaha + Xayiran: %d + Aan dejinayn: %d + Waxa la cusbooneysiiyey %d sidkane + Ugu horreyn cloudstream ma laha wax websaydyo uu filimaanta kasoo xigto, waxay noqonaysaa inaad adigu rakibato reboositarradooda... +\n +\nSababtuna waa in mar dhexdaas ah na dacweeyeen shirkadda Sky UK Limited🤮, markaa si aan mar dambe taasi u dhicin anagu kuma rakibi karno... +\n +\nDiscord naga soo qabo ama internetka ka baadh. + Soo deji dhamaan sidkanayaasha reboositarkan\? + Boodhka + Boodhka xalqadda + Boodhka weyn + Lambarkasirta123 + Dib-u-dhaca Qrl-hoosaadka + Dheeraadka + Goo-gooska + Lifaaqa daalacashada + Tixraacaha + Xiga + Ku daawo muuqaallada luuqadahan + Kii hore + Is dhaafi fadhiisintan + Beddel sida appku u muuqdo si uu ugu habboonaado moobilekaaga + Warbixinra cillad-saarka + Maxaad rabtaa inaad daawato + Dhan + Ku dar kayd-weyne + Magaca kayd-weynaha + Haybta kayd-weynaha + Waa la keenay sidkanaha + Waa ka dejiyey sidkanaha + Waa la tiray sidkanahan + Waa ka keeni kari waayey %s + 18+ + Waa la dejinayaa %d %s… + Waa la dejiyey %d %s + Dhammaan %s way dejisan yihiin + Kordhiyeyaasha + Liiska bulshada kale + Muqaal daaraha appka + VLC + MPV + Web Video Cast + Barawsarka + Kuuguma jiro appkaasi + Dhammaan luuqadaha + Is dhaafi %s + Dhamaadka + Soo koobidda + Dhamaad isku qasan + Bilowga + Bilow isku qasan + Qoraalka dhamaadka + \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 7e83f840..32336b66 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -2,7 +2,8 @@ Betygsatt: %.1f Hastighet (%.2fx) - Ny uppdatering hittad!\n%s -> %s + Ny uppdatering hittad! +\n%s -> %s CloudStream Hem Sök @@ -32,7 +33,7 @@ Undertexter Försök ansluta igen… Gå tillbaka - @string/result_poster_img_des + \@string/result_poster_img_des Spela Avsnitt Ladda ner Intern lagring @@ -43,7 +44,7 @@ Inaktivera automatisk felrapportering Mer information Hide - @string/result_poster_img_des + \@string/result_poster_img_des Spela upp Info Nästa @@ -124,7 +125,8 @@ A Ta bort nerladdad fil Ta bort - %s kommer att raderas permanent\nÄr du helt säker? + %s kommer att raderas permanent +\nÄr du helt säker\? Pågående Färdig Status diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 12b4e96c..9e5b29d4 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -5,7 +5,7 @@ %s Ep %d Poster - @string/result_poster_img_des + \@string/result_poster_img_des Episode Poster Main Poster Next Random @@ -15,7 +15,8 @@ Bilis (%.2fx) Rated: %.1f - Bagong update!\n%s -> %s + Bagong update! +\n%s -> %s CloudStream Home Maghanap @@ -145,7 +146,8 @@ Tanggalin I-pause I-resume - This will permanently delete %s\nAre you sure? + This will permanently delete %s +\nAre you sure\? Patuloy Tapos na Katayuan diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 969acd70..807716d8 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -13,25 +13,26 @@ %d %s Ep %d Cast: %s - Bölüm %d şu tarihte yayınlanacak: + Bölüm %d şu tarihte yayınlanacak %dd %dh %dm %dh %dm %dm Poster - @string/result_poster_img_des - Episode Poster - Main Poster - Next Random - @string/play_episode - Go back - @string/home_change_provider_img_des + Afiş + Bölüm Posteri + Ana Poster + Sonraki Rastgele + \@string/play_episode + Geri git + \@string/home_change_provider_img_des Change Provider Preview Background Hız (%.2fx) Puan: %.1f - Yeni güncelleme bulundu!\n%s -> %s + Yeni güncelleme bulundu! +\n%s -> %s Filler %d dakika CloudStream @@ -45,7 +46,7 @@ Veri yok Daha fazla seçenek Sonraki bölüm - @string/synopsis + \@string/synopsis Türler Paylaş Tarayıcıda aç @@ -122,7 +123,7 @@ İzlemeye devam et Kaldır Daha fazla bilgi - @string/home_play + \@string/home_play Bu sağlayıcının düzgün çalışması için bir VPN gerekebilir Bu sağlayıcı bir torrent. VPN önerilir Metadata site tarafından sağlanmamış, veri site\'de bulunmuyorsa video yüklenmesi başarısız olacak. @@ -204,13 +205,15 @@ Bölüm bulunamadı Dosyayı sil Sil - @string/sort_cancel + \@string/sort_cancel Durdur Sürdür -30 +30 - %s dosyası tamamen silinecek\nEmins misiniz? - %dm\nkaldı + %s dosyası tamamen silinecek +\nEmins misiniz\? + %dm +\nkaldı Devam ediyor Tamamlandı Durum @@ -241,8 +244,8 @@ Film Dizi Çizgi film - @string/anime - @string/ova + \@string/anime + \@string/ova Torrent Belgesel Asya draması @@ -257,7 +260,7 @@ Bölümü Chromecast ile yayınla Bağlantıyı Chromecast ile yayınla Uygulamada oynat - %s\'de\/da oynat + %s\'deda oynat Tarayıcıda oynat Linki kopyala Otomatik indir @@ -304,22 +307,7 @@ Yakınlaştır Disclaimer 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. - + 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. Genel Rastgele butonu Ana sayfada rastgele butonunu göster @@ -370,7 +358,7 @@ Senkronize et Puan %d / 10 - /?? + /\?\? /%d %s için doğrulama başarılı %s için doğrulama başarısız @@ -380,16 +368,16 @@ Hepsi Maksimum Minimum - @string/none + \@string/none Dış hat Çökmüş Gölge Yükseltilmiş Alt yazı senkronu - 1000ms + 1000 ms Alt yazı gecikmesi - Alt yazılar %dms erken gözüküyorsa bunu kullanın - Alt yazılar %dms geç gözüküyorsa bunu kullanın + Alt yazılar %d ms erken gözüküyorsa bunu kullanın + Alt yazılar %d ms geç gözüküyorsa bunu kullanın Alt yazı gecikmesi yok Poster - @string/result_poster_img_des + \@string/result_poster_img_des Episode Poster Main Poster Next Random @@ -20,7 +20,8 @@ Tốc độ (%.2fx) Đánh giá: %.1f - Đã có bản cập nhật mới!\n%s -> %s + Đã có bản cập nhật mới! +\n%s -> %s Bộ lọc %d phút CloudStream @@ -65,7 +66,7 @@ Tải thành công Trực tiếp Đã có lỗi xảy ra - Bộ nhớ máy + Bộ nhớ trong Lồng Tiếng Phụ Đề Xóa Tệp @@ -109,16 +110,15 @@ Tiếp tục xem Loại bỏ Thông tin thêm - @string/home_play + \@string/home_play Bạn có thể sẽ cần sử dụng VPN để xem phim này Phim này được chiếu dưới dạng Torrent. Hãy sử dụng VPN để xem - Siêu dữ liệu không được cung cấp bởi trang web, quá trình tải video sẽ không thành công nếu nó không tồn tại trên trang web. Thông tin phim Đang cập nhật Không tìm thấy thông tin Hiển thị Logcat 🐈 Chế độ cửa sổ nhỏ - Tiếp tục xem phim khi thoát app hoặc đang tìm kiếm + Tiếp tục xem phim khi thoát ứng dụng hoặc khi tìm kiếm Bật nút thu phóng khi xem Xóa khoảng đen của phim Phụ đề @@ -147,7 +147,7 @@ Đã tải dữ liệu sao lưu Không thể khôi phục dữ liệu từ %s Khôi phục dữ liệu thành công - Thiếu quyền truy cập bộ nhớ, hãy thử lại + Thiếu quyền truy cập bộ nhớ, hãy thử lại. Lỗi khi sao lưu %s Tìm kiếm Tài khoản @@ -159,8 +159,8 @@ Không gửi dữ liệu Hiển thị tập phụ cho anime Hiển thị trailer - Hiển thị poster từ kitsu - Ẩn chất lượng Video khi tìm kiếm + Hiển thị poster từ Kitsu + Ẩn chất lượng video khi tìm kiếm Tự động cập nhật plugin Hiển thị thông báo cập nhật App Tự động tìm kiếm và thông báo khi có bản cập nhật mới @@ -194,8 +194,10 @@ Tiếp Tục -30 +30 - %s sẽ bị xoá vĩnh viễn\nBạn có chắc chắn muốn xóa? - %dm\ncòn lại + %s sẽ bị xoá vĩnh viễn +\nBạn có chắc chắn muốn xóa\? + %dm +\ncòn lại Đang Chiếu Hoàn Thành Trạng Thái @@ -208,7 +210,7 @@ Không có phụ đề Mặc Định Còn trống - Đã dùng + Đã sử dụng App Phim Lẻ @@ -226,8 +228,8 @@ Phim Lẻ Phim Bộ Hoạt Hình - @string/anime - @string/ova + Anime + \@string/ova Torrent Phim Tài Liệu Truyền Hình Châu Á @@ -259,7 +261,7 @@ Khóa Thu Phóng Tuỳ chọn - Tập tiếp + Tua nhanh Không hiện lại Bỏ qua Cập nhật @@ -270,8 +272,8 @@ Thời lượng bộ nhớ đệm Dung lượng video cache Xoá hình ảnh và video - Sẽ gây lỗi nếu đặt quá cao. Không thay đổi nếu máy có dung lượng ram thấp, chẳng hạn như Android TV hoặc điện thoại cũ - Sẽ thể gây lỗi trên các máy có dung lượng lưu trữ thấp, chẳng hạn như thiết bị Android TV nếu bạn đặt nó quá cao + Sẽ gây lỗi nếu đặt quá cao trên máy có dung lượng ram thấp như Android TV. + Sẽ gây lỗi nếu đặt quá cao trên máy có dung lượng lưu trữ thấp như Android TV. DNS over HTTPS Rất hữu ích để bỏ chặn ISP Sao chép trang web @@ -322,7 +324,7 @@ Đồng bộ Đánh giá %d / 10 - /?? + /\?\? /%d Đã xác thực %s Không thể xác thực %s @@ -408,7 +410,7 @@ Đã xoá plugin Không tải được %s 18+ - Bắt đầu tải %d %s + Bắt đầu tải %d %s… Tải xuống %d %s thành công Toàn bộ %s đã được tải xuống Tải hàng loạt @@ -420,11 +422,15 @@ Đã tải: %d Đã vô hiệu: %d Không tải: %d - Thêm kho lưu trữ để cài tiện ích + CloudStream không có sẵn trang web nào. Bạn cần cài đặt các trang web từ kho lưu trữ. +\n +\nDo Sky UK Limited đã gỡ xuống theo DMCA một cách thiếu suy nghĩ 🤮 chúng tôi không thể cài sẵn trang web. +\n +\nHãy tham gia Discord của chúng tôi hoặc tìm kiếm trực tuyến. Xem kho lưu trữ của cộng đồng Danh sách công khai In hoa toàn bộ phụ đề - Tải toàn bộ plugin từ kho lưu trữ này? + Tải toàn bộ plugin từ kho lưu trữ này\? %s (Đã vô hiệu) Thêm Âm thanh @@ -435,4 +441,82 @@ Xem thông tin sự cố Lịch sử Đánh dấu là đã xem + Tự động tải xuống plugin + Thiết lập lại + Bộ cài APK + Một số máy không hỗ trợ trình cài đặt gói mới. Hãy thử tùy chọn cũ nếu các bản cập nhật không cài đặt. + %s %d%s + Xem giới thiệu + Tự động tải plugin còn thiếu. + Bắt đầu cập nhật + Liên kết + Danh sách HLS + Trình phát ưu tiên + Trình phát mặc định + Đánh giá: %s + Không + Phiên bản + Tác giả + Cập nhật ứng dụng + Sao lưu + Tiện ích + Hành động + Cache + Cử chỉ + Tính năng trình phát + Phụ đề + Bố cục + Mặc định + Giao diện + Tính năng + Đã cập nhật %d plugin + Mô tả + Trạng thái + Kích thước + Hỗ trợ + Ngôn ngữ + Cài đặt tiện ích trước + VLC + MPV + Web Video Cast + Trình duyệt web + Không thấy ứng dụng + Tất cả ngôn ngữ + Tua %s + Mở đầu + Kết thúc + Tóm tắt + Mở đầu tuỳ chọn + Kết thúc tuỳ chọn + Danh đề + Giới thiệu + Xoá lịch sử + Hiển thị nút tua nhanh cho mở đầu/kết thúc + Văn bản quá dài. Không thể lưu vào bộ nhớ tạm. + Xoá khỏi đã xem + Bạn có chắc muốn thoát\? + + Đang tải bản cập nhật… + Đang cài bản cập nhật… + Không thể cài đặt phiên bản mới + Ứng dụng sẽ được cập nhật khi thoát + Thư viện + Trình duyệt + Plugin đã tải + Mặc định + Tải lên (Mới đến Cũ) + Tải lên (Cũ đến Mới) + Thư viện của bạn đang trống :( +\nHãy đăng nhập vào thư viện hoặc thêm phim vào thư viện cục bộ + Mở với + Siêu dữ liệu không có sẵn, video sẽ không được tải nếu nó không tồn tại trên trang web. + PackageInstaller + Sắp xếp + Xếp hạng (Cao đến Thấp) + Xếp hạng (Thấp đến Cao) + Chữ cái (Z đến A) + Sắp xếp + Có vẻ như danh sách này trống, hãy thử chuyển sang danh sách khác + Chữ cái (A đến Z) + Chọn Thư viện \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 764304d7..a8341d46 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -19,19 +19,20 @@ %dm 封面 - @string/result_poster_img_des + \@string/result_poster_img_des 劇集封面 主封面 隨機下一個 - @string/play_episode + \@string/play_episode 返回 - @string/home_change_provider_img_des + \@string/home_change_provider_img_des 更改片源 預覽背景 速度(%.2fx) 評分:%.1f - 發現新版本!\n%s -> %s + 發現新版本! +\n%s -> %s 填充 %d 分鐘 CloudStream @@ -45,7 +46,7 @@ 無資料 更多選項 下一集 - @string/synopsis + \@string/synopsis 類型 分享 在瀏覽器中打開 @@ -122,7 +123,7 @@ 繼續觀看 移除 更多資訊 - @string/home_play + \@string/home_play 此片源可能需要 VPN 才能正常使用 此片源是種子,建議使用 VPN 站點不提供元數據,如果站點上不存在元數據,影片載入將失敗。 @@ -149,8 +150,7 @@ 輕按兩下以控制進度 輕按兩下以暫停 輕按兩下以控制進度時間 - 在右側或左側輕按兩次以向前或向後快轉 - + 在右側或左側輕按兩次以向前或向後快轉 輕按兩下中間以暫停 使用系統亮度 在應用程序播放器中使用系統亮度替代黑色遮罩 @@ -205,12 +205,13 @@ 未找到劇集 刪除文件 刪除 - @string/sort_cancel + \@string/sort_cancel 暫停 繼續 -30 +30 - 這將永遠刪除 %s\n你確定嗎? + 這將永遠刪除 %s +\n你確定嗎\? %d 分鐘 \n剩餘的 連載中 @@ -243,8 +244,8 @@ 電影 電視劇 卡通 - @string/anime - @string/ova + \@string/anime + \@string/ova 種子 紀錄片 亞洲劇 @@ -306,22 +307,7 @@ 縮放 免責聲明 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. - + 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. 通用 隨機按鈕 在主頁中顯示隨機按鈕 @@ -372,7 +358,7 @@ 同步 評分 %d / 10 - /?? + /\?\? /%d %s 已驗證 無法在 %s 登入 @@ -382,7 +368,7 @@ 全部 最大 最小 - @string/none + \@string/none 輪廓 凹陷 陰影 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 9545956e..c57e3ca1 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -19,19 +19,20 @@ %dm 封面 - @string/result_poster_img_des + 封面 剧集封面 主封面 随机下一个 - @string/play_episode + \@string/play_episode 返回 - @string/home_change_provider_img_des + \@string/home_change_provider_img_des 更改片源 预览背景 速度(%.2fx) 评分:%.1f - 发现新版本!\n%s -> %s + 发现新版本! +\n%s -> %s 填充 %d 分钟 CloudStream @@ -45,7 +46,7 @@ 无数据 更多选项 下一集 - @string/synopsis + \@string/synopsis 类型 分享 在浏览器中打开 @@ -122,7 +123,7 @@ 继续观看 移除 更多信息 - @string/home_play + \@string/home_play 此片源可能需要 VPN 才能正常使用 此片源为种子文件,建议使用 VPN 站点不提供元数据,如果站点上不存在元数据,视频加载将失败。 @@ -149,12 +150,10 @@ 双击控制进度 双击暂停 双击控制进度时间 - 在左右侧双击快进或快退 - + 在左右侧双击快进或快退 双击中间暂停 使用系统亮度 - 在应用播放器中使用系统亮度替代黑色遮罩 - + 在应用播放器中使用系统亮度替代黑色遮罩 更新观看进度 自动同步当前剧集进度 从备份中恢复数据 @@ -194,8 +193,7 @@ 链接已复制到剪贴板 播放剧集 重置为默认值 - 抱歉,应用崩溃了,将发送一份匿名错误报告给开发者 - + 抱歉,应用崩溃了,将发送一份匿名错误报告给开发者 %s %d%s 无季 @@ -208,12 +206,13 @@ 未找到剧集 删除文件 删除 - @string/sort_cancel + \@string/sort_cancel 暂停 继续 -30 +30 - 这将永久删除 %s\n您确定吗? + 这将永久删除 %s +\n您确定吗\? %d 分钟 \n剩余 连载中 @@ -246,8 +245,8 @@ 电影 电视剧 卡通 - @string/anime - @string/ova + \@string/anime + \@string/ova 种子 纪录片 亚洲剧 @@ -309,22 +308,7 @@ 缩放 免责声明 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. - + 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. 通用 随机按钮 在主页中显示随机按钮 @@ -375,7 +359,7 @@ 同步 评分 %d / 10 - /?? + /\?\? /%d 已验证 %s 无法登录到 %s @@ -385,7 +369,7 @@ 全部 最大 最小 - @string/none + \@string/none 轮廓 凹陷 阴影 @@ -552,4 +536,22 @@ 更新开始 应用退出后将会更新 插件已下载 + 从已观看中移除 + 发现安全模式文件! +\n启动时不加载任何扩展,直到文件被删除。 + 浏览器 + + 排序方式 + 排序 + 评分(从高到低) + 评分(从低到高) + 更新(从新到旧) + 更新(从旧到新) + 字母排序(从 A 到 Z) + 字母排序(从 Z 到 A) + 选择库 + 打开方式 + 看来您的库是空的 :( +\n登录库账户或添加节目到您的本地库 + 看来此列表是空的,请尝试切换到另一个 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 85eb3a48..61ff0c2b 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -36,6 +36,8 @@ #F53B66 #BEC8FF ?attr/colorPrimaryDark + #4C3115 + #FFA662 #FF6F63 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6c10d865..778f34c9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,5 @@ - - + search_providers_list app_locale @@ -32,6 +31,8 @@ double_tap_enabled_key double_tap_pause_enabled_key double_tap_seek_time_key2 + android_tv_interface_off_seek_key + android_tv_interface_on_seek_key swipe_vertical_enabled_key autoplay_next_key display_sub_key @@ -80,7 +81,7 @@ %dm Poster - @string/result_poster_img_des + Poster Episode Poster Main Poster Next Random @@ -110,6 +111,7 @@ Genres Share Open In Browser + Browser Skip Loading Loading… Watching @@ -229,6 +231,7 @@ Storage permissions missing. Please try again. Error backing up %s Search + Library Accounts Updates and backup Info @@ -362,6 +365,10 @@ Video buffer length Video cache on disk Clear video and image cache + Player Shown - Seek Amount + The seek amount used when the player is visible + Player Hidden - Seek Amount + The seek amount used when the player is hidden Causes crashes if set too high on devices with low memory, such as Android TV. Causes problems if set too high on devices with low storage space, such as Android TV. DNS over HTTPS @@ -399,6 +406,7 @@ Extensions Actions Cache + Android TV Gestures Player features Subtitles @@ -612,9 +620,20 @@ Downloading app update… Installing app update… Could not install the new version of the app - Legacy PackageInstaller App will be updated upon exit - - + Sort by + Sort + Rating (High to Low) + Rating (Low to High) + Updated (New to Old) + Updated (Old to New) + Alphabetical (A to Z) + Alphabetical (Z to A) + Select Library + Open with + Looks like your library is empty :(\nLogin to a library account or add shows to your local library + Looks like this list is empty, try switching to another one + Safe mode file found!\nNot loading any extensions on startup until file is removed. + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b14cd189..78c62c69 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -90,14 +90,17 @@ @font/google_sans 0dp + + + + + + diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index d9b7e247..0e5bd84f 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -149,4 +149,31 @@ android:key="@string/video_buffer_clear_key" android:title="@string/video_buffer_clear_settings" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/settings_updates.xml b/app/src/main/res/xml/settings_updates.xml index ba098c8c..f2ec6747 100644 --- a/app/src/main/res/xml/settings_updates.xml +++ b/app/src/main/res/xml/settings_updates.xml @@ -2,7 +2,7 @@