From c7c1f12dade36a515fb7a09ececc3f1d10a7b0d1 Mon Sep 17 00:00:00 2001 From: KillerDogeEmpire Date: Mon, 6 Feb 2023 15:28:40 -0800 Subject: [PATCH 001/570] Added a way for easy mal and anilist tracker, All credit gos to Hexated for helping me --- .../com/lagradost/cloudstream3/MainAPI.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 73859021..98367b11 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -163,6 +163,18 @@ object APIHolder { return null } + private suspend fun getTracker(title: String?, type: String?, year: Int?): Tracker { + val res = app.get("https://api.consumet.org/meta/anilist/$title") + .parsedSafe()?.results?.find { media -> + (media.title?.english.equals(title, true) || media.title?.romaji.equals( + title, + true + )) || (media.type.equals(type, true) && media.releaseDate == year) + } + return Tracker(res?.malId, res?.aniId, res?.image, res?.cover) + } + + fun Context.getApiSettings(): HashSet { //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) @@ -1590,3 +1602,30 @@ 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, +) + +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(), + + ) From 830fd7ee55882353c80c3c6939cda8d98b97f4e9 Mon Sep 17 00:00:00 2001 From: KillerDogeEmpire Date: Tue, 7 Feb 2023 14:52:34 -0800 Subject: [PATCH 002/570] Made CodeFactor Fucking Happy --- app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 98367b11..89ec315c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -166,10 +166,8 @@ object APIHolder { private suspend fun getTracker(title: String?, type: String?, year: Int?): Tracker { val res = app.get("https://api.consumet.org/meta/anilist/$title") .parsedSafe()?.results?.find { media -> - (media.title?.english.equals(title, true) || media.title?.romaji.equals( - title, - true - )) || (media.type.equals(type, true) && media.releaseDate == year) + media.title?.english.equals(title, true) || media.title?.romaji.equals( title, true ) + || media.type.equals(type, true) && media.releaseDate == year } return Tracker(res?.malId, res?.aniId, res?.image, res?.cover) } From b08608d31a0b6813135ec89d6ce61ce55f49bc1b Mon Sep 17 00:00:00 2001 From: Blatzar <46196380+Blatzar@users.noreply.github.com> Date: Wed, 8 Feb 2023 10:19:08 +0100 Subject: [PATCH 003/570] prettified the getTracker method --- .../com/lagradost/cloudstream3/MainAPI.kt | 67 +++++++++++++++++-- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 89ec315c..5ccaa762 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -163,12 +163,34 @@ object APIHolder { return null } - private suspend fun getTracker(title: String?, type: String?, year: Int?): Tracker { - val res = app.get("https://api.consumet.org/meta/anilist/$title") + /** + * 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 mainTitle Title to search by and match + * @param secondaryTitle Optional extra title if you have multiple titles and want extra guarantee to match. + * @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( + mainTitle: String, + secondaryTitle: String?, + types: Set?, + year: Int? + ): Tracker { + val res = app.get("https://api.consumet.org/meta/anilist/$mainTitle") .parsedSafe()?.results?.find { media -> - media.title?.english.equals(title, true) || media.title?.romaji.equals( title, true ) - || media.type.equals(type, true) && media.releaseDate == year + val matchingYears = year == null || media.releaseDate == year + val matchingTitles = + (media.title?.isMatchingTitles(mainTitle) == true || media.title?.isMatchingTitles( + secondaryTitle + ) == true) + + val matchingTypes = types?.any { it.name.equals(media.type, true) } == true + matchingTitles && matchingTypes && matchingYears } + return Tracker(res?.malId, res?.aniId, res?.image, res?.cover) } @@ -1611,7 +1633,12 @@ data class Tracker( 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, @@ -1624,6 +1651,32 @@ data class Results( ) data class AniSearch( - @JsonProperty("results") val results: ArrayList? = arrayListOf(), + @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() + } + } + } +} From 58763708f7888899ced9fe79a2d9a90666010631 Mon Sep 17 00:00:00 2001 From: Blatzar <46196380+Blatzar@users.noreply.github.com> Date: Wed, 8 Feb 2023 10:20:48 +0100 Subject: [PATCH 004/570] remove parenthesis --- app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 5ccaa762..36b1c83d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -183,9 +183,9 @@ object APIHolder { .parsedSafe()?.results?.find { media -> val matchingYears = year == null || media.releaseDate == year val matchingTitles = - (media.title?.isMatchingTitles(mainTitle) == true || media.title?.isMatchingTitles( + media.title?.isMatchingTitles(mainTitle) == true || media.title?.isMatchingTitles( secondaryTitle - ) == true) + ) == true val matchingTypes = types?.any { it.name.equals(media.type, true) } == true matchingTitles && matchingTypes && matchingYears From eac7c2b2e3abbcc2b82428eedfd552dcfed9b827 Mon Sep 17 00:00:00 2001 From: reduplicated <110570621+reduplicated@users.noreply.github.com> Date: Thu, 9 Feb 2023 00:37:29 +0100 Subject: [PATCH 005/570] fixed --- .../com/lagradost/cloudstream3/MainAPI.kt | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 36b1c83d..4014e34d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -15,12 +15,9 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniList 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.ui.result.UiText 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.* @@ -163,35 +160,50 @@ 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 mainTitle Title to search by and match - * @param secondaryTitle Optional extra title if you have multiple titles and want extra guarantee to match. + * @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( - mainTitle: String, - secondaryTitle: String?, + titles: List, types: Set?, year: Int? - ): Tracker { - val res = app.get("https://api.consumet.org/meta/anilist/$mainTitle") - .parsedSafe()?.results?.find { media -> + ): 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?.isMatchingTitles(mainTitle) == true || media.title?.isMatchingTitles( - secondaryTitle - ) == true + 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 - return Tracker(res?.malId, res?.aniId, res?.image, res?.cover) + Tracker(res.malId, res.aniId, res.image, res.cover) + } catch (t: Throwable) { + logError(t) + null + } } From 3e2c2a5c86b93b9cd29ff1b8fd27a4a32a163a86 Mon Sep 17 00:00:00 2001 From: Sir Aguacata <87155550+KillerDogeEmpire@users.noreply.github.com> Date: Wed, 8 Feb 2023 15:58:15 -0800 Subject: [PATCH 006/570] Added a way for easy mal and anilist tracker (#359) * Added a way for easy mal and anilist tracker, All credit gos to Hexated for helping me * Made CodeFactor Fucking Happy * prettified the getTracker method * remove parenthesis * fixed --------- Co-authored-by: Blatzar <46196380+Blatzar@users.noreply.github.com> Co-authored-by: reduplicated <110570621+reduplicated@users.noreply.github.com> --- .../com/lagradost/cloudstream3/MainAPI.kt | 108 +++++++++++++++++- 1 file changed, 105 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 73859021..4014e34d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -15,12 +15,9 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniList 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.ui.result.UiText 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.* @@ -163,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) @@ -1590,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() + } + } + } +} From 4596afee06a4c8b95423c9f406bc4e912823c484 Mon Sep 17 00:00:00 2001 From: reduplicated <110570621+reduplicated@users.noreply.github.com> Date: Thu, 9 Feb 2023 01:32:48 +0100 Subject: [PATCH 007/570] auto track anilist/mal --- .../ui/result/ResultViewModel2.kt | 166 +++++++++++------- 1 file changed, 102 insertions(+), 64 deletions(-) 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 6817af6a..26237771 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 @@ -25,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 @@ -1422,85 +1423,122 @@ 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 = 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 - - argamap({ - addTrailer(meta.trailers) - }, { - if (this !is AnimeLoadResponse) return@argamap - val map = - Kitsu.getEpisodesDetails( - getMalId(), - getAniListId(), - isResponseRequired = false + argamap( + { + if (this !is AnimeLoadResponse) return@argamap + val res = APIHolder.getTracker( + listOfNotNull( + this.engName, + this.name, + this.japName + ).distinct(), 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 } From 84493b7f3bc2095a2a706c1f9341df36fd422519 Mon Sep 17 00:00:00 2001 From: reduplicated <110570621+reduplicated@users.noreply.github.com> Date: Thu, 9 Feb 2023 01:46:07 +0100 Subject: [PATCH 008/570] mini fix --- .../com/lagradost/cloudstream3/MainAPI.kt | 2 +- .../ui/result/ResultViewModel2.kt | 31 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 4014e34d..a277f622 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -82,7 +82,7 @@ object APIHolder { initMap() return apiMap?.get(apiName)?.let { apis.getOrNull(it) } // Leave the ?. null check, it can crash regardless - ?: allProviders.firstOrNull { it?.name == apiName } + ?: allProviders.firstOrNull { it.name == apiName } } } 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 26237771..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 @@ -1465,12 +1465,17 @@ class ResultViewModel2 : ViewModel() { argamap( { if (this !is AnimeLoadResponse) return@argamap + // already exist, no need to run getTracker + if (this.getAniListId() != null && this.getMalId() != null) return@argamap + val res = APIHolder.getTracker( listOfNotNull( this.engName, this.name, this.japName - ).distinct(), TrackerType.getTypes(this.type), this.year + ).filter { it.length > 2 }.distinct(), // the reason why we filter is due to not wanting smth like " " or "?" + TrackerType.getTypes(this.type), + this.year ) val ids = arrayOf( @@ -1499,7 +1504,7 @@ class ResultViewModel2 : ViewModel() { backgroundPosterUrl = backgroundPosterUrl ?: res?.cover }, { - if(meta == null) return@argamap + if (meta == null) return@argamap addTrailer(meta.trailers) }, { if (this !is AnimeLoadResponse) return@argamap @@ -2162,7 +2167,7 @@ class ResultViewModel2 : ViewModel() { autostart: AutoResume?, loadTrailers: Boolean = true, ) = - viewModelScope.launchSafe { + ioSafe { _page.postValue(Resource.Loading(url)) _episodes.postValue(ResourceSome.Loading()) @@ -2180,7 +2185,7 @@ class ResultViewModel2 : ViewModel() { "This provider does not exist" ) ) - return@launchSafe + return@ioSafe } @@ -2191,21 +2196,15 @@ class ResultViewModel2 : ViewModel() { 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 @@ -2215,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 @@ -2247,7 +2246,7 @@ class ResultViewModel2 : ViewModel() { updateFillers = showFillers, apiRepository = repo ) - if (!isActive) return@launchSafe + if (!isActive) return@ioSafe handleAutoStart(activity, autostart) } is Resource.Loading -> { From dd385561025dd2098944c21ae8b515e2e389b61b Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Thu, 9 Feb 2023 20:25:00 +0100 Subject: [PATCH 009/570] address issue #339 Co-authored-by: eightyy8 <64216434+eightyy8@users.noreply.github.com> --- .../ui/settings/SettingsGeneral.kt | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) 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 649aa634..45bd8bd6 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 @@ -57,47 +57,47 @@ fun getCurrentLocale(context: Context): String { val appLanguages = arrayListOf( /* begin language list */ Triple("", "العربية", "ar"), - Triple("", "български език", "bg"), + Triple("", "български", "bg"), Triple("", "বাংলা", "bn"), Triple("\uD83C\uDDE7\uD83C\uDDF7", "Brazilian Portuguese", "bp"), Triple("", "čeština", "cs"), Triple("", "Deutsch", "de"), - Triple("", "ελληνικά", "el"), + Triple("", "Ελληνικά", "el"), Triple("", "English", "en"), Triple("", "Esperanto", "eo"), - Triple("", "Español", "es"), + Triple("", "español", "es"), Triple("", "فارسی", "fa"), Triple("", "français", "fr"), + Triple("\uD83C\uDDEE\uD83C\uDDF1", "עברית", "he"), Triple("", "हिन्दी", "hi"), - Triple("", "hrvatski jezik", "hr"), + Triple("", "hrvatski", "hr"), Triple("", "magyar", "hu"), - Triple("\uD83C\uDDEE\uD83C\uDDE9", "Indonesian", "in"), - Triple("", "Italiano", "it"), - Triple("\uD83C\uDDEE\uD83C\uDDF1", "עִברִית", "iw"), + Triple("\uD83C\uDDEE\uD83C\uDDE9", "Bahasa Indonesia", "in"), + Triple("", "italiano", "it"), Triple("", "ಕನ್ನಡ", "kn"), - Triple("", "македонски јазик", "mk"), + Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), Triple("", "Nederlands", "nl"), - Triple("", "Norsk nynorsk", "nn"), - Triple("", "Norsk", "no"), - Triple("", "język polski", "pl"), - Triple("\uD83C\uDDF5\uD83C\uDDF9", "Português", "pt"), + 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("", "română", "ro"), + Triple("", "русский", "ru"), Triple("", "slovenčina", "sk"), Triple("", "Soomaaliga", "so"), Triple("", "svenska", "sv"), Triple("", "தமிழ்", "ta"), - Triple("", "Wikang Tagalog", "tl"), + Triple("", "Tagalog", "tl"), Triple("", "Türkçe", "tr"), - Triple("", "Українська", "uk"), + Triple("", "українська", "uk"), Triple("", "اردو", "ur"), Triple("", "Tiếng Việt", "vi"), - Triple("", "中文 (Zhōngwén)", "zh"), - Triple("\uD83C\uDDF9\uD83C\uDDFC", "Chinese Traditional", "zh-rTW"), + 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?) { From 0d2613d183f016e84bdec850d703294aa3343929 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 9 Feb 2023 20:20:04 +0100 Subject: [PATCH 010/570] Translated using Weblate (qt (generated) (qt)) Currently translated at 51.4% (303 of 589 strings) Translated using Weblate (Ukrainian) Currently translated at 99.3% (585 of 589 strings) Translated using Weblate (Persian) Currently translated at 20.3% (120 of 589 strings) Translated using Weblate (Russian) Currently translated at 98.9% (583 of 589 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (589 of 589 strings) Translated using Weblate (Czech) Currently translated at 100.0% (589 of 589 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (589 of 589 strings) Translated using Weblate (qt (generated) (qt)) Currently translated at 50.3% (293 of 582 strings) Translated using Weblate (Persian) Currently translated at 18.7% (109 of 582 strings) Translated using Weblate (Russian) Currently translated at 99.1% (577 of 582 strings) Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> Co-authored-by: Fjuro Co-authored-by: Hosted Weblate Co-authored-by: Rex_sa Co-authored-by: Skrripy Co-authored-by: Soroush Co-authored-by: eightyy8 Co-authored-by: gallegonovato Co-authored-by: sina Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fa/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/qt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translation: Cloudstream/App --- app/src/main/res/values-ar/strings.xml | 5 +++++ app/src/main/res/values-cs/strings.xml | 5 +++++ app/src/main/res/values-es/strings.xml | 3 ++- app/src/main/res/values-fa/strings.xml | 10 +++++++++ app/src/main/res/values-qt/strings.xml | 28 +++++++++++++++++++++++--- app/src/main/res/values-ru/strings.xml | 22 +++++++++++--------- app/src/main/res/values-uk/strings.xml | 1 + 7 files changed, 61 insertions(+), 13 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index f318478e..652df937 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -538,4 +538,9 @@ المكتبة تم العثور على ملف الوضع الآمن! \nلا يتم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف. + مدة التقديم عنما يكون المشغل مخفيا + مدة التقديم - المشغل مخفي + تلفزيون أندرويد + مدة التقديم عنما يكون المشغل مرئيا + مدة التقديم- المشغل المرئي \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 214cb86d..a78da8a4 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -530,4 +530,9 @@ Odstranit repozitář Nestaženo: %d Ne + Skrytý přehrávač - doba hledání + Množství vyhledávané doby při skrytém přehrávači + Zobrazený přehrávač - doba hledání + Android TV + Množství vyhledávané doby při zobrazeném přehrávači \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d253e256..8366b294 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -509,5 +509,6 @@ Jugadora mostrada - buscar cantidad Jugadora oculta - buscar cantidad Android TV - La cantidad de búsqueda utilizada cuando la jugadora es visible. + La cantidad de búsqueda utilizada cuando la jugadora es visible + La cantidad de búsqueda utilizada cuando el jugador está oculto \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index a34c5a05..81853674 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -23,4 +23,14 @@ هیچ‌کدام عنوان تاریخچه + پوستر + پوستر + پوستر قسمت + %dروز %dساعت %dدقیقه + %s قسمت %d + بازیگران: %s + قسمت %d پخش خواهد شد + %dساعت %dدقیقه + %dدقیقه + پوستر اصلی \ No newline at end of file diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index 24c51a05..b36f3b16 100644 --- a/app/src/main/res/values-qt/strings.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 @@ -196,4 +196,26 @@ a ou oh ouhuouhoaaha aaooohhouhhha hauauuu 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-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 29e98598..6e9fb394 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -25,8 +25,8 @@ Серия %d будет выпущен в Плакат Плакат - Постер Эпизода - Главный постер + Плакат эпизода + Главный плакат Следующий случайный Вернуться Изменить поставщика @@ -61,11 +61,11 @@ Нет Пересмотрю Смотреть фильм - Воспроизвести трейлер - Воспроизвести Livestream + Смотреть трейлер + Смотреть Livestream Источники Субтитры - Воспроизвести эпизод + Смотреть эпизод Повторная попытка подключение… Вернуться Скачано @@ -90,7 +90,7 @@ Скорость проигрыватель Воспроизвести Эпизод %dд %dч %dм - %d мин + %d мин. Dub Sub Установите смотреть состояние @@ -114,7 +114,7 @@ Шрифт Размер шрифта Удалить файл - Проиграть файл + Воспроизвести файл Внутренняя память Продолжить Скачать Остановить скачивание @@ -213,7 +213,7 @@ NSFW NSFW Фильм - Серия + Сериал Торрент Документальный Азиатская драма @@ -472,7 +472,7 @@ Скачайте список сайтов, который вы хотите использовать Отображать Аниме с Дубляжом/Субтитрами Включить NSFW на поддерживаемых провайдерах - Убрать скрытые субтитры из субтитров + Удалять скрытые субтитры из субтитров Дополнительно Изменить вид интерфейса, чтобы соответствовать устройству Аудио дорожки @@ -502,4 +502,8 @@ Предпочтительные медиа Опущенные Объем перемотки плеера + Объем перемотка, используемый, когда плеер виден + Плеер показан - Перемотки объем + Плеер спрятан - Перемотки объем + Удалять лишнее из субтитров \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 82baa6b4..871e0a28 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -506,4 +506,5 @@ Схоже, цей список порожній, спробуйте перейти до іншого Файл безпечного режиму знайдено! \nРозширеня не завантажуються під час запуску, доки файл не буде видалено. + Android TV \ No newline at end of file From 5c20b479e56c5d574d0e7e3bdc542e787b93a172 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Thu, 9 Feb 2023 19:25:34 +0000 Subject: [PATCH 011/570] update list of locales --- .../com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 1 + 1 file changed, 1 insertion(+) 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 45bd8bd6..ae37675f 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 @@ -74,6 +74,7 @@ val appLanguages = arrayListOf( Triple("", "magyar", "hu"), Triple("\uD83C\uDDEE\uD83C\uDDE9", "Bahasa Indonesia", "in"), Triple("", "italiano", "it"), + Triple("", "iw", "iw"), Triple("", "ಕನ್ನಡ", "kn"), Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), From 7b11b9b585a030de02a52c964e43433485f8b870 Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Thu, 9 Feb 2023 20:27:37 +0100 Subject: [PATCH 012/570] fix wrong hebrew lang code --- .../com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 ae37675f..2c6b6d47 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 @@ -68,13 +68,12 @@ val appLanguages = arrayListOf( Triple("", "español", "es"), Triple("", "فارسی", "fa"), Triple("", "français", "fr"), - Triple("\uD83C\uDDEE\uD83C\uDDF1", "עברית", "he"), + 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("", "iw", "iw"), Triple("", "ಕನ್ನಡ", "kn"), Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), From 1117271a715ba181615ddad7e4f941fb2d674a1b Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Fri, 10 Feb 2023 15:16:58 +0100 Subject: [PATCH 013/570] =?UTF-8?q?[skip=20ci]=20portugu=C3=AAs=20brasilei?= =?UTF-8?q?ro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2c6b6d47..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 @@ -59,7 +59,7 @@ val appLanguages = arrayListOf( Triple("", "العربية", "ar"), Triple("", "български", "bg"), Triple("", "বাংলা", "bn"), - Triple("\uD83C\uDDE7\uD83C\uDDF7", "Brazilian Portuguese", "bp"), + Triple("\uD83C\uDDE7\uD83C\uDDF7", "português brasileiro", "bp"), Triple("", "čeština", "cs"), Triple("", "Deutsch", "de"), Triple("", "Ελληνικά", "el"), From df6c395acb3a939636d2b818edc1ff8b9d109991 Mon Sep 17 00:00:00 2001 From: Stormunblessed <86633626+Stormunblessed@users.noreply.github.com> Date: Tue, 14 Feb 2023 09:11:20 -0600 Subject: [PATCH 014/570] Sendvid extractor (#365) * fix fastream, tomatomatela, and added okrulink * forgot this * sendvid extractor * sendvid extractor * fixes --- .../cloudstream3/extractors/Sendvid.kt | 28 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 1 + 2 files changed, 29 insertions(+) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Sendvid.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Sendvid.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Sendvid.kt new file mode 100644 index 00000000..514b802d --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Sendvid.kt @@ -0,0 +1,28 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 + +open class Sendvid : ExtractorApi() { + override var name = "Sendvid" + override val mainUrl = "https://sendvid.com" + override val requiresReferer = false + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val doc = app.get(url).document + val urlString = doc.select("head meta[property=og:video:secure_url]").attr("content") + if (urlString.contains("m3u8")) { + generateM3u8( + name, + urlString, + mainUrl, + ).forEach(callback) + } + } +} \ No newline at end of file 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 1ad3639b..b0dba9ff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -265,6 +265,7 @@ val extractorApis: MutableList = arrayListOf( OkRu(), OkRuHttps(), Okrulink(), + Sendvid(), // dood extractors DoodCxExtractor(), From 4a8ee550185f817fffd68372b2144886bd29066a Mon Sep 17 00:00:00 2001 From: Lag <> Date: Wed, 15 Feb 2023 21:40:10 +0100 Subject: [PATCH 015/570] Added provider tests --- .../cloudstream3/ExampleInstrumentedTest.kt | 198 +------------ .../com/lagradost/cloudstream3/MainAPI.kt | 15 + .../lagradost/cloudstream3/MainActivity.kt | 1 + .../cloudstream3/mvvm/ArchComponentExt.kt | 22 +- .../ui/settings/SettingsProviders.kt | 17 ++ .../ui/settings/testing/TestFragment.kt | 97 +++++++ .../ui/settings/testing/TestResultAdapter.kt | 80 ++++++ .../ui/settings/testing/TestView.kt | 119 ++++++++ .../ui/settings/testing/TestViewModel.kt | 108 +++++++ .../lagradost/cloudstream3/utils/AppUtils.kt | 17 ++ .../cloudstream3/utils/TestingUtils.kt | 267 ++++++++++++++++++ .../res/drawable/baseline_network_ping_24.xml | 5 + .../res/drawable/baseline_text_snippet_24.xml | 5 + app/src/main/res/layout/fragment_testing.xml | 53 ++++ .../main/res/layout/provider_test_item.xml | 71 +++++ .../main/res/navigation/mobile_navigation.xml | 17 ++ app/src/main/res/values/attrs.xml | 6 +- app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/strings.xml | 10 + app/src/main/res/xml/settings_providers.xml | 36 ++- 20 files changed, 936 insertions(+), 211 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt create mode 100644 app/src/main/res/drawable/baseline_network_ping_24.xml create mode 100644 app/src/main/res/drawable/baseline_text_snippet_24.xml create mode 100644 app/src/main/res/layout/fragment_testing.xml create mode 100644 app/src/main/res/layout/provider_test_item.xml diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index 81753f6b..92042d60 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -1,9 +1,8 @@ package com.lagradost.cloudstream3 import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.SubtitleHelper +import com.lagradost.cloudstream3.utils.TestingUtils import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.Test @@ -16,142 +15,11 @@ import org.junit.runner.RunWith */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { - //@Test - //fun useAppContext() { - // // Context of the app under test. - // val appContext = InstrumentationRegistry.getInstrumentation().targetContext - // assertEquals("com.lagradost.cloudstream3", appContext.packageName) - //} - private fun getAllProviders(): List { + println("Providers: ${APIHolder.allProviders.size}") return APIHolder.allProviders //.filter { !it.usesWebView } } - private suspend fun loadLinks(api: MainAPI, url: String?): Boolean { - Assert.assertNotNull("Api ${api.name} has invalid url on episode", url) - if (url == null) return true - var linksLoaded = 0 - try { - val success = api.loadLinks(url, false, {}) { link -> - Assert.assertTrue( - "Api ${api.name} returns link with invalid Quality", - Qualities.values().map { it.value }.contains(link.quality) - ) - Assert.assertTrue( - "Api ${api.name} returns link with invalid url ${link.url}", - link.url.length > 4 - ) - linksLoaded++ - } - if (success) { - return linksLoaded > 0 - } - Assert.assertTrue("Api ${api.name} has returns false on .loadLinks", success) - } catch (e: Exception) { - if (e.cause is NotImplementedError) { - Assert.fail("Provider has not implemented .loadLinks") - } - logError(e) - } - return true - } - - private suspend fun testSingleProviderApi(api: MainAPI): Boolean { - val searchQueries = listOf("over", "iron", "guy") - var correctResponses = 0 - var searchResult: List? = null - for (query in searchQueries) { - val response = try { - api.search(query) - } catch (e: Exception) { - if (e.cause is NotImplementedError) { - Assert.fail("Provider has not implemented .search") - } - logError(e) - null - } - if (!response.isNullOrEmpty()) { - correctResponses++ - if (searchResult == null) { - searchResult = response - } - } - } - - if (correctResponses == 0 || searchResult == null) { - System.err.println("Api ${api.name} did not return any valid search responses") - return false - } - - try { - var validResults = false - for (result in searchResult) { - Assert.assertEquals( - "Invalid apiName on response on ${api.name}", - result.apiName, - api.name - ) - val load = api.load(result.url) ?: continue - Assert.assertEquals( - "Invalid apiName on load on ${api.name}", - load.apiName, - result.apiName - ) - Assert.assertTrue( - "Api ${api.name} on load does not contain any of the supportedTypes", - api.supportedTypes.contains(load.type) - ) - when (load) { - is AnimeLoadResponse -> { - val gotNoEpisodes = - load.episodes.keys.isEmpty() || load.episodes.keys.any { load.episodes[it].isNullOrEmpty() } - - if (gotNoEpisodes) { - println("Api ${api.name} got no episodes on ${load.url}") - continue - } - - val url = (load.episodes[load.episodes.keys.first()])?.first()?.data - validResults = loadLinks(api, url) - if (!validResults) continue - } - is MovieLoadResponse -> { - val gotNoEpisodes = load.dataUrl.isBlank() - if (gotNoEpisodes) { - println("Api ${api.name} got no movie on ${load.url}") - continue - } - - validResults = loadLinks(api, load.dataUrl) - if (!validResults) continue - } - is TvSeriesLoadResponse -> { - val gotNoEpisodes = load.episodes.isEmpty() - if (gotNoEpisodes) { - println("Api ${api.name} got no episodes on ${load.url}") - continue - } - - validResults = loadLinks(api, load.episodes.first().data) - if (!validResults) continue - } - } - break - } - if (!validResults) { - System.err.println("Api ${api.name} did not load on any") - } - - return validResults - } catch (e: Exception) { - if (e.cause is NotImplementedError) { - Assert.fail("Provider has not implemented .load") - } - logError(e) - return false - } - } - @Test fun providersExist() { Assert.assertTrue(getAllProviders().isNotEmpty()) @@ -159,6 +27,7 @@ class ExampleInstrumentedTest { } @Test + @Throws(AssertionError::class) fun providerCorrectData() { val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 } Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty()) @@ -181,67 +50,20 @@ class ExampleInstrumentedTest { fun providerCorrectHomepage() { runBlocking { getAllProviders().amap { api -> - if (api.hasMainPage) { - try { - val f = api.mainPage.first() - val homepage = - api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages)) - when { - homepage == null -> { - System.err.println("Homepage provider ${api.name} did not correctly load homepage!") - } - homepage.items.isEmpty() -> { - System.err.println("Homepage provider ${api.name} does not contain any items!") - } - homepage.items.any { it.list.isEmpty() } -> { - System.err.println("Homepage provider ${api.name} does not have any items on result!") - } - } - } catch (e: Exception) { - if (e.cause is NotImplementedError) { - Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") - } - logError(e) - } - } + TestingUtils.testHomepage(api, ::println) } } println("Done providerCorrectHomepage") } -// @Test -// fun testSingleProvider() { -// testSingleProviderApi(ThenosProvider()) -// } - @Test - fun providerCorrect() { + fun testAllProvidersCorrect() { runBlocking { - val invalidProvider = ArrayList>() - val providers = getAllProviders() - providers.amap { api -> - try { - println("Trying $api") - if (testSingleProviderApi(api)) { - println("Success $api") - } else { - System.err.println("Error $api") - invalidProvider.add(Pair(api, null)) - } - } catch (e: Exception) { - logError(e) - invalidProvider.add(Pair(api, e)) - } - } - if (invalidProvider.isEmpty()) { - println("No Invalid providers! :D") - } else { - println("Invalid providers are: ") - for (provider in invalidProvider) { - println("${provider.first}") - } - } + TestingUtils.getDeferredProviderTests( + this, + getAllProviders(), + ::println + ) { _, _ -> } } - println("Done providerCorrect") } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index a277f622..3958984e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -17,8 +17,10 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import okhttp3.Interceptor +import org.mozilla.javascript.Scriptable import java.text.SimpleDateFormat import java.util.* import kotlin.math.absoluteValue @@ -734,6 +736,19 @@ fun fixTitle(str: String): String { .replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it } } } +/** + * Get rhino context in a safe way as it needs to be initialized on the main thread. + * Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects() + * Use like the following: rhino.evaluateString(scope, js, "JavaScript", 1, null) + **/ +suspend fun getRhinoContext(): org.mozilla.javascript.Context { + return Coroutines.mainWork { + val rhino = org.mozilla.javascript.Context.enter() + rhino.initSafeStandardObjects() + rhino.optimizationLevel = -1 + rhino + } +} /** https://www.imdb.com/title/tt2861424/ -> tt2861424 */ fun imdbUrlToId(url: String): String? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index eddec15e..28419e7a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -402,6 +402,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { R.id.navigation_settings_general, R.id.navigation_settings_extensions, R.id.navigation_settings_plugins, + R.id.navigation_test_providers, ).contains(destination.id) diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index afe956cc..bb15bc85 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -49,7 +49,7 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) { } } -fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { +fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { liveData.observe(this) { it?.let { t -> action(t) } } } @@ -121,13 +121,21 @@ suspend fun suspendSafeApiCall(apiCall: suspend () -> T): T? { } } +fun Throwable.getAllMessages(): String { + return (this.localizedMessage ?: "") + (this.cause?.getAllMessages()?.let { "\n$it" } ?: "") +} + +fun Throwable.getStackTracePretty(showMessage: Boolean = true): String { + val prefix = if (showMessage) this.localizedMessage?.let { "\n$it" } ?: "" else "" + return prefix + this.stackTrace.joinToString( + separator = "\n" + ) { + "${it.fileName} ${it.lineNumber}" + } +} + fun safeFail(throwable: Throwable): Resource { - val stackTraceMsg = - (throwable.localizedMessage ?: "") + "\n\n" + throwable.stackTrace.joinToString( - separator = "\n" - ) { - "${it.fileName} ${it.lineNumber}" - } + val stackTraceMsg = throwable.getStackTracePretty() return Resource.Failure(false, null, null, stackTraceMsg) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 3b01508d..42a864a6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -2,6 +2,8 @@ package com.lagradost.cloudstream3.ui.settings import android.os.Bundle import android.view.View +import androidx.navigation.NavOptions +import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.* @@ -16,6 +18,7 @@ import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard +import com.lagradost.cloudstream3.utils.UIHelper.navigate class SettingsProviders : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -56,6 +59,20 @@ class SettingsProviders : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + getPref(R.string.test_providers_key)?.setOnPreferenceClickListener { + // Somehow animations do not work without this. + val options = NavOptions.Builder() + .setEnterAnim(R.anim.enter_anim) + .setExitAnim(R.anim.exit_anim) + .setPopEnterAnim(R.anim.pop_enter) + .setPopExitAnim(R.anim.pop_exit) + .build() + + this@SettingsProviders.findNavController() + .navigate(R.id.navigation_test_providers, null, options) + true + } + getPref(R.string.prefer_media_type_key)?.setOnPreferenceClickListener { val names = enumValues().sorted().map { it.name } val default = diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt new file mode 100644 index 00000000..34cd67cd --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt @@ -0,0 +1,97 @@ +package com.lagradost.cloudstream3.ui.settings.testing + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.fragment.app.activityViewModels +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.mvvm.observeNullable +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar +import kotlinx.android.synthetic.main.fragment_testing.* +import kotlinx.android.synthetic.main.view_test.* + + +class TestFragment : Fragment() { + + private val testViewModel: TestViewModel by activityViewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + setUpToolbar(R.string.category_provider_test) + super.onViewCreated(view, savedInstanceState) + + provider_test_recycler_view?.adapter = TestResultAdapter( + mutableListOf() + ) + + testViewModel.init() + if (testViewModel.isRunningTest) { + provider_test?.setState(TestView.TestState.Running) + } + + observe(testViewModel.providerProgress) { (passed, failed, total) -> + provider_test?.setProgress(passed, failed, total) + } + + observeNullable(testViewModel.providerResults) { + normalSafeApiCall { + val newItems = it.sortedBy { api -> api.first.name } + (provider_test_recycler_view?.adapter as? TestResultAdapter)?.updateList( + newItems + ) + } + } + + provider_test?.setOnPlayButtonListener { state -> + when (state) { + TestView.TestState.Stopped -> testViewModel.stopTest() + TestView.TestState.Running -> testViewModel.startTest() + TestView.TestState.None -> testViewModel.startTest() + } + } + + if (isTrueTvSettings()) { + tests_play_pause?.isFocusableInTouchMode = true + tests_play_pause?.requestFocus() + } + + provider_test?.playPauseButton?.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + provider_test_appbar?.setExpanded(true, true) + } + } + + fun focusRecyclerView() { + // Hack to make it possible to focus the recyclerview. + if (isTrueTvSettings()) { + provider_test_recycler_view?.requestFocus() + provider_test_appbar?.setExpanded(false, true) + } + } + + provider_test?.setOnMainClick { + testViewModel.setFilterMethod(TestViewModel.ProviderFilter.All) + focusRecyclerView() + } + provider_test?.setOnFailedClick { + testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Failed) + focusRecyclerView() + } + provider_test?.setOnPassedClick { + testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Passed) + focusRecyclerView() + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_testing, container, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt new file mode 100644 index 00000000..d04e2379 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt @@ -0,0 +1,80 @@ +package com.lagradost.cloudstream3.ui.settings.testing + +import android.app.AlertDialog +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.getAllMessages +import com.lagradost.cloudstream3.mvvm.getStackTracePretty +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso +import com.lagradost.cloudstream3.utils.TestingUtils +import kotlinx.android.synthetic.main.provider_test_item.view.* + +class TestResultAdapter(override val items: MutableList>) : + AppUtils.DiffAdapter>(items) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return ProviderTestViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.provider_test_item, parent, false), + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is ProviderTestViewHolder -> { + val item = items[position] + holder.bind(item.first, item.second) + } + } + } + + inner class ProviderTestViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val languageText: TextView = itemView.lang_icon + private val providerTitle: TextView = itemView.main_text + private val statusText: TextView = itemView.passed_failed_marker + private val failDescription: TextView = itemView.fail_description + private val logButton: ImageView = itemView.action_button + + private fun String.lastLine(): String? { + return this.lines().lastOrNull { it.isNotBlank() } + } + + fun bind(api: MainAPI, result: TestingUtils.TestResultProvider) { + languageText.text = getFlagFromIso(api.lang) + providerTitle.text = api.name + + val (resultText, resultColor) = if (result.success) { + R.string.test_passed to R.color.colorTestPass + } else { + R.string.test_failed to R.color.colorTestFail + } + + statusText.setText(resultText) + statusText.setTextColor(ContextCompat.getColor(itemView.context, resultColor)) + + val stackTrace = result.exception?.getStackTracePretty(false)?.ifBlank { null } + val messages = result.exception?.getAllMessages()?.ifBlank { null } + val fullLog = + result.log + (messages?.let { "\n\n$it" } ?: "") + (stackTrace?.let { "\n\n$it" } ?: "") + + failDescription.text = messages?.lastLine() ?: result.log.lastLine() + + logButton.setOnClickListener { + val builder: AlertDialog.Builder = + AlertDialog.Builder(it.context, R.style.AlertDialogCustom) + builder.setMessage(fullLog) + .setTitle(R.string.test_log) + .show() + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt new file mode 100644 index 00000000..26513f4a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt @@ -0,0 +1,119 @@ +package com.lagradost.cloudstream3.ui.settings.testing + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.core.widget.ContentLoadingProgressBar +import com.google.android.material.button.MaterialButton +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.utils.AppUtils.animateProgressTo + +class TestView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : CardView(context, attrs) { + enum class TestState(@StringRes val stringRes: Int, @DrawableRes val icon: Int) { + None(R.string.start, R.drawable.ic_baseline_play_arrow_24), + + // Paused(R.string.resume, R.drawable.ic_baseline_play_arrow_24), + Stopped(R.string.restart, R.drawable.ic_baseline_play_arrow_24), + Running(R.string.stop, R.drawable.pause_to_play), + } + + var mainSection: View? = null + var testsPassedSection: View? = null + var testsFailedSection: View? = null + + var mainSectionText: TextView? = null + var mainSectionHeader: TextView? = null + var testsPassedSectionText: TextView? = null + var testsFailedSectionText: TextView? = null + var totalProgressBar: ContentLoadingProgressBar? = null + + var playPauseButton: MaterialButton? = null + var stateListener: (TestState) -> Unit = {} + + private var state = TestState.None + + init { + LayoutInflater.from(context).inflate(R.layout.view_test, this, true) + + mainSection = findViewById(R.id.main_test_section) + testsPassedSection = findViewById(R.id.passed_test_section) + testsFailedSection = findViewById(R.id.failed_test_section) + + mainSectionHeader = findViewById(R.id.main_test_header) + mainSectionText = findViewById(R.id.main_test_section_progress) + testsPassedSectionText = findViewById(R.id.passed_test_section_progress) + testsFailedSectionText = findViewById(R.id.failed_test_section_progress) + + totalProgressBar = findViewById(R.id.test_total_progress) + playPauseButton = findViewById(R.id.tests_play_pause) + + attrs?.let { + val typedArray = context.obtainStyledAttributes(it, R.styleable.TestView) + val headerText = typedArray.getString(R.styleable.TestView_header_text) + mainSectionHeader?.text = headerText + typedArray.recycle() + } + + playPauseButton?.setOnClickListener { + val newState = when (state) { + TestState.None -> TestState.Running + TestState.Running -> TestState.Stopped + TestState.Stopped -> TestState.Running + } + setState(newState) + } + } + + fun setOnPlayButtonListener(listener: (TestState) -> Unit) { + stateListener = listener + } + + fun setState(newState: TestState) { + state = newState + stateListener.invoke(newState) + playPauseButton?.setText(newState.stringRes) + playPauseButton?.icon = ContextCompat.getDrawable(context, newState.icon) + } + + fun setProgress(passed: Int, failed: Int, total: Int?) { + val totalProgress = passed + failed + mainSectionText?.text = "$totalProgress / ${total?.toString() ?: "?"}" + testsPassedSectionText?.text = passed.toString() + testsFailedSectionText?.text = failed.toString() + + totalProgressBar?.max = (total ?: 0) * 1000 + totalProgressBar?.animateProgressTo(totalProgress * 1000) + + totalProgressBar?.isVisible = !(totalProgress == 0 || (total ?: 0) == 0) + if (totalProgress == total) { + setState(TestState.Stopped) + } + } + + fun setMainHeader(@StringRes header: Int) { + mainSectionHeader?.setText(header) + } + + fun setOnMainClick(listener: OnClickListener) { + mainSection?.setOnClickListener(listener) + } + + fun setOnPassedClick(listener: OnClickListener) { + testsPassedSection?.setOnClickListener(listener) + } + + fun setOnFailedClick(listener: OnClickListener) { + testsFailedSection?.setOnClickListener(listener) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt new file mode 100644 index 00000000..2e05baff --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt @@ -0,0 +1,108 @@ +package com.lagradost.cloudstream3.ui.settings.testing + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.lagradost.cloudstream3.APIHolder +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf +import com.lagradost.cloudstream3.utils.TestingUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel + +class TestViewModel : ViewModel() { + data class TestProgress( + val passed: Int, + val failed: Int, + val total: Int + ) + + enum class ProviderFilter { + All, + Passed, + Failed + } + + private val _providerProgress = MutableLiveData(null) + val providerProgress: LiveData = _providerProgress + + private val _providerResults = + MutableLiveData>>( + emptyList() + ) + + val providerResults: LiveData>> = + _providerResults + + private var scope: CoroutineScope? = null + val isRunningTest + get() = scope != null + + private var filter = ProviderFilter.All + private val providers = threadSafeListOf>() + private var passed = 0 + private var failed = 0 + private var total = 0 + + private fun updateProgress() { + _providerProgress.postValue(TestProgress(passed, failed, total)) + postProviders() + } + + private fun postProviders() { + synchronized(providers) { + val filtered = when (filter) { + ProviderFilter.All -> providers + ProviderFilter.Passed -> providers.filter { it.second.success } + ProviderFilter.Failed -> providers.filter { !it.second.success } + } + _providerResults.postValue(filtered) + } + } + + fun setFilterMethod(filter: ProviderFilter) { + if (this.filter == filter) return + this.filter = filter + postProviders() + } + + private fun addProvider(api: MainAPI, results: TestingUtils.TestResultProvider) { + synchronized(providers) { + val index = providers.indexOfFirst { it.first == api } + if (index == -1) { + providers.add(api to results) + if (results.success) passed++ else failed++ + } else { + providers[index] = api to results + } + updateProgress() + } + } + + fun init() { + val apis = APIHolder.allProviders + total = apis.size + updateProgress() + } + + fun startTest() { + scope = CoroutineScope(Dispatchers.Default) + + val apis = APIHolder.allProviders + total = apis.size + failed = 0 + passed = 0 + providers.clear() + updateProgress() + + TestingUtils.getDeferredProviderTests(scope ?: return, apis, ::println) { api, result -> + addProvider(api, result) + } + } + + fun stopTest() { + scope?.cancel() + scope = null + } +} \ No newline at end of file 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 00dee9b2..4b1053b1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.utils +import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.app.Activity import android.app.Activity.RESULT_CANCELED @@ -17,6 +18,7 @@ import android.os.* import android.provider.MediaStore import android.text.Spanned import android.util.Log +import android.view.animation.DecelerateInterpolator import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi @@ -25,6 +27,7 @@ import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.text.HtmlCompat import androidx.core.text.toSpanned +import androidx.core.widget.ContentLoadingProgressBar import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.findNavController @@ -179,6 +182,20 @@ object AppUtils { touchSlopField.set(recyclerView, touchSlop * f) // "8" was obtained experimentally } + fun ContentLoadingProgressBar?.animateProgressTo(to: Int) { + if (this == null) return + val animation: ObjectAnimator = ObjectAnimator.ofInt( + this, + "progress", + this.progress, + to + ) + animation.duration = 500 + animation.setAutoCancel(true) + animation.interpolator = DecelerateInterpolator() + animation.start() + } + @SuppressLint("RestrictedApi") fun getAllWatchNextPrograms(context: Context): Set { val COLUMN_WATCH_NEXT_ID_INDEX = 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt new file mode 100644 index 00000000..66e1e504 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -0,0 +1,267 @@ +package com.lagradost.cloudstream3.utils + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.mvvm.logError +import kotlinx.coroutines.* +import org.junit.Assert + +object TestingUtils { + open class TestResult(val success: Boolean) { + companion object { + val Pass = TestResult(true) + val Fail = TestResult(false) + } + } + + class TestResultSearch(val results: List) : TestResult(true) + class TestResultLoad(val extractorData: String) : TestResult(true) + + class TestResultProvider(success: Boolean, val log: String, val exception: Throwable?) : + TestResult(success) + + @Throws(AssertionError::class, CancellationException::class) + suspend fun testHomepage( + api: MainAPI, + logger: (String) -> Unit + ): TestResult { + if (api.hasMainPage) { + try { + val f = api.mainPage.first() + val homepage = + api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages)) + when { + homepage == null -> { + logger.invoke("Homepage provider ${api.name} did not correctly load homepage!") + } + homepage.items.isEmpty() -> { + logger.invoke("Homepage provider ${api.name} does not contain any items!") + } + homepage.items.any { it.list.isEmpty() } -> { + logger.invoke("Homepage provider ${api.name} does not have any items on result!") + } + } + } catch (e: Throwable) { + if (e is NotImplementedError) { + Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") + } else if (e is CancellationException) { + throw e + } + logError(e) + } + } + return TestResult.Pass + } + + @Throws(AssertionError::class, CancellationException::class) + private suspend fun testSearch( + api: MainAPI + ): TestResult { + val searchQueries = listOf("over", "iron", "guy") + val searchResults = searchQueries.firstNotNullOfOrNull { query -> + try { + api.search(query).takeIf { !it.isNullOrEmpty() } + } catch (e: Throwable) { + if (e is NotImplementedError) { + Assert.fail("Provider has not implemented search()") + } else if (e is CancellationException) { + throw e + } + logError(e) + null + } + } + + return if (searchResults.isNullOrEmpty()) { + Assert.fail("Api ${api.name} did not return any valid search responses") + TestResult.Fail // Should not be reached + } else { + TestResultSearch(searchResults) + } + + } + + + @Throws(AssertionError::class, CancellationException::class) + private suspend fun testLoad( + api: MainAPI, + result: SearchResponse, + logger: (String) -> Unit + ): TestResult { + try { + Assert.assertEquals( + "Invalid apiName on SearchResponse on ${api.name}", + result.apiName, + api.name + ) + + val loadResponse = api.load(result.url) + + if (loadResponse == null) { + logger.invoke("Returned null loadResponse on ${result.url} on ${api.name}") + return TestResult.Fail + } + + Assert.assertEquals( + "Invalid apiName on LoadResponse on ${api.name}", + loadResponse.apiName, + result.apiName + ) + Assert.assertTrue( + "Api ${api.name} on load does not contain any of the supportedTypes: ${loadResponse.type}", + api.supportedTypes.contains(loadResponse.type) + ) + + val url = when (loadResponse) { + is AnimeLoadResponse -> { + val gotNoEpisodes = + loadResponse.episodes.keys.isEmpty() || loadResponse.episodes.keys.any { loadResponse.episodes[it].isNullOrEmpty() } + + if (gotNoEpisodes) { + logger.invoke("Api ${api.name} got no episodes on ${loadResponse.url}") + return TestResult.Fail + } + + (loadResponse.episodes[loadResponse.episodes.keys.firstOrNull()])?.firstOrNull()?.data + } + is MovieLoadResponse -> { + val gotNoEpisodes = loadResponse.dataUrl.isBlank() + if (gotNoEpisodes) { + logger.invoke("Api ${api.name} got no movie on ${loadResponse.url}") + return TestResult.Fail + } + + loadResponse.dataUrl + } + is TvSeriesLoadResponse -> { + val gotNoEpisodes = loadResponse.episodes.isEmpty() + if (gotNoEpisodes) { + logger.invoke("Api ${api.name} got no episodes on ${loadResponse.url}") + return TestResult.Fail + } + loadResponse.episodes.firstOrNull()?.data + } + is LiveStreamLoadResponse -> { + loadResponse.dataUrl + } + else -> { + logger.invoke("Unknown load response: ${loadResponse.javaClass.name}") + return TestResult.Fail + } + } ?: return TestResult.Fail + + return TestResultLoad(url) + +// val loadTest = testLoadResponse(api, load, logger) +// if (loadTest is TestResultLoad) { +// testLinkLoading(api, loadTest.extractorData, logger).success +// } else { +// false +// } +// if (!validResults) { +// logger("Api ${api.name} did not load on the first search results: ${smallSearchResults.map { it.name }}") +// } + +// return TestResult(validResults) + } catch (e: Throwable) { + if (e is NotImplementedError) { + Assert.fail("Provider has not implemented load()") + } + throw e + } + } + + @Throws(AssertionError::class, CancellationException::class) + private suspend fun testLinkLoading( + api: MainAPI, + url: String?, + logger: (String) -> Unit + ): TestResult { + Assert.assertNotNull("Api ${api.name} has invalid url on episode", url) + if (url == null) return TestResult.Fail // Should never trigger + + var linksLoaded = 0 + try { + val success = api.loadLinks(url, false, {}) { link -> + logger.invoke("Video loaded: ${link.name}") + Assert.assertTrue( + "Api ${api.name} returns link with invalid url ${link.url}", + link.url.length > 4 + ) + linksLoaded++ + } + if (success) { + logger.invoke("Links loaded: $linksLoaded") + return TestResult(linksLoaded > 0) + } else { + Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") + } + } catch (e: Throwable) { + when (e) { + is NotImplementedError -> { + Assert.fail("Provider has not implemented loadLinks()") + } + else -> { + logger.invoke("Failed link loading on ${api.name} using data: $url") + throw e + } + } + } + return TestResult.Pass + } + + fun getDeferredProviderTests( + scope: CoroutineScope, + providers: List, + logger: (String) -> Unit, + callback: (MainAPI, TestResultProvider) -> Unit + ) { + providers.forEach { api -> + scope.launch { + var log = "" + fun addToLog(string: String) { + log += string + "\n" + logger.invoke(string) + } + fun getLog(): String { + return log.removeSuffix("\n") + } + + val result = try { + addToLog("Trying ${api.name}") + + // Test Homepage + val homepage = testHomepage(api, logger).success + Assert.assertTrue("Homepage failed to load", homepage) + + // Test Search Results + val searchResults = testSearch(api) + Assert.assertTrue("Failed to get search results", searchResults.success) + searchResults as TestResultSearch + + // Test Load and LoadLinks + // Only try the first 3 search results to prevent spamming + val success = searchResults.results.take(3).any { searchResponse -> + addToLog("Testing search result: ${searchResponse.url}") + val loadResponse = testLoad(api, searchResponse, ::addToLog) + if (loadResponse !is TestResultLoad) { + false + } else { + testLinkLoading(api, loadResponse.extractorData, ::addToLog).success + } + } + + if (success) { + logger.invoke("Success ${api.name}") + TestResultProvider(true, getLog(), null) + } else { + logger.invoke("Error ${api.name}") + TestResultProvider(false, getLog(), null) + } + } catch (e: Throwable) { + TestResultProvider(false, getLog(), e) + } + callback.invoke(api, result) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_network_ping_24.xml b/app/src/main/res/drawable/baseline_network_ping_24.xml new file mode 100644 index 00000000..1caae667 --- /dev/null +++ b/app/src/main/res/drawable/baseline_network_ping_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_text_snippet_24.xml b/app/src/main/res/drawable/baseline_text_snippet_24.xml new file mode 100644 index 00000000..c1f3654b --- /dev/null +++ b/app/src/main/res/drawable/baseline_text_snippet_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/fragment_testing.xml b/app/src/main/res/layout/fragment_testing.xml new file mode 100644 index 00000000..1426f59e --- /dev/null +++ b/app/src/main/res/layout/fragment_testing.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/provider_test_item.xml b/app/src/main/res/layout/provider_test_item.xml new file mode 100644 index 00000000..065b1bd8 --- /dev/null +++ b/app/src/main/res/layout/provider_test_item.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + \ 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 d71eeb06..e59f670e 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -519,6 +519,23 @@ app:popExitAnim="@anim/exit_anim" tools:layout="@layout/fragment_player" /> + + + - + @@ -13,6 +13,10 @@ ?attr/colorPrimary + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 61ff0c2b..7dd4c989 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -82,4 +82,7 @@ #515151 #FFFFFF #622C00 + + #48E484 + #ea596e \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 778f34c9..cb9d5508 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,6 +13,7 @@ fast_forward_button_time benene_count subtitle_settings_key + test_providers_key subtitle_settings_chromecast_key quality_pref_key player_pref_key @@ -195,6 +196,7 @@ No Plot Found No Description Found Show Logcat 🐈 + Log Picture-in-picture Continues playback in a miniature player on top of other apps Player resize button @@ -282,6 +284,9 @@ Delete @string/sort_cancel Pause + Start + Failed + Passed Resume -30 +30 @@ -423,6 +428,7 @@ Enable NSFW on supported providers Subtitle encoding Providers + Provider test Layout Auto TV layout @@ -579,6 +585,8 @@ Audio tracks Video tracks Apply on Restart + Restart + Stop Safe mode on All extensions were turned off due to a crash to help you find the one causing trouble. View crash info @@ -636,4 +644,6 @@ 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. + + Hello blank fragment \ No newline at end of file diff --git a/app/src/main/res/xml/settings_providers.xml b/app/src/main/res/xml/settings_providers.xml index a177865b..1ee58faf 100644 --- a/app/src/main/res/xml/settings_providers.xml +++ b/app/src/main/res/xml/settings_providers.xml @@ -1,24 +1,30 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> + android:icon="@drawable/ic_baseline_language_24" + android:key="@string/provider_lang_key" + android:title="@string/provider_lang_settings" /> + android:icon="@drawable/ic_baseline_play_arrow_24" + android:key="@string/prefer_media_type_key" + android:title="@string/preferred_media_settings" /> + android:icon="@drawable/ic_outline_voice_over_off_24" + android:key="@string/display_sub_key" + android:title="@string/display_subbed_dubbed_settings" /> + android:icon="@drawable/ic_baseline_extension_24" + android:key="@string/enable_nsfw_on_providers_key" + android:summary="@string/apply_on_restart" + android:title="@string/enable_nsfw_on_providers" + app:defaultValue="false" /> + + + \ No newline at end of file From 9d0cce47a67472260e553ba8acd2e4d08fd43cc9 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 20:40:50 +0000 Subject: [PATCH 016/570] update list of locales --- .../com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2e249948..354dc89c 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 @@ -68,12 +68,12 @@ val appLanguages = arrayListOf( Triple("", "español", "es"), Triple("", "فارسی", "fa"), Triple("", "français", "fr"), - Triple("\uD83C\uDDEE\uD83C\uDDF1", "עברית", "iw"), Triple("", "हिन्दी", "hi"), Triple("", "hrvatski", "hr"), Triple("", "magyar", "hu"), Triple("\uD83C\uDDEE\uD83C\uDDE9", "Bahasa Indonesia", "in"), Triple("", "italiano", "it"), + Triple("\uD83C\uDDEE\uD83C\uDDF1", "עברית", "iw"), Triple("", "ಕನ್ನಡ", "kn"), Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), From 789cd14ef6fcb9cfdd5646d79af25f16916cf3d3 Mon Sep 17 00:00:00 2001 From: Lag <> Date: Wed, 15 Feb 2023 21:41:20 +0100 Subject: [PATCH 017/570] remove placeholder --- app/src/main/res/values/strings.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cb9d5508..47517378 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -644,6 +644,4 @@ 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. - - Hello blank fragment \ No newline at end of file From 3dd0fc6c8e90e70eb413aba256da5cc4e3ab6f2e Mon Sep 17 00:00:00 2001 From: Lag <> Date: Wed, 15 Feb 2023 22:09:08 +0100 Subject: [PATCH 018/570] add view_test --- app/src/main/res/layout/view_test.xml | 138 ++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 app/src/main/res/layout/view_test.xml diff --git a/app/src/main/res/layout/view_test.xml b/app/src/main/res/layout/view_test.xml new file mode 100644 index 00000000..99300ce4 --- /dev/null +++ b/app/src/main/res/layout/view_test.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From aacd57cb5d2ae860fad11640d0ada1fe3fd55d2d Mon Sep 17 00:00:00 2001 From: Lag <> Date: Thu, 16 Feb 2023 01:15:30 +0100 Subject: [PATCH 019/570] Fixed scrolling up on bottom dialogs and removing stuff from AniList --- .../syncproviders/providers/AniListApi.kt | 53 +++++++++++++----- .../ui/result/ResultFragmentPhone.kt | 2 - .../ui/result/ResultFragmentTv.kt | 5 +- .../layout/bottom_selection_dialog_direct.xml | 54 +++++++++---------- 4 files changed, 69 insertions(+), 45 deletions(-) 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 7d9de43a..0010ce25 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 @@ -759,6 +759,11 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { return data != "" } + /** Used to query a saved MediaItem on the list to get the id for removal */ + data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null) + data class MediaListItem(@JsonProperty("MediaList") val MediaList: MediaListId? = null) + data class MediaListId(@JsonProperty("id") val id: Long? = null) + private suspend fun postDataAboutId( id: Int, type: AniListStatusType, @@ -766,19 +771,43 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { progress: Int? ): Boolean { val q = - """mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${ - aniListStatusString[maxOf( - 0, - type.value - )] - }, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) { - SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) { - id - status - progress - score - } + // Delete item if status type is None + if (type == AniListStatusType.None) { + val userID = getKey(accountId, ANILIST_USER_KEY)?.id ?: return false + // Get list ID for deletion + val idQuery = """ + query MediaList(${'$'}userId: Int = $userID, ${'$'}mediaId: Int = $id) { + MediaList(userId: ${'$'}userId, mediaId: ${'$'}mediaId) { + id + } + } + """ + val response = postApi(idQuery) + val listId = + tryParseJson(response)?.data?.MediaList?.id ?: return false + """ + mutation(${'$'}id: Int = $listId) { + DeleteMediaListEntry(id: ${'$'}id) { + deleted + } + } + """ + } else { + """mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${ + aniListStatusString[maxOf( + 0, + type.value + )] + }, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) { + SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) { + id + status + progress + score + } }""" + } + val data = postApi(q) return data != "" } 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 b38e1765..2f232995 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 @@ -322,9 +322,7 @@ class ResultFragmentPhone : ResultFragment() { // it?.dismiss() //} builder.setCanceledOnTouchOutside(true) - builder.show() - builder } } 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 2bd8ff0f..71ecb0e9 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 @@ -176,8 +176,7 @@ class ResultFragmentTv : ResultFragment() { loadingDialog = null } loadingDialog = loadingDialog ?: context?.let { ctx -> - val builder = - BottomSheetDialog(ctx) + val builder = BottomSheetDialog(ctx) builder.setContentView(R.layout.bottom_loading) builder.setOnDismissListener { loadingDialog = null @@ -187,9 +186,7 @@ class ResultFragmentTv : ResultFragment() { // it?.dismiss() //} builder.setCanceledOnTouchOutside(true) - builder.show() - builder } } diff --git a/app/src/main/res/layout/bottom_selection_dialog_direct.xml b/app/src/main/res/layout/bottom_selection_dialog_direct.xml index 0d179ebb..cf31ba1f 100644 --- a/app/src/main/res/layout/bottom_selection_dialog_direct.xml +++ b/app/src/main/res/layout/bottom_selection_dialog_direct.xml @@ -1,34 +1,34 @@ + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + android:id="@+id/text1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_rowWeight="1" + android:layout_marginTop="20dp" + android:layout_marginBottom="10dp" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:textColor="?attr/textColor" + android:textSize="20sp" + android:textStyle="bold" + tools:text="Test" /> + android:id="@+id/listview1" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_rowWeight="1" + android:layout_marginBottom="60dp" + android:nestedScrollingEnabled="true" + android:nextFocusLeft="@id/apply_btt" + android:nextFocusRight="@id/cancel_btt" + android:paddingTop="10dp" + android:requiresFadingEdge="vertical" + tools:listitem="@layout/sort_bottom_single_choice_no_checkmark" /> From b6ac155350cf6d4b070e79276efd6389b5858f05 Mon Sep 17 00:00:00 2001 From: MhmdIbrahim1 <107378571+MhmdIbrahim1@users.noreply.github.com> Date: Fri, 17 Feb 2023 23:42:20 +0200 Subject: [PATCH 020/570] update VideoDownloadService (#377) --- .../services/VideoDownloadService.kt | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt b/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt index be2fe75b..6151a0ed 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt @@ -1,11 +1,22 @@ package com.lagradost.cloudstream3.services - -import android.app.IntentService +import android.app.Service import android.content.Intent +import android.os.IBinder import com.lagradost.cloudstream3.utils.VideoDownloadManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch -class VideoDownloadService : IntentService("VideoDownloadService") { - override fun onHandleIntent(intent: Intent?) { +class VideoDownloadService : Service() { + + private val downloadScope = CoroutineScope(Dispatchers.Default) + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent != null) { val id = intent.getIntExtra("id", -1) val type = intent.getStringExtra("type") @@ -14,10 +25,36 @@ class VideoDownloadService : IntentService("VideoDownloadService") { "resume" -> VideoDownloadManager.DownloadActionType.Resume "pause" -> VideoDownloadManager.DownloadActionType.Pause "stop" -> VideoDownloadManager.DownloadActionType.Stop - else -> return + else -> return START_NOT_STICKY + } + + downloadScope.launch { + VideoDownloadManager.downloadEvent.invoke(Pair(id, state)) } - VideoDownloadManager.downloadEvent.invoke(Pair(id, state)) } } + + return START_NOT_STICKY } -} \ No newline at end of file + + override fun onDestroy() { + downloadScope.coroutineContext.cancel() + super.onDestroy() + } +} +// override fun onHandleIntent(intent: Intent?) { +// if (intent != null) { +// val id = intent.getIntExtra("id", -1) +// val type = intent.getStringExtra("type") +// if (id != -1 && type != null) { +// val state = when (type) { +// "resume" -> VideoDownloadManager.DownloadActionType.Resume +// "pause" -> VideoDownloadManager.DownloadActionType.Pause +// "stop" -> VideoDownloadManager.DownloadActionType.Stop +// else -> return +// } +// VideoDownloadManager.downloadEvent.invoke(Pair(id, state)) +// } +// } +// } +//} From b4065b69beeb6ab12298aba3ea74583fe5f7372f Mon Sep 17 00:00:00 2001 From: no-commit <> Date: Fri, 17 Feb 2023 23:05:11 +0100 Subject: [PATCH 021/570] Added dropdown indicators Solves #375 --- app/src/main/res/layout/fragment_result.xml | 23 ++++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index afbf735d..a481ed6b 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"> @@ -516,8 +515,8 @@ android:visibility="gone" /> نصٌّ حكيمٌ لهُ سِرٌّ قاطِعٌ وَذُو شَأنٍ عَظيمٍ مكتوبٌ على ثوبٍ أخضرَ ومُغلفٌ بجلدٍ أزرق - مُوصي به + مُوصى به تم تحميل %s إختيار ملف تحميل من الانترنت @@ -543,4 +543,16 @@ تلفزيون أندرويد مدة التقديم عنما يكون المشغل مرئيا مدة التقديم- المشغل المرئي + فشل + نجح + إختبار المزود + إعادة التشغيل + سجل + بَدأ + إيقاف + تحديث العروض التي تم الاشتراك فيها + إلغاء الاشتراك من %s + تم إصدار الحلقة %d! + مشترك + مشترك في %s \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index a78da8a4..966cd7d9 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -535,4 +535,16 @@ Zobrazený přehrávač - doba hledání Android TV Množství vyhledávané doby při zobrazeném přehrávači + Protokol + Test poskytovatele + Neúspěšné + Úspěšné + Restart + Spustit + Zastavit + Aktualizace odebíraných pořadů + Přihlášeno k odběru %s + Odhlášen odběr od %s + Byla vydána epizoda %d! + Odebíráno \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e190aa9c..f6583c20 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -53,7 +53,7 @@ Abgebrochen Geplant Nichts - Erneut anschauen + Erneut schauen Film abspielen Livestream abspielen Torrent streamen @@ -212,7 +212,7 @@ Keine Untertitel Standard Frei - Benutzt + Belegt App Filme TV-Serien @@ -284,7 +284,7 @@ Strecken Vergrößern Haftungsausschluss - General + Allgemein Zufalls-Button Zufallsbutton auf der Startseite anzeigen Anbieter-Sprachen @@ -460,11 +460,11 @@ Automatische Installation aller noch nicht installierten Plugins aus hinzugefügten Repositories. Einrichtungsvorgang wiederholen APK-Installer - Einige Telefone unterstützen das neue Installationsprogramm für Pakete nicht. Benutze die Legacy-Option, wenn sich die Updates nicht installieren lassen. + Einige Telefone unterstützen den neuen Package-Installer nicht. Benutze die Legacy-Option, wenn sich die Updates nicht installieren lassen. %s %d%s Links App-Updates - Back-Up + Sicherung Erweiterungen Wartung Cache @@ -506,4 +506,16 @@ Diese Liste scheint leer zu sein. Versuche, zu einer anderen Liste zu wechseln. Datei für abgesicherten Modus gefunden! \nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird. + Player ausgeblendet - Betrag zum vor- und zurückspulen + Der Betrag, welcher verwendet wird, wenn der Player eingeblendet ist + Der Betrag, welcher verwendet wird, wenn der Player ausgeblendet ist + Android-TV + Player eingeblendet - Betrag zum vor- und zurückspulen + Fehlgeschlagen + Erfolgreich + Anbieter-Test + Stopp + Log + Start + Neustarten \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 0d0b7fb2..5e9dafd8 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -150,7 +150,7 @@ Επεισόδια %d-%d %d %s - Κ + Σ E Δεν βρέθηκαν επεισόδια Διαγραφή αρχείου diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 8366b294..2040169b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -511,4 +511,16 @@ Android TV La cantidad de búsqueda utilizada cuando la jugadora es visible La cantidad de búsqueda utilizada cuando el jugador está oculto + Parar + Falló + Registro + Empezar + Aprobado + Prueba del proveedor + Reiniciar + Suscrito + Suscrito a %s + Darse de baja de %s + Actualizando los programas suscritos + ¡Episodio %d publicado! \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f0e112a8..18255b3b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -6,12 +6,12 @@ Téléchargements Paramètres Rechercher… - Miniature + Affiche Aucune Donnée Plus d\'options Retour Épisode suivant - Miniature + Affiche Genres Partager Ouvrir dans le navigateur @@ -29,7 +29,7 @@ Sous-titres Réessayer la connection… Retour - Miniature de l\'Épisode + Affiche de l\'épisode Lire l\'Épisode Télécharger @@ -51,10 +51,10 @@ Désactiver le rapport de bug automatique Plus d\'informations Cacher - Poster principal + Affiche principale Lecture - Info - Suivant Aléatoire + Infos + Aléatoire suivant Changer le fournisseur Filtrer les marques-pages Marque-pages @@ -211,7 +211,7 @@ Arrière plan Source Aléatoire - À venir … + Bientôt disponible… Image de l\'affiche %s Connecté Définir le statut de visionage @@ -490,4 +490,22 @@ L\'application sera mise à jour dès la fin de la session Plugin Téléchargé Retirer de la vue + Bibliothèque + Navigateur + Trier + Note (basse à haute) + Note (haut à bas) + Alphabétique (A à Z) + On dirait que votre bibliothèque est vide :( +\nConnectez-vous à un compte ou ajoutez des séries à votre bibliothèque locale + Il semble que cette liste soit vide, essayez d\'en choisir une autre + Android TV + Trié par + Alphabétique (Z à A) + Sélectionnez la bibliothèque + Ouvrir avec + Mis à jour (Nouveau vers ancien) + Mis à jour (ancien vers nouveau) + Fichier du mode sans échec trouvé ! +\nAucune extension ne sera chargée au démarrage avant que le fichier ne soit enlevé. \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 0f3e36bc..926c7f57 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -531,4 +531,21 @@ Č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. + Prikazan player- iznos preskakanja + Količina preskakanja koja se koristi kada je player vidljiv + Player skriven - Količina preskakanja + Količina preskakanja koja se koristi kada je player skriven + Android TV + Prošlo + Restart + Log + Početak + Neuspješno + Stop + Test pružatelja usluga + Ažuriram pretplaćene serije + Epizoda %d izbačena! + Pretplaćeno + Pretplaćen na %s + Otkazana pretplata sa %s \ 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 d54e4fa9..46d61e44 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -35,7 +35,7 @@ Skip Loading Loading… Sedang Menonton - Tertahan + Tertunda Selesai Dihentikan Rencana untuk Menonton @@ -387,7 +387,7 @@ %d %s 17+ Lainnya - Vidio + Video Duplikasi Website Duplikasi website yang telah ada, dengan alamat berbeda Tautan @@ -395,7 +395,7 @@ Cadangkan Fitur Tambahan Putar di CloudStream - Sembunyikan kualitas vidio terpilih di pencarian + Sembunyikan kualitas video terpilih di pencarian %s %d%s Siaran langsung Hapus Website @@ -444,7 +444,7 @@ Peringkat: %s Pembuat Bahasa - Pemutar vidio utama + Pemutar video utama Pemutar Bawaan VLC MPV @@ -475,7 +475,7 @@ Hapus teks tertutup dari subtitel Hapus karakter sampah dari subtitel Audio Trek - Vidio Trek + Video Trek Dukungan Daftar putar HLS Penginstal APK @@ -529,4 +529,21 @@ Yahh daftar ini kosong, coba ganti ke yang lain Mode aman file ditemukan! \nTidak memuat ekstensi pada startup sampai berkas dihapus. + Sembunyikan Pemutaran - Geser + Pemutar terlihat - Geser + Geser untuk menghilangkan + Geser untuk menghilangkan + Android TV + Log + Berhasil + Tes provider + Berhenti + Mulai + Mulai lagi + Gagal + Memperbarui acara langganan + Berlangganan + Berlangganan ke %s + Berhenti berlangganan di %s + Episode %d telah rilis! \ 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 9dbc627f..89f6b4ee 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -528,4 +528,16 @@ 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. + Quantità di ricerca usata quando il player è nascosto + TV Android + Quantità di ricerca usata quando il player è visibile + Player visibile - Quantità di ricerca + Player nascosto - Quantità di ricerca + Registro + Avvia + Test del provider + Riavvia + Ferma + Superato + Fallito \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 00000000..a3d1d434 --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,185 @@ + + + %d分 + ダウンロード + 検索 + 設定 + シェア + 映画 + ホーム + ライブラリ + 再生 + %d日 %d時間%d分 + %d時間%d分 + 検索… + ダウンロード + 情報 + シーズン + 予告編 + シリーズ + エピソード + 再生速度 (%.2fx) + 次のエピソード + 適用 + アカウント + カートゥーン + TVシリーズ + トレント + ドキュメンタリー + OVA + アジアドラマ + ライブ配信 + 映画 + その他 + カートゥーン + トレント + ドキュメンタリー + アジアドラマ + ライブ配信 + NSFW + キャンセル + アニメ + ロック + ソース + NSFW + 履歴を削除 + 視聴中コンテンツ + 全般 + 動画 + プレーヤー + 懐う + 予告編を再生 + エピソード + 視聴 + ジャンル + 映画を再生 + 字幕 + CloudStream + CloudStreamで再生 + ブラウザ + 完成 + 放置 + 保留 + ローディング… + ブラウザで開く + シーズン + 残り +\n%d分 + 再生エピソード + ダウンロード済 + バックアップ + ソース + 履歴 + ポスター + なし + コピー + 閉じる + 保存 + 消去 + %sエピ%d + 出演者:%s + ポスター + エピソードポスター + 主要ポスター + 次のランダム + 戻り + 視聴率 %.1f + 新しいアップデートを発見! +\n%s -> %s + %d分 + %sを検索… + ソース + ろくごうきじ + 接続を再試行… + 戻り + 削除 + 詳細情報 + 閉じる + アップデート・バックアップ + アプリ言語 + GitHub(ギットハブ) + -30 + +30 + 免責 + 拡張機能 + アプリ更新 + 提供者 + 字幕 + 特徴 + デフォルト + 自動 + 任意 + 拡張機能 + リンク + Android TV + ログイン + ログアウト + 最大 + 最小 + なし + + 18+ + + で開く + エピソード + 時間 + 概要 + サイト + 使用 + アプリ + 詳細情報 + 削除 + ピクチャーインピクチャー + 字幕 + 情報 + 一時停止 + 再生エピソード + 削除 + 開始 + 状態 + + 再開 + 失敗 + 合格 + 空き + 完成 + 進行中 + デフォルト + ウェブブラウザ + VLC + MPV + 言語 + 作成者 + サイズ + 状態 + バージョン + 視聴率 %s + 視聴率 + デフォルト + ダウンロード失敗 + ダウンロード開始 + ダウンロード完了 + ダウンロード終了 + ストリーム + アップデート開始 + シーズンなし + 字幕なし + アスペクト比 + ロードをスキップする + その他のオプション + データなし + ダウンロード中 + ブックマーク + 内部記憶装置 + ダウンロードが一時停止 + メタデータはこのサイトでは提供されません。メタデータがサイト上に存在しない場合、ビデオの読み込みに失敗します。 + 記述 + Logcat 🐈を表示 + ログ + 検索 + Discordに参加 + アップデート + アップデートを確認 + 作品名 + アプリのアップデートをインストール中… + \ 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 efe0a1d8..c36459b7 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -1,3 +1,128 @@ - \ No newline at end of file + %sಎಪಿ%d + ಕ್ಯಾಸ್ಟ್:%s + ಹಿಂದೆ ಹೋಗು + ಫಿಲ್ಲರ್ + ಹುಡುಕು + ಡೌನ್ಲೋಡ್ + ಫಾಂಟ್ + ಪೂರೈಕೆದಾರರನ್ನು ಬಳಸಿಕೊಂಡು ಹುಡುಕಿ + ಪ್ರಕಾರಗಳನ್ನು ಬಳಸಿಕೊಂಡು ಹುಡುಕಿ + ಯಾವುದೇ ಬೆನೆನ್ಸ್ ನೀಡಿಲ್ಲ + ಸ್ವಯಂ-ಆಯ್ಕೆ ಭಾಷೆ + ಹೆಚ್ಚಿನ ಮಾಹಿತಿ + \@ಸ್ಟ್ರಿಂಗ್/ಹೋಮ್_ಪ್ಲೇ + ಈ ಪೂರೈಕೆದಾರರು ಸರಿಯಾಗಿ ಕೆಲಸ ಮಾಡಲು VPN ಬೇಕಾಗಬಹುದು + ಕಪ್ಪು ಗಡಿಗಳನ್ನು ತೆಗೆದುಹಾಕಿ + ಸಂಚಿಕೆ%d ಬಿಡುಗಡೆಯಾಗಲಿದೆ + %dh %dm + ಪೋಸ್ಟರ್ + ಪೋಸ್ಟರ್ + ಸಂಚಿಕೆ ಪೋಸ್ಟರ್ + ಮೇನ್ ಪೋಸ್ಟರ್ + ಅಪ್ಡೇಟ್ ಪ್ರಾರಂಭವಾಗಿದೆ + ಲೋಡಿಂಗ್ ಲಿಂಕ್ ಎರರ್ ಬಂದಿದೆ + ಇಂಟರ್ನಲ್ ಸ್ಟೋರೇಜ್ + ಡಬ್ + ಸಬ್ + ಸ್ವಯಂಚಾಲಿತ ದೋಷ ವರದಿಯನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿ + ಹೈಡ್ + ಪ್ಲೇ + ಮಾಹಿತಿ + ಸೆಟ್ ವಾಚ್ ಸ್ಟೇಟಸ್ + ಅನ್ವಯಿಸು + ರದ್ದುಮಾಡು + ಸಬ್ ಟೈಟಲ್ಸ್ ಎಲೆವಷನ್ + ಫಾಂಟ್ ಸೈಜ್ + ಸಬ್ ಟೈಟಲ್ಸ್ ಭಾಷೆ + ತೆಗೆದುಹಾಕಿ + ಈ ಪೂರೈಕೆದಾರರು ಟೊರೆಂಟ್ ಆಗಿದೆ, VPN ಅನ್ನು ಶಿಫಾರಸು ಮಾಡಲಾಗಿದೆ + ಯಾವುದೇ ಪ್ಲಾಟ್ ಕಂಡುಬಂದಿಲ್ಲ + ಲಾಗ್‌ಕ್ಯಾಟ್ 🐈 ತೋರಿಸಿ + ಲಾಗ್ + ಚಿತ್ರದಲ್ಲಿ-ಚಿತ್ರದಲ್ಲಿ + ಪ್ಲೇಯರ್ ಮರುಗಾತ್ರಗೊಳಿಸಿ ಬಟನ್ + ಸಬ್ ಟೈಟಲ್ಸ್ + ಪ್ಲೇಯರ್ ಸಬ್ ಟೈಟಲ್ಸ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳು + ಕ್ರೋಮ್ ಕ್ಯಾಸ್ಟ್ ಸಬ್ ಟೈಟಲ್ಸ್ ಸೆಟ್ಟಿಂಗ್ಸ್ + ಹಿಂದೆ ಹೋಗು + ಡೌನ್‌ಲೋಡ್ ವಿರಾಮಗೊಳಿಸಿ + ಬುಕ್‌ಮಾರ್ಕ್‌ + ಬ್ಯಾಕ್ ಗ್ರೌಂಡ್ ಕಲರ್ + %d ಡೇವ್‌ಗಳಿಗೆ ಬೆನೆನೆಸ್ ನೀಡಲಾಗಿದೆ + ಡೀಫಾಲ್ಟ್‌ಗೆ ಮರುಹೊಂದಿಸಲು ಹಿಡಿದುಕೊಳ್ಳಿ + ಸೈಟ್‌ನಿಂದ ಮೆಟಾಡೇಟಾವನ್ನು ಒದಗಿಸಲಾಗಿಲ್ಲ, ಅದು ಸೈಟ್‌ನಲ್ಲಿ ಅಸ್ತಿತ್ವದಲ್ಲಿಲ್ಲದಿದ್ದರೆ ವೀಡಿಯೊ ಲೋಡಿಂಗ್ ವಿಫಲಗೊಳ್ಳುತ್ತದೆ. + ಇತರ ಅಪ್ಲಿಕೇಶನ್‌ಗಳ ಮೇಲೆ ಚಿಕಣಿ ಪ್ಲೇಯರ್‌ನಲ್ಲಿ ಪ್ಲೇಬ್ಯಾಕ್ ಅನ್ನು ಮುಂದುವರಿಸುತ್ತದೆ + ಕ್ರೋಮ್ ಕ್ಯಾಸ್ಟ್ ಸಬ್ ಟೈಟಲ್ಸ್ + ರೇಟೆಡ್:%.1f + ತೆಗೆದುಹಾಕಿ + ಡೌನ್‌ಲೋಡ್ ಅನ್ನು ಪುನರಾರಂಭಿಸಿ + ಕ್ಲೋಸ್ + ಕ್ಲಿಯರ್ + ಸೇವ್ + ಸಬ್ ಟೈಟಲ್ಸ್ ಸೆಟ್ಟಿಂಗ್ಸ್ + ಫೈಲ್ ಪ್ಲೇ + ಟೆಕ್ಸ್ಟ್ ಕಲರ್ + ಔಟ್ ಲೈನ್ ಕಲರ್ + ವಿಂಡೋ ಕಲರ್ + ಎಡ್ಜ್ ಟೈಪ್ + ಪ್ರೊವೈಡರ್ ಬದಲಾಯಿಸಿ + %dಮಿನ + ವಿವರಣೆ + ಸ್ಪೀಡ್(%.2fx) + ಹೋಂ + ಸಬ್ ಟೈಟಲ್ಸ್ + ಸೆಟ್ಟಿಂಗ್ಸ್ + ಬುಕ್‌ಮಾರ್ಕ್‌ಗಳನ್ನು ಫಿಲ್ಟರ್ ಮಾಡಿ + ಹುಡುಕು… + ಚಲನಚಿತ್ರವನ್ನು ಪ್ಲೇ ಮಾಡಿ + ಪ್ರಿವ್ಯೂ ಹಿನ್ನೆಲೆ + ಮುಂದಿನ ಸಂಚಿಕೆ + ಕ್ಲೌಡ್ ಸ್ಟ್ರೀಮ್ + ಡೌನ್‌ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ + ಸ್ಟ್ರೀಮ್ + ಶೇರ್ + ಫೈಲ್ ಅಳಿಸಿ + ಹೆಚ್ಚಿನ ಮಾಹಿತಿ + ಹೊಸ ಅಪ್ಡೇಟ್ ಬಂದಿದೆ +\n%s-%s + ಲೋಡಿಂಗ್… + ಡೌನ್‌ಲೋಡ್ ಭಾಷೆಗಳನ್ನು ಮಾಡಿ + ಲೈವ್‌ಸ್ಟ್ರೀಮ್ ಪ್ಲೇ ಮಾಡಿ + ಕ್ಲೌಡ್ ಸ್ಟ್ರೀಮ್ ಇದರೊಂದಿಗೆ ಪ್ಲೇ ಮಾಡಿ + ವೀಕ್ಷಿಸಲು ಯೋಜನೆ + ಸಂಚಿಕೆಯನ್ನು ಪ್ಲೇ ಮಾಡಿ + ಕಂಟಿನ್ಯೂ ವಾಟಚಿಂಗ್ + ಯಾವುದೇ ವಿವರಣೆ ಕಂಡುಬಂದಿಲ್ಲ + ಸ್ಟ್ರೀಮ್ ಟೊರೆಂಟ್ + ಡೌನ್‌ಲೋಡ್ + ಕಾಪಿ + ನೋ ಡೇಟಾ + ಪ್ಲೇಯರ್ ಸ್ಪೀಡ್ + %d %dh %dm + ಹುಡುಕು %s… + ಹೆಚ್ಚಿನ ಆಯ್ಕೆ + ಫಾಂಟ್‌ಗಳನ್ನು ಇರಿಸುವ ಮೂಲಕ ಆಮದು ಮಾಡಿ %s + %dm + ಪ್ರಕಾರಗಳು + ಬ್ರೌಸರ್ ತೆರೆಯಿರಿ + ಆನ್-ಹೋಲ್ಡ್ + ನನ್ + ಸಂಪರ್ಕವನ್ನು ಮರುಪ್ರಯತ್ನಿಸಿ… + ಡೌನ್‌ಲೋಡ್ ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ + ಡೌನ್‌ಲೋಡ್ ವಿಫಲವಾಗಿದೆ + ಡೌನ್‌ಲೋಡ್ ಮುಗಿದಿದೆ + ಬ್ರೌಸರ್ + ಸ್ಕಿಪ್ ಲೋಡಿಂಗ್ + ವಾಚಿಂಗ್ + ಪೂರ್ಣಗೊಂಡಿದೆ + ಕೈಬಿಡಲಾಯಿತು + ಪುನಃ ವೀಕ್ಷಿಸುತ್ತಿದೆ + ಟ್ರೈಲರ್ ಪ್ಲೇ ಮಾಡಿ + ಮೂಲಗಳು + ಡೌನ್‌ಲೋಡ್ ಮಾಡಲಾಗಿದೆ + ಡೌನ್‌ಲೋಡ್ ಪ್ರಾರಂಭವಾಗಿದೆ + ಡೌನ್‌ಲೋಡ್ ರದ್ದುಗೊಳಿಸಲಾಗಿದೆ + ಮುಂದಿನ ರಾಂಡಮ್ + \ 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 c709f124..411f0b45 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -373,7 +373,7 @@ Pomiń setup Dostosuj wygląd aplikacji do urządzenia Zgłaszanie błędów - Co chciałbyś obejrzeć\? + Co chciałbyś obejrzeć Gotowe Rozszerzenia Dodaj repozytorium @@ -509,4 +509,9 @@ 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. + Używana ilość przewijania, gdy widoczny jest odtwarzacz + Ukryty odtwarzacz - ilość przewijania + Android TV + Pokazany odtwarzacz — ilość przewijania + Używana ilość przewijania, gdy ukryty jest odtwarzacz \ 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 982546bc..42d9b7c8 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 + Poster Poster Episod Poster Principal Următorul la Întâmplare @@ -142,7 +142,7 @@ Fișier de rezervă încărcat Imposibilitatea de a restaura datele din %s Date stocate - Permisiuni de arhivare lipsă, vă rugăm să încercați din nou + Permisiunea de arhivare lipșe, vă rugăm să încercați din nou. Eroare de backup %s Căutare Conturi și credite @@ -154,7 +154,7 @@ Nu trimiteți niciun fel de date Afișează etichetele [filler] pentru anime Arată trailerul - Arată posterele de la Kitsu + Arată afișele de la Kitsu Afișați actualizările aplicației Căutați automat noi actualizări la pornire Actualizați la prerelease @@ -384,4 +384,8 @@ Începe următorul episod când se termină episodul curent Ascundeți calitatea video selectată în rezultatele căutării Redare Livestream + Librărie + Log + Browser + Joacă cu CloudStream \ 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 6e9fb394..2812667a 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -506,4 +506,16 @@ Плеер показан - Перемотки объем Плеер спрятан - Перемотки объем Удалять лишнее из субтитров + Местоположение ползунка, когда игрок скрыт + Android TV + Второго планa + Смешанный опенинг + Смешанный конец + Тест провайдер + Журнал + Запустить + Выполнено + Неудачный + Прекратить + Перезапустить \ 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 index 97039233..66d8ada9 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -17,7 +17,7 @@ %dd %dh %dm %dm %d min - \@string/result_poster_img_des + Plagát Plagát epizódy Hlavný plagát Prehrať s CloudStream diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 871e0a28..5330d3ec 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -507,4 +507,20 @@ Файл безпечного режиму знайдено! \nРозширеня не завантажуються під час запуску, доки файл не буде видалено. Android TV + Плеєр сховано - обсяг пошуку + Плеєр показано - обсяг пошуку + Обсяг пошуку, який використовується, коли плеєр видимий + Обсяг пошуку, який використовується, коли гравець прихований + Не вдалося + Пройдено + Перезапуск + Журнал + Старт + Стоп + Тест постачальника + Оновлення підписаних шоу + Підписано + Підписано на %s + Відписатися від %s + Епізод %d випущено! \ 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 a8341d46..8a10208a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -19,7 +19,7 @@ %dm 封面 - \@string/result_poster_img_des + 封面 劇集封面 主封面 隨機下一個 @@ -533,4 +533,5 @@ 預設 外觀 功能 + 瀏覽器 \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index c57e3ca1..9e2d6137 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -554,4 +554,21 @@ 看来您的库是空的 :( \n登录库账户或添加节目到您的本地库 看来此列表是空的,请尝试切换到另一个 + 播放器显示 - 快进快退秒数 + 播放器可见时使用的快进快退秒数 + 播放器隐藏 - 快进快退秒数 + 播放器隐藏时使用的快进快退秒数 + Android TV + 失败 + 片源测试 + 重启 + 停止 + 正在更新订阅节目 + 已订阅 + 已订阅 %s + 已取消订阅 %s + 开始 + 第 %d 集已发布! + 成功 + 日志 \ No newline at end of file From 51137701f2c6228660720fba94d9dfc53cefb582 Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Tue, 21 Feb 2023 18:43:35 +0100 Subject: [PATCH 025/570] add proxy to raw.githubusercontent.com (#368) --- .../lagradost/cloudstream3/MainActivity.kt | 34 ++++++++++++++++++ .../cloudstream3/plugins/RepositoryManager.kt | 30 ++++++++++++---- .../ui/settings/SettingsGeneral.kt | 13 ++++--- .../lagradost/cloudstream3/utils/AppUtils.kt | 8 ++++- app/src/main/res/values/strings.xml | 6 ++++ app/src/main/res/xml/settins_general.xml | 36 ++++++++++++------- 6 files changed, 102 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 28419e7a..e626dcd6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -32,7 +32,9 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.google.android.gms.cast.framework.* import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.navigationrail.NavigationRailView +import com.google.android.material.snackbar.Snackbar import com.jaredrummler.android.colorpicker.ColorPickerDialogListener +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings @@ -79,6 +81,7 @@ import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable +import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.loadRepository import com.lagradost.cloudstream3.utils.AppUtils.loadResult @@ -86,6 +89,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching @@ -717,6 +721,28 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { changeStatusBarState(isEmulatorSettings()) + // Automatically enable jsdelivr if cant connect to raw.githubusercontent.com + if (this.getKey(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) { + main { + if (checkGithubConnectivity()) { + this.setKey(getString(R.string.jsdelivr_proxy_key), false) + } else { + this.setKey(getString(R.string.jsdelivr_proxy_key), true) + val parentView: View = findViewById(android.R.id.content) + Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG).let { snackbar -> + snackbar.setAction(R.string.revert) { + setKey(getString(R.string.jsdelivr_proxy_key), false) + } + snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground)) + snackbar.setTextColor(colorFromAttribute(R.attr.textColor)) + snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary)) + snackbar.show() + } + } + + } + } + if (PluginManager.checkSafeModeFile()) { normalSafeApiCall { @@ -1090,4 +1116,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // } } + + suspend fun checkGithubConnectivity(): Boolean { + return try { + app.get("https://raw.githubusercontent.com/recloudstream/.github/master/connectivitycheck", timeout = 5).text.trim() == "ok" + } catch (t: Throwable) { + false + } + } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt index e77b2d54..742bf308 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -2,8 +2,10 @@ package com.lagradost.cloudstream3.plugins import android.content.Context import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError @@ -71,6 +73,15 @@ object RepositoryManager { val PREBUILT_REPOSITORIES: Array by lazy { getKey("PREBUILT_REPOSITORIES") ?: emptyArray() } + val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$") + + /* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */ + fun convertRawGitUrl(url: String): String { + if (getKey(context!!.getString(R.string.jsdelivr_proxy_key)) != true) return url + val match = GH_REGEX.find(url) ?: return url + val (user, repo, rest) = match.destructured + return "https://cdn.jsdelivr.net/gh/$user/$repo@$rest" + } suspend fun parseRepoUrl(url: String): String? { val fixedUrl = url.trim() @@ -84,10 +95,15 @@ object RepositoryManager { } } else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) { suspendSafeApiCall { - app.get("https://l.cloudstream.cf/${fixedUrl}").let { - return@let if (it.isSuccessful && !it.url.startsWith("https://cutt.ly/branded-domains")) it.url - else app.get("https://cutt.ly/${fixedUrl}").let let2@{ it2 -> - return@let2 if (it2.isSuccessful) it2.url else null + app.get("https://l.cloudstream.cf/${fixedUrl}", allowRedirects = false).let { + it.headers["Location"]?.let { url -> + return@suspendSafeApiCall if (!url.startsWith("https://cutt.ly/branded-domains")) url + else null + } + app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false).let { it2 -> + it2.headers["Location"]?.let { url -> + return@suspendSafeApiCall if (url.startsWith("https://cutt.ly/404")) url else null + } } } } @@ -97,14 +113,14 @@ object RepositoryManager { suspend fun parseRepository(url: String): Repository? { return suspendSafeApiCall { // Take manifestVersion and such into account later - app.get(url).parsedSafe() + app.get(convertRawGitUrl(url)).parsedSafe() } } private suspend fun parsePlugins(pluginUrls: String): List { // Take manifestVersion and such into account later return try { - val response = app.get(pluginUrls) + val response = app.get(convertRawGitUrl(pluginUrls)) // Normal parsed function not working? // return response.parsedSafe() tryParseJson>(response.text)?.toList() ?: emptyList() @@ -139,7 +155,7 @@ object RepositoryManager { } file.createNewFile() - val body = app.get(pluginUrl).okhttpResponse.body + val body = app.get(convertRawGitUrl(pluginUrl)).okhttpResponse.body write(body.byteStream(), file.outputStream()) file } 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 354dc89c..c5a11cce 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 @@ -82,7 +82,7 @@ val appLanguages = arrayListOf( Triple("", "norsk bokmål", "no"), Triple("", "polski", "pl"), Triple("\uD83C\uDDF5\uD83C\uDDF9", "português", "pt"), - Triple("🦍", "mmmm... monke", "qt"), + Triple("\uD83E\uDD8D", "mmmm... monke", "qt"), Triple("", "română", "ro"), Triple("", "русский", "ru"), Triple("", "slovenčina", "sk"), @@ -97,7 +97,7 @@ val appLanguages = arrayListOf( Triple("", "中文", "zh"), Triple("\uD83C\uDDF9\uD83C\uDDFC", "文言", "zh-rTW"), /* end language list */ -).sortedBy { it.second?.toLowerCase() } //ye, we go alphabetical, so ppl don't put their lang on top +).sortedBy { it.second.lowercase() } //ye, we go alphabetical, so ppl don't put their lang on top class SettingsGeneral : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -157,9 +157,6 @@ class SettingsGeneral : PreferenceFragmentCompat() { getPref(R.string.locale_key)?.setOnPreferenceClickListener { pref -> val tempLangs = appLanguages.toMutableList() - //if (beneneCount > 100) { - // tempLangs.add(Triple("\uD83E\uDD8D", "mmmm... monke", "mo")) - //} val current = getCurrentLocale(pref.context) val languageCodes = tempLangs.map { (_, _, iso) -> iso } val languageNames = tempLangs.map { (emoji, name, iso) -> @@ -316,6 +313,12 @@ class SettingsGeneral : PreferenceFragmentCompat() { } ?: emptyList() } + settingsManager.edit().putBoolean(getString(R.string.jsdelivr_proxy_key), getKey(getString(R.string.jsdelivr_proxy_key), false) ?: false).apply() + getPref(R.string.jsdelivr_proxy_key)?.setOnPreferenceChangeListener { _, newValue -> + setKey(getString(R.string.jsdelivr_proxy_key), newValue) + return@setOnPreferenceChangeListener true + } + getPref(R.string.download_path_key)?.setOnPreferenceClickListener { val dirs = getDownloadDirs() 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 860144ee..205f0a6b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -491,6 +491,12 @@ object AppUtils { } } + fun Context.isNetworkAvailable(): Boolean { + val manager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetworkInfo = manager.activeNetworkInfo + return activeNetworkInfo != null && activeNetworkInfo.isConnected || manager.allNetworkInfo?.any { it.isConnected } ?: false + } + fun splitQuery(url: URL): Map { val queryPairs: MutableMap = LinkedHashMap() val query: String = url.query @@ -815,4 +821,4 @@ object AppUtils { } return currentAudioFocusRequest } -} \ No newline at end of file +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0858fdfa..2d46a70d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,6 +43,7 @@ random_button_key provider_lang_key dns_key + jsdelivr_proxy_key download_path_key Cloudstream app_layout_key @@ -378,6 +379,9 @@ Causes problems if set too high on devices with low storage space, such as Android TV. DNS over HTTPS Useful for bypassing ISP blocks + raw.githubusercontent.com Proxy + Failed to reach GitHub, enabling jsdelivr proxy. + Bypasses blocking of GitHub using jsdelivr, may cause updates to be delayed by few days. Clone site Remove site Add a clone of an existing site, with a different URL @@ -405,6 +409,7 @@ responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream 3 at your own risk. + ISP Bypasses Links App updates Backup @@ -644,6 +649,7 @@ 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. + Revert Updating subscribed shows Subscribed Subscribed to %s diff --git a/app/src/main/res/xml/settins_general.xml b/app/src/main/res/xml/settins_general.xml index 726f3fd0..c4900bca 100644 --- a/app/src/main/res/xml/settins_general.xml +++ b/app/src/main/res/xml/settins_general.xml @@ -6,18 +6,6 @@ android:title="@string/app_language" android:icon="@drawable/ic_baseline_language_24" /> - - - - + + + + + + + + + + From 1da6a925692de08cb8e8ec00d93d1e5d1f76b8c6 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 18:07:04 +0000 Subject: [PATCH 026/570] update list of locales --- .../com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 1 + 1 file changed, 1 insertion(+) 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 c5a11cce..078419e2 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 @@ -74,6 +74,7 @@ val appLanguages = arrayListOf( Triple("\uD83C\uDDEE\uD83C\uDDE9", "Bahasa Indonesia", "in"), Triple("", "italiano", "it"), Triple("\uD83C\uDDEE\uD83C\uDDF1", "עברית", "iw"), + Triple("", "日本語 (にほんご)", "ja"), Triple("", "ಕನ್ನಡ", "kn"), Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), From aeab423d2929d1de6dd2681592e2e832f261a935 Mon Sep 17 00:00:00 2001 From: Lag <> Date: Fri, 24 Feb 2023 18:47:54 +0100 Subject: [PATCH 027/570] Excluded the referer header when empty --- .../java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 4772a7f1..cd384c6f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -40,6 +40,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File +import java.time.Duration import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession @@ -535,15 +536,16 @@ class CS3IPlayer : IPlayer { OkHttpDataSource.Factory(client).setUserAgent(USER_AGENT) } + // Do no include empty referer, if the provider wants those they can use the header map. + val refererMap = if (link.referer.isBlank()) emptyMap() else mapOf("referer" to link.referer) val headers = mapOf( - "referer" to link.referer, "accept" to "*/*", "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", "sec-ch-ua-mobile" to "?0", "sec-fetch-user" to "?1", "sec-fetch-mode" to "navigate", "sec-fetch-dest" to "video" - ) + link.headers // Adds the headers from the provider, e.g Authorization + ) + refererMap + link.headers // Adds the headers from the provider, e.g Authorization return source.apply { setDefaultRequestProperties(headers) From f722785a379547fb9f28893e0661eeff186b0a3d Mon Sep 17 00:00:00 2001 From: Hexated <37908684+hexated@users.noreply.github.com> Date: Sat, 25 Feb 2023 01:49:53 +0700 Subject: [PATCH 028/570] fixed Linkbox (#390) --- .../cloudstream3/extractors/Linkbox.kt | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt index c28a8900..6a4945bb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt @@ -18,31 +18,36 @@ open class Linkbox : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val id = Regex("""(/file/|id=)(\S+)[&/?]""").find(url)?.groupValues?.get(2) - app.get("$mainUrl/api/open/get_url?itemId=$id", referer=url).parsedSafe()?.data?.rList?.map { link -> - callback.invoke( - ExtractorLink( - name, - name, - link.url, - url, - getQualityFromName(link.resolution) + val id = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1) + app.get("$mainUrl/api/file/detail?itemId=$id", referer = url) + .parsedSafe()?.data?.itemInfo?.resolutionList?.map { link -> + callback.invoke( + ExtractorLink( + name, + name, + link.url ?: return@map null, + url, + getQualityFromName(link.resolution) + ) ) - ) - } + } } - data class RList( - @JsonProperty("url") val url: String, - @JsonProperty("resolution") val resolution: String?, + data class Resolutions( + @JsonProperty("url") val url: String? = null, + @JsonProperty("resolution") val resolution: String? = null, + ) + + data class ItemInfo( + @JsonProperty("resolutionList") val resolutionList: ArrayList? = arrayListOf(), ) data class Data( - @JsonProperty("rList") val rList: List?, + @JsonProperty("itemInfo") val itemInfo: ItemInfo? = null, ) data class Responses( - @JsonProperty("data") val data: Data?, + @JsonProperty("data") val data: Data? = null, ) } \ No newline at end of file From 2926dc6c8eea53a35006b4188f9a03a9a9bb5216 Mon Sep 17 00:00:00 2001 From: Allen Baby <64322605+allenbaby@users.noreply.github.com> Date: Sat, 25 Feb 2023 00:21:03 +0530 Subject: [PATCH 029/570] Issue #376: Added new feature for separate watch quality on mobile data. (#391) * Issue #376: Added new feature for separate watch quality on mobile data. --- .../cloudstream3/ui/player/CS3IPlayer.kt | 3 ++- .../ui/player/FullScreenPlayer.kt | 4 ++-- .../ui/settings/SettingsPlayer.kt | 24 +++++++++++++++++++ .../lagradost/cloudstream3/utils/AppUtils.kt | 7 +++++- app/src/main/res/values/strings.xml | 6 +++-- app/src/main/res/xml/settings_player.xml | 4 ++++ 6 files changed, 42 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index cd384c6f..782e3fa4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -35,6 +35,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.EpisodeSkip +import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorUri @@ -849,7 +850,7 @@ class CS3IPlayer : IPlayer { Log.i(TAG, "loadExo") val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) val maxVideoHeight = settingsManager.getInt( - context.getString(com.lagradost.cloudstream3.R.string.quality_pref_key), + context.getString(if (context.isUsingMobileData()) com.lagradost.cloudstream3.R.string.quality_pref_mobile_data_key else com.lagradost.cloudstream3.R.string.quality_pref_key), Int.MAX_VALUE ) 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 8d28fd9d..d1b2814d 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 @@ -40,6 +40,7 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvidersIsActive import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -1246,9 +1247,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { ctx.getString(R.string.double_tap_pause_enabled_key), false ) - currentPrefQuality = settingsManager.getInt( - ctx.getString(R.string.quality_pref_key), + ctx.getString(if (ctx.isUsingMobileData()) R.string.quality_pref_mobile_data_key else R.string.quality_pref_key), currentPrefQuality ) // useSystemBrightness = diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 33d41934..e10a5a1a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -113,6 +113,30 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + getPref(R.string.quality_pref_mobile_data_key)?.setOnPreferenceClickListener { + val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() + prefValues.remove(Qualities.Unknown.value) + + val prefNames = prefValues.map { Qualities.getStringByInt(it) } + + val currentQuality = + settingsManager.getInt( + getString(R.string.quality_pref_mobile_data_key), + Qualities.values().last().value + ) + + activity?.showBottomDialog( + prefNames.toList(), + prefValues.indexOf(currentQuality), + getString(R.string.watch_quality_pref_data), + true, + {}) { + settingsManager.edit().putInt(getString(R.string.quality_pref_mobile_data_key), prefValues[it]) + .apply() + } + return@setOnPreferenceClickListener true + } + getPref(R.string.player_pref_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.player_pref_names) val prefValues = resources.getIntArray(R.array.player_pref_values) 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 205f0a6b..a76b62fd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -776,8 +776,13 @@ object AppUtils { return networkInfo.any { conManager.getNetworkCapabilities(it) ?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true + } && + !networkInfo.any { + conManager.getNetworkCapabilities(it) + ?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true + } } - } + private fun Activity?.cacheClass(clazz: String?) { clazz?.let { c -> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2d46a70d..49380b5e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,6 +16,7 @@ test_providers_key subtitle_settings_chromecast_key quality_pref_key + quality_pref_mobile_data_key player_pref_key prefer_limit_title_key prefer_limit_title_rez_key @@ -364,7 +365,8 @@ Don\'t show again Skip this Update Update - Preferred watch quality + Preferred watch quality (WiFi) + Preferred watch quality (Mobile Data) Video player title max chars Video player resolution Video buffer size @@ -655,4 +657,4 @@ Subscribed to %s Unsubscribed from %s Episode %d released! - \ No newline at end of file + diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index 0e5bd84f..2d2905ea 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -15,6 +15,10 @@ android:icon="@drawable/ic_baseline_hd_24" android:key="@string/quality_pref_key" android:title="@string/watch_quality_pref" /> + Date: Sat, 25 Feb 2023 21:18:48 +0000 Subject: [PATCH 030/570] Added some extractors mirrors and added Vido Extractor (#393) * Added some mirrors and fixed some extractors * Fixed Vido extractor (for MesFilms and Wiflix) --- .../cloudstream3/extractors/DoodExtractor.kt | 3 ++ .../cloudstream3/extractors/Evolaod.kt | 25 ++------------ .../cloudstream3/extractors/Filesim.kt | 5 +++ .../cloudstream3/extractors/StreamSB.kt | 4 +++ .../cloudstream3/extractors/Uqload.kt | 26 ++++---------- .../lagradost/cloudstream3/extractors/Vido.kt | 34 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 5 +++ 7 files changed, 61 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Vido.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt index 7ec1fb22..0d94eb08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt @@ -38,6 +38,9 @@ class DoodWsExtractor : DoodLaExtractor() { override var mainUrl = "https://dood.ws" } +class DoodYtExtractor : DoodLaExtractor() { + override var mainUrl = "https://dood.yt" +} open class DoodLaExtractor : ExtractorApi() { override var name = "DoodStream" diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt index eddbf6df..3e38b446 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt @@ -16,26 +16,7 @@ open class Evoload : ExtractorApi() { override suspend fun getUrl(url: String, referer: String?): List { - val lang = url.substring(0, 2) - val flag = - if (lang == "vo") { - " \uD83C\uDDEC\uD83C\uDDE7" - } - else if (lang == "vf"){ - " \uD83C\uDDE8\uD83C\uDDF5" - } else { - "" - } - - val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http:// - url - } else { - url.substring(2, url.length) - } - //println(lang) - //println(cleaned_url) - - val id = cleaned_url.replace("https://evoload.io/e/", "") // wanted media id + val id = url.replace("https://evoload.io/e/", "") // wanted media id val csrv_token = app.get("https://csrv.evosrv.com/captcha?m412548=").text // whatever that is val captchaPass = app.get("https://cd2.evosrv.com/html/jsx/e.jsx").text.take(300).split("captcha_pass = '")[1].split("\'")[0] //extract the captcha pass from the js response (located in the 300 first chars) val payload = mapOf("code" to id, "csrv_token" to csrv_token, "pass" to captchaPass) @@ -44,9 +25,9 @@ open class Evoload : ExtractorApi() { return listOf( ExtractorLink( name, - name + flag, + name, link, - cleaned_url, + url, Qualities.Unknown.value, ) ) 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 bc910a7e..382ca756 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt @@ -13,6 +13,11 @@ class FileMoon : Filesim() { override val name = "FileMoon" } +class FileMoonSx : Filesim() { + override val mainUrl = "https://filemoon.sx" + override val name = "FileMoonSx" +} + open class Filesim : ExtractorApi() { override val name = "Filesim" override val mainUrl = "https://files.im" diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt index b77617c2..b7477242 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt @@ -77,6 +77,10 @@ class StreamSB10 : StreamSB() { override var mainUrl = "https://sbplay2.xyz" } +class StreamSB11 : StreamSB() { + override var mainUrl = "https://sbbrisk.com" +} + // This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt // The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE open class StreamSB : ExtractorApi() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt index 5109acc3..86bd9e0b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt @@ -7,6 +7,10 @@ class Uqload1 : Uqload() { override var mainUrl = "https://uqload.com" } +class Uqload2 : Uqload() { + override var mainUrl = "https://uqload.co" +} + open class Uqload : ExtractorApi() { override val name: String = "Uqload" override val mainUrl: String = "https://www.uqload.com" @@ -15,30 +19,14 @@ open class Uqload : ExtractorApi() { override suspend fun getUrl(url: String, referer: String?): List? { - val lang = url.substring(0, 2) - val flag = - if (lang == "vo") { - " \uD83C\uDDEC\uD83C\uDDE7" - } - else if (lang == "vf"){ - " \uD83C\uDDE8\uD83C\uDDF5" - } else { - "" - } - - val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http:// - url - } else { - url.substring(2, url.length) - } - with(app.get(cleaned_url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile" + with(app.get(url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile" srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link -> return listOf( ExtractorLink( name, - name + flag, + name, link, - cleaned_url, + url, Qualities.Unknown.value, ) ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vido.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vido.kt new file mode 100644 index 00000000..67e59281 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vido.kt @@ -0,0 +1,34 @@ +package com.lagradost.cloudstream3.extractors +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.getAndUnpack + +class Vido : ExtractorApi() { + override var name = "Vido" + override var mainUrl = "https://vido.lol" + private val srcRegex = Regex("""sources:\s*\["(.*?)"\]""") + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?): List? { + val methode = app.get(url.replace("/e/", "/embed-")) // fix wiflix and mesfilms + with(methode) { + if (!methode.isSuccessful) return null + //val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull() + srcRegex.find(this.text)?.groupValues?.get(1)?.let { link -> + return listOf( + ExtractorLink( + name, + name, + link, + url, + Qualities.Unknown.value, + true, + ) + ) + } + } + return null + } +} \ No newline at end of file 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 b0dba9ff..6540b8c4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -229,6 +229,7 @@ val extractorApis: MutableList = arrayListOf( StreamSB8(), StreamSB9(), StreamSB10(), + StreamSB11(), SBfull(), // Streamhub(), cause Streamhub2() works Streamhub2(), @@ -254,6 +255,7 @@ val extractorApis: MutableList = arrayListOf( // WatchSB(), 'cause StreamSB.kt works Uqload(), Uqload1(), + Uqload2(), Evoload(), Evoload1(), VoeExtractor(), @@ -277,6 +279,7 @@ val extractorApis: MutableList = arrayListOf( DoodShExtractor(), DoodWatchExtractor(), DoodWfExtractor(), + DoodYtExtractor(), AsianLoad(), @@ -324,6 +327,8 @@ val extractorApis: MutableList = arrayListOf( Filesim(), FileMoon(), + FileMoonSx(), + Vido(), Linkbox(), Acefile(), SpeedoStream(), From e5834d485b1447a3687891d698f776e7922289d8 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sat, 25 Feb 2023 20:37:54 +0100 Subject: [PATCH 031/570] Translated using Weblate (German) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Czech) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Polish) Currently translated at 100.0% (608 of 608 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (608 of 608 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (608 of 608 strings) Translated using Weblate (Portuguese) Currently translated at 81.0% (493 of 608 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (608 of 608 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 99.5% (605 of 608 strings) Translated using Weblate (Croatian) Currently translated at 100.0% (608 of 608 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (608 of 608 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (608 of 608 strings) Translated using Weblate (Italian) Currently translated at 100.0% (608 of 608 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (608 of 608 strings) Translated using Weblate (Polish) Currently translated at 97.3% (592 of 608 strings) Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Translated using Weblate (Japanese) Currently translated at 44.5% (268 of 602 strings) Translated using Weblate (qt (generated) (qt)) Currently translated at 50.4% (304 of 602 strings) Translated using Weblate (Slovak) Currently translated at 31.7% (191 of 602 strings) Translated using Weblate (Portuguese) Currently translated at 76.9% (463 of 602 strings) Translated using Weblate (Somali) Currently translated at 94.3% (568 of 602 strings) Translated using Weblate (Somali) Currently translated at 94.3% (568 of 602 strings) Translated using Weblate (Norwegian Nynorsk) Currently translated at 44.5% (268 of 602 strings) Translated using Weblate (Norwegian Nynorsk) Currently translated at 44.5% (268 of 602 strings) Translated using Weblate (Norwegian Nynorsk) Currently translated at 44.5% (268 of 602 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (602 of 602 strings) Translated using Weblate (Esperanto) Currently translated at 27.5% (166 of 602 strings) Translated using Weblate (Esperanto) Currently translated at 27.5% (166 of 602 strings) Translated using Weblate (Persian) Currently translated at 20.0% (121 of 602 strings) Translated using Weblate (Hungarian) Currently translated at 55.6% (335 of 602 strings) Translated using Weblate (German) Currently translated at 99.1% (597 of 602 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (602 of 602 strings) Translated using Weblate (Russian) Currently translated at 99.1% (597 of 602 strings) Translated using Weblate (Kannada) Currently translated at 35.2% (212 of 602 strings) Translated using Weblate (Urdu) Currently translated at 72.2% (435 of 602 strings) Translated using Weblate (Tamil) Currently translated at 18.2% (110 of 602 strings) Translated using Weblate (Tamil) Currently translated at 18.2% (110 of 602 strings) Translated using Weblate (Hebrew) Currently translated at 97.1% (585 of 602 strings) Translated using Weblate (Bengali) Currently translated at 38.7% (233 of 602 strings) Translated using Weblate (Bengali) Currently translated at 38.7% (233 of 602 strings) Translated using Weblate (Bengali) Currently translated at 38.7% (233 of 602 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (602 of 602 strings) Translated using Weblate (Chinese (Traditional)) Currently translated at 94.1% (567 of 602 strings) Translated using Weblate (Vietnamese) Currently translated at 96.8% (583 of 602 strings) Translated using Weblate (Turkish) Currently translated at 97.1% (585 of 602 strings) Translated using Weblate (Tagalog) Currently translated at 56.1% (338 of 602 strings) Translated using Weblate (Tagalog) Currently translated at 56.1% (338 of 602 strings) Translated using Weblate (Tagalog) Currently translated at 56.1% (338 of 602 strings) Translated using Weblate (Swedish) Currently translated at 74.9% (451 of 602 strings) Translated using Weblate (Swedish) Currently translated at 74.9% (451 of 602 strings) Translated using Weblate (Swedish) Currently translated at 74.9% (451 of 602 strings) Translated using Weblate (Romanian) Currently translated at 73.0% (440 of 602 strings) Translated using Weblate (Romanian) Currently translated at 73.0% (440 of 602 strings) Translated using Weblate (Romanian) Currently translated at 73.0% (440 of 602 strings) Translated using Weblate (Polish) Currently translated at 98.0% (590 of 602 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 88.3% (532 of 602 strings) Translated using Weblate (Dutch) Currently translated at 75.0% (452 of 602 strings) Translated using Weblate (Dutch) Currently translated at 75.0% (452 of 602 strings) Translated using Weblate (Dutch) Currently translated at 75.0% (452 of 602 strings) Translated using Weblate (Malayalam) Currently translated at 37.2% (224 of 602 strings) Translated using Weblate (Malayalam) Currently translated at 37.2% (224 of 602 strings) Translated using Weblate (Malayalam) Currently translated at 37.2% (224 of 602 strings) Translated using Weblate (Macedonian) Currently translated at 48.6% (293 of 602 strings) Translated using Weblate (Macedonian) Currently translated at 48.6% (293 of 602 strings) Translated using Weblate (Macedonian) Currently translated at 48.6% (293 of 602 strings) Translated using Weblate (Italian) Currently translated at 99.1% (597 of 602 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (602 of 602 strings) Translated using Weblate (Croatian) Currently translated at 100.0% (602 of 602 strings) Translated using Weblate (Hindi) Currently translated at 37.7% (227 of 602 strings) Translated using Weblate (Hindi) Currently translated at 37.7% (227 of 602 strings) Translated using Weblate (Hindi) Currently translated at 37.7% (227 of 602 strings) Translated using Weblate (French) Currently translated at 97.3% (586 of 602 strings) Translated using Weblate (Greek) Currently translated at 97.0% (584 of 602 strings) Translated using Weblate (Czech) Currently translated at 100.0% (602 of 602 strings) Translated using Weblate (bp (generated) (bp)) Currently translated at 77.4% (466 of 602 strings) Translated using Weblate (bp (generated) (bp)) Currently translated at 77.4% (466 of 602 strings) Translated using Weblate (bp (generated) (bp)) Currently translated at 77.4% (466 of 602 strings) Translated using Weblate (Bulgarian) Currently translated at 94.5% (569 of 602 strings) Translated using Weblate (Bulgarian) Currently translated at 94.5% (569 of 602 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (602 of 602 strings) Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Translated using Weblate (Japanese) Currently translated at 44.5% (268 of 602 strings) Translated using Weblate (qt (generated) (qt)) Currently translated at 50.4% (304 of 602 strings) Translated using Weblate (Slovak) Currently translated at 31.7% (191 of 602 strings) Translated using Weblate (Portuguese) Currently translated at 76.9% (463 of 602 strings) Translated using Weblate (Somali) Currently translated at 94.3% (568 of 602 strings) Translated using Weblate (Somali) Currently translated at 94.3% (568 of 602 strings) Translated using Weblate (Norwegian Nynorsk) Currently translated at 44.5% (268 of 602 strings) Translated using Weblate (Norwegian Nynorsk) Currently translated at 44.5% (268 of 602 strings) Translated using Weblate (Norwegian Nynorsk) Currently translated at 44.5% (268 of 602 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (602 of 602 strings) Translated using Weblate (Esperanto) Currently translated at 27.5% (166 of 602 strings) Translated using Weblate (Esperanto) Currently translated at 27.5% (166 of 602 strings) Translated using Weblate (Persian) Currently translated at 20.0% (121 of 602 strings) Translated using Weblate (Hungarian) Currently translated at 55.6% (335 of 602 strings) Translated using Weblate (German) Currently translated at 99.1% (597 of 602 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (602 of 602 strings) Translated using Weblate (Russian) Currently translated at 99.1% (597 of 602 strings) Translated using Weblate (Kannada) Currently translated at 35.2% (212 of 602 strings) Translated using Weblate (Kannada) Currently translated at 35.2% (212 of 602 strings) Translated using Weblate (Urdu) Currently translated at 72.2% (435 of 602 strings) Translated using Weblate (Tamil) Currently translated at 18.2% (110 of 602 strings) Translated using Weblate (Tamil) Currently translated at 18.2% (110 of 602 strings) Translated using Weblate (Hebrew) Currently translated at 97.1% (585 of 602 strings) Translated using Weblate (Bengali) Currently translated at 38.7% (233 of 602 strings) Translated using Weblate (Bengali) Currently translated at 38.7% (233 of 602 strings) Translated using Weblate (Bengali) Currently translated at 38.7% (233 of 602 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (602 of 602 strings) Translated using Weblate (Chinese (Traditional)) Currently translated at 94.1% (567 of 602 strings) Translated using Weblate (Vietnamese) Currently translated at 96.8% (583 of 602 strings) Translated using Weblate (Turkish) Currently translated at 97.1% (585 of 602 strings) Translated using Weblate (Tagalog) Currently translated at 56.1% (338 of 602 strings) Translated using Weblate (Tagalog) Currently translated at 56.1% (338 of 602 strings) Translated using Weblate (Tagalog) Currently translated at 56.1% (338 of 602 strings) Translated using Weblate (Swedish) Currently translated at 74.9% (451 of 602 strings) Translated using Weblate (Swedish) Currently translated at 74.9% (451 of 602 strings) Translated using Weblate (Swedish) Currently translated at 74.9% (451 of 602 strings) Translated using Weblate (Romanian) Currently translated at 73.0% (440 of 602 strings) Translated using Weblate (Romanian) Currently translated at 73.0% (440 of 602 strings) Translated using Weblate (Romanian) Currently translated at 73.0% (440 of 602 strings) Translated using Weblate (Polish) Currently translated at 98.0% (590 of 602 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 88.3% (532 of 602 strings) Translated using Weblate (Dutch) Currently translated at 75.0% (452 of 602 strings) Translated using Weblate (Dutch) Currently translated at 75.0% (452 of 602 strings) Translated using Weblate (Dutch) Currently translated at 75.0% (452 of 602 strings) Translated using Weblate (Malayalam) Currently translated at 37.2% (224 of 602 strings) Translated using Weblate (Malayalam) Currently translated at 37.2% (224 of 602 strings) Translated using Weblate (Malayalam) Currently translated at 37.2% (224 of 602 strings) Translated using Weblate (Macedonian) Currently translated at 48.6% (293 of 602 strings) Translated using Weblate (Macedonian) Currently translated at 48.6% (293 of 602 strings) Translated using Weblate (Macedonian) Currently translated at 48.6% (293 of 602 strings) Translated using Weblate (Italian) Currently translated at 99.1% (597 of 602 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (602 of 602 strings) Translated using Weblate (Croatian) Currently translated at 100.0% (602 of 602 strings) Translated using Weblate (Hindi) Currently translated at 37.7% (227 of 602 strings) Translated using Weblate (Hindi) Currently translated at 37.7% (227 of 602 strings) Translated using Weblate (Hindi) Currently translated at 37.7% (227 of 602 strings) Translated using Weblate (French) Currently translated at 97.3% (586 of 602 strings) Translated using Weblate (Greek) Currently translated at 97.0% (584 of 602 strings) Translated using Weblate (Czech) Currently translated at 100.0% (602 of 602 strings) Translated using Weblate (bp (generated) (bp)) Currently translated at 77.4% (466 of 602 strings) Translated using Weblate (bp (generated) (bp)) Currently translated at 77.4% (466 of 602 strings) Translated using Weblate (bp (generated) (bp)) Currently translated at 77.4% (466 of 602 strings) Translated using Weblate (Bulgarian) Currently translated at 94.5% (569 of 602 strings) Translated using Weblate (Bulgarian) Currently translated at 94.5% (569 of 602 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (602 of 602 strings) Co-authored-by: Aitor Salaberria Co-authored-by: Allan Nordhøy Co-authored-by: Anarchydr Co-authored-by: Anonymous Co-authored-by: Cliff Heraldo <123844876+clxf12@users.noreply.github.com> Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> Co-authored-by: Dan Co-authored-by: Fjuro Co-authored-by: Geovani Amaral Co-authored-by: Hosted Weblate Co-authored-by: Julian Co-authored-by: MedRAM Co-authored-by: Prathap Rathod Co-authored-by: Rex_sa Co-authored-by: Sandyran Co-authored-by: Sdarfeesh Co-authored-by: Translator-3000 Co-authored-by: Walter H Co-authored-by: gallegonovato Co-authored-by: gnu-ewm Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bg/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bp/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/el/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/eo/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fa/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/he/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hi/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/kn/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ml/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nn/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/qt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ro/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/so/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sv/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ur/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hant/ Translation: Cloudstream/App --- app/src/main/res/values-ar/strings.xml | 8 ++++++- app/src/main/res/values-cs/strings.xml | 8 ++++++- app/src/main/res/values-de/strings.xml | 13 ++++++++++- app/src/main/res/values-es/strings.xml | 8 ++++++- app/src/main/res/values-hr/strings.xml | 5 +++++ app/src/main/res/values-in/strings.xml | 5 +++++ app/src/main/res/values-it/strings.xml | 10 +++++++++ app/src/main/res/values-kn/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 17 ++++++++++++++ app/src/main/res/values-pt/strings.xml | 31 ++++++++++++++++++++++++-- app/src/main/res/values-qt/strings.xml | 23 ++++++++++--------- app/src/main/res/values-uk/strings.xml | 9 +++++++- app/src/main/res/values-zh/strings.xml | 5 +++++ 13 files changed, 125 insertions(+), 19 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 1e9bcfcc..cfd761e3 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -259,7 +259,7 @@ لا تظهر مرة أخرى تخطي هذا التحديث تحديث - جودة المشاهدة المفضلة + جودة المشاهدة المفضلة (WiFi) أقصى عدد لحروف عنوان مُشغل الفيديو أبعاد مُشغل الفيديو حجم ذاكرة التخزين المؤقت للفيديو @@ -555,4 +555,10 @@ تم إصدار الحلقة %d! مشترك مشترك في %s + تجاوز مزود خدمة الإنترنت + استرجاع + فشل الوصول إلى GitHub ، وتمكين وكيل jsdelivr. + تجاوز حظر GitHub باستخدام jsdelivr ، قد يتسبب في تأخير التحديثات لبضعة أيام. + وكيل raw.githubusercontent.com + جودة المشاهدة المفضلة (بيانات الجوال) \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 966cd7d9..e99e1010 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -245,7 +245,7 @@ Již nezobrazovat Přeskočit tuto aktualizace Aktualizovat - Upřednostněná kvalita sledování + Upřednostněná kvalita sledování (WiFi) Maximální počet znaků v názvu přehrávače Rozlišení přehrávače Velikost vyrovnávací paměti videa @@ -547,4 +547,10 @@ Odhlášen odběr od %s Byla vydána epizoda %d! Odebíráno + Proxy raw.githubusercontent.com + Nepodařilo se připojit ke GitHubu, povolování proxy jsdelivr. + Upřednostněná kvalita sledování (mobilní data) + Vrátit zpět + Obchází blokování GitHubu pomocí jsdelivr, může způsobit zpoždění aktualizací o několik dní. + Obcházení ISP \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f6583c20..c5e74a60 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -263,7 +263,7 @@ Nicht mehr anzeigen Update ignorieren Update - Bevorzugte Auflösung + Bevorzugte Videoqualität (WLAN) Videoplayertitel max. Zeichen Videoplayer Auflösung Videopuffergröße @@ -518,4 +518,15 @@ Log Start Neustarten + Bevorzugte Videoqualität (mobile Daten) + Umgehung der GitHub Sperre mit jsdelivr, kann zu einigen Tagen Verzögerung bei Updates führen. + %s abonniert + %s deabonniert + Episode %d erschienen! + raw.githubusercontent.com Proxy + GitHub kann nicht erreicht werden, der jsdelivr-Proxy wird aktiviert. + Aktualisierung abonnierter Sendungen + Rückgängig + Abonniert + ISP-Umgehungen \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2040169b..18647ef8 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -24,7 +24,7 @@ Ocultar la calidad de video en los resultados de búsqueda Diseño Diseño - Calidad de visualización preferida + Calidad de visualización preferida (WiFi) Reproductor de video preferido Diseño para emulador Diseño de la aplicación @@ -523,4 +523,10 @@ Darse de baja de %s Actualizando los programas suscritos ¡Episodio %d publicado! + Proxy raw.githubusercontent.com + No se ha podido acceder a GitHub, activando el proxy jsdelivr. + Evita el bloqueo de GitHub usando jsdelivr, puede causar que las actualizaciones se retrasen unos días. + Revertir + ISP Bypasses + Calidad de visualización preferida (Datos móviles) \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 926c7f57..b623ec5d 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -548,4 +548,9 @@ Pretplaćeno Pretplaćen na %s Otkazana pretplata sa %s + Vraćanje + ISP zaobilaznice + raw.githubusercontent.com Proxy + Neuspješno dohvaćanje GitHuba, omogućavanje jsdelivr proxyja. + Zaobilazi blokiranje GitHuba pomoću jsdelivr, može uzrokovati odgode ažuriranja za nekoliko dana. \ 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 46d61e44..84179352 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -546,4 +546,9 @@ Berlangganan ke %s Berhenti berlangganan di %s Episode %d telah rilis! + raw.githubusercontent.com Proksi + Gagal mencapai GitHub, mengaktifkan proksi jsdelivr. + Bypass pemblokiran Github menggunakan JSDeliVR, dapat menyebabkan pembaruan tertunda beberapa hari. + Bypass ISP + Pulihkan \ 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 89f6b4ee..d6bdc204 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -540,4 +540,14 @@ Ferma Superato Fallito + Proxy raw.githubusercontent.com + Disiscritto da %s + Iscritto + Iscritto a %s + Impossibile contattare GitHub, abilitazione proxy jsdelivr avviata. + Bypassa il blocco di GitHub utilizzando jsdelivr, potrebbe causare un ritardo di alcuni giorni. + Baypass ISP + Ripristina + Aggiornando shows a cui sei iscritto + L\'episodio %d è stato rilasciato! \ 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 c36459b7..242653be 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -85,7 +85,7 @@ ಶೇರ್ ಫೈಲ್ ಅಳಿಸಿ ಹೆಚ್ಚಿನ ಮಾಹಿತಿ - ಹೊಸ ಅಪ್ಡೇಟ್ ಬಂದಿದೆ + ಹೊಸ ಅಪ್ಡೇಟ್ ಬಂದಿದೆ \n%s-%s ಲೋಡಿಂಗ್… ಡೌನ್‌ಲೋಡ್ ಭಾಷೆಗಳನ್ನು ಮಾಡಿ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 411f0b45..bbaaec57 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -514,4 +514,21 @@ Android TV Pokazany odtwarzacz — ilość przewijania Używana ilość przewijania, gdy ukryty jest odtwarzacz + Dziennik + Uruchom ponownie + Rozpocznij + Nie powiodło się + Ukończone powodzeniem + Serwer pośredniczący raw.githubusercontent.com + Obejścia ISP + Test dostawcy + Zatrzymaj + Przywróć + Aktualizowanie subskrybowanych programów + Zasubskrybowano + Zasubskrybowano %s + Anulowano subskrypcję %s + Został wydany odcinek %d! + Obchodzi blokadę GitHuba za pomocą jsdelivr, może spowodować opóźnienie aktualizacji o kilka dni. + Nie udało się połączyć z GitHub, włączono serwer pośredniczący jsdelivr. \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 9353664e..3754de8b 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -6,12 +6,12 @@ Episódio %d será lançado em Poster Capa do Episódio - \@string/result_poster_img_des + Poster Capa Principal Próximo Aleatório Voltar Trocar Provedor - %dd %dh %dm + %d dia(s), %d hora(s) e %d mese(s) Fonte Resolução Extras @@ -381,4 +381,31 @@ Todas as legendas em maiúsculas Transferir todos os plugins deste repositório\? %s (Desativado) + Instalador APK + %d minuto(s) + Reproduzir trailer + Marcar como visto/não visto + Reproduzir + Instalar automaticamente todas as extensões dos repositórios cadastrados. + Baixar extensões automaticamente + Refazer o processo de configuração + -30 + Vídeo + +30 + %s %d%s + Elenco: %s + Atualização em andamento + Log + Alguns aparelhos não possuem suporte para o novo instalador de pacotes. Use a opção legado caso não esteja conseguindo atualizar. + %d-%d + %d %s + Iniciar + Falha + Sucesso + Biblioteca + Navegar + Aplicativo de Anime pelos mesmos desenvolvedores + Ova + Anime + Player visível - Procurar valor \ No newline at end of file diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index b36f3b16..c1119bfc 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -172,30 +172,31 @@ oouuh haa oohahaha hahha ooooohaha oohahaha hahha ooooohaha haaoou - u ahhu uuuh hau uaohuau + u ahhu uuuh hau uaohuau aahuuouhh ouh hhhah hhaohuhha a auoo ohauh - uhaauauau ahuuouaha + uhaauauau ahuuouaha auuuha h a ahuhaaaa - uaoh uhu uahaaaaoo - uauhah u aao u oah - h u ahahh aoou ha + uaoh uhu uahaaaaoo + uauhah u aao u oah + h u ahahh aoou ha haoooo aaoou uou ah oahuouooaouoa ouuhh o ouou uhauuuoaah h ou aouhouo aaooao hh - hhauhohhuu au aaohu - uhuoh o a ohahuhohoa hah + hhauhohhuu au aaohu + uhuoh o a ohahuhohoa hah ua hu ouo o aoau hah ah - ah huu oouhhau aoaoaaohoo ha - a ahu uoo uoahuo uo + ah huu oouhhau aoaoaaohoo ha + a ahu uoo uoahuo uo uo u ohouao uuoouhh hhuhuuh ouhoaao hau aouo - uha uh huo uooaah u + uha uh huo uooaah u 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 diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 5330d3ec..a676b583 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -217,7 +217,7 @@ Пропустити OP Не показувати знову Оновити - Бажана якість перегляду + Бажана якість перегляду (WiFi) Заголовок Перемикання елементів інтерфейсу на плакаті Оновлення не знайдено @@ -523,4 +523,11 @@ Підписано на %s Відписатися від %s Епізод %d випущено! + Повернути + raw.githubusercontent.com +\nProxy + Не вдалося зв\'язатися з GitHub, увімкнувши проксі-сервер jsdelivr. + Обходи ISP + Обходити блокування GitHub з використанням jsdlitr, може викликати затримку оновлень на кілька днів. + Бажана якість перегляду (Мобільні дані) \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 9e2d6137..626cc0fe 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -571,4 +571,9 @@ 第 %d 集已发布! 成功 日志 + raw.githubusercontent.com 代理 + 连接 Github 失败,正在启用 jsdelivr 代理。 + 使用 jsdelivr 绕过对 Github 的封锁,可能导致更新延迟几天。 + ISP 绕过 + 还原 \ No newline at end of file From d6df24eff2d425fc79e52b94a1fe600f857ec19a Mon Sep 17 00:00:00 2001 From: Stormunblessed <86633626+Stormunblessed@users.noreply.github.com> Date: Mon, 27 Feb 2023 14:05:42 -0600 Subject: [PATCH 032/570] Fixes on filesim and added filemoon, ztreamhub (#397) * fix fastream, tomatomatela, and added okrulink * forgot this * sendvid extractor * sendvid extractor * fixes * Filesim fix, added filemoon and ztreamhub --- .../cloudstream3/extractors/Filesim.kt | 45 +++++++++---------- .../cloudstream3/utils/ExtractorApi.kt | 1 + 2 files changed, 21 insertions(+), 25 deletions(-) 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 382ca756..84fd0552 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt @@ -1,13 +1,15 @@ 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 +import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 + +class Ztreamhub : Filesim() { + override val mainUrl: String = "https://ztreamhub.com" //Here 'cause works + override val name = "Zstreamhub" +} class FileMoon : Filesim() { override val mainUrl = "https://filemoon.to" override val name = "FileMoon" @@ -29,34 +31,27 @@ open class Filesim : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - with(app.get(url).document) { - this.select("script").forEach { script -> - if (script.data().contains("eval(function(p,a,c,k,e,d)")) { - 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") - ) - ) - } + val response = app.get(url, referer = mainUrl).document + response.select("script[type=text/javascript]").map { script -> + if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) { + val unpackedscript = getAndUnpack(script.data()) + val m3u8Regex = Regex("file.\\\"(.*?m3u8.*?)\\\"") + val m3u8 = m3u8Regex.find(unpackedscript)?.destructured?.component1() ?: "" + if (m3u8.isNotEmpty()) { + generateM3u8( + name, + m3u8, + mainUrl + ).forEach(callback) } } } } - private data class ResponseSource( + /* private data class ResponseSource( @JsonProperty("file") val file: String, @JsonProperty("type") val type: String?, @JsonProperty("label") val label: String? - ) + ) */ } \ No newline at end of file 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 6540b8c4..0bced6b2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -370,6 +370,7 @@ val extractorApis: MutableList = arrayListOf( Cda(), Dailymotion(), ByteShare(), + Ztreamhub() ) From ab324b93e89b2e955436d7ec2099b3256cb0d005 Mon Sep 17 00:00:00 2001 From: no-commit <> Date: Tue, 28 Feb 2023 01:19:59 +0100 Subject: [PATCH 033/570] Small fixes to Intents and Subscriptions --- .../lagradost/cloudstream3/MainActivity.kt | 6 ++++- .../services/SubscriptionWorkManager.kt | 22 ++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index e626dcd6..a7449255 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -319,7 +319,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } else if (safeURI(str)?.scheme == appStringSearch) { nextSearchQuery = URLDecoder.decode(str.substringAfter("$appStringSearch://"), "UTF-8") - nav_view.selectedItemId = R.id.navigation_search + + // Use both navigation views to support both layouts. + // It might be better to use the QuickSearch. + nav_view?.selectedItemId = R.id.navigation_search + nav_rail_view?.selectedItemId = R.id.navigation_search } else if (safeURI(str)?.scheme == appStringResumeWatching) { val id = str.substringAfter("$appStringResumeWatching://").toIntOrNull() diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt index d1b1b660..adf5abfa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt @@ -105,14 +105,12 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete SUBSCRIPTION_CHANNEL_DESCRIPTION ) - safeApiCall { - setForeground( - ForegroundInfo( - SUBSCRIPTION_NOTIFICATION_ID, - progressNotificationBuilder.build() - ) + setForeground( + ForegroundInfo( + SUBSCRIPTION_NOTIFICATION_ID, + progressNotificationBuilder.build() ) - } + ) val subscriptions = getAllSubscriptions() @@ -196,7 +194,15 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete PendingIntent.getActivity(context, 0, intent, 0) } - val poster = ioWork { savedData.posterUrl?.let { url -> context.getImageBitmapFromUrl(url, savedData.posterHeaders) } } + val poster = ioWork { + savedData.posterUrl?.let { url -> + context.getImageBitmapFromUrl( + url, + savedData.posterHeaders + ) + } + } + val updateNotification = updateNotificationBuilder.setContentTitle(updateHeader) .setContentText(updateDescription) From f0515c4dc9e38bcf80f9766e7dff9f9ffd802f72 Mon Sep 17 00:00:00 2001 From: Stormunblessed <86633626+Stormunblessed@users.noreply.github.com> Date: Fri, 3 Mar 2023 09:24:02 +0000 Subject: [PATCH 034/570] Support qualities for Dailymotion (#407) * Dailymotion qualities --- .../cloudstream3/extractors/Dailymotion.kt | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt index 125e4bcf..4b7cb19f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt @@ -6,6 +6,7 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import com.lagradost.cloudstream3.utils.Qualities import java.net.URL @@ -42,18 +43,9 @@ open class Dailymotion : ExtractorApi() { ) val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = cookies) .parsedSafe() ?: return - metaData.qualities.forEach { (key, video) -> + metaData.qualities.forEach { (_, video) -> video.forEach { - callback.invoke( - ExtractorLink( - name, - "$name $key", - it.url, - "", - Qualities.Unknown.value, - true - ) - ) + getStream(it.url, this.name, callback) } } } @@ -75,6 +67,17 @@ open class Dailymotion : ExtractorApi() { return null } + private suspend fun getStream( + streamLink: String, + name: String, + callback: (ExtractorLink) -> Unit + ) { + return generateM3u8( + name, + streamLink, + "", + ).forEach(callback) + } data class Config( val context: Context, val dmInternalData: InternalData From 76545f55c3efeb66e14c6a4c62d2c980adf51830 Mon Sep 17 00:00:00 2001 From: no-commit <> Date: Fri, 3 Mar 2023 17:45:26 +0100 Subject: [PATCH 035/570] Standardized some home screen padding and made subtitle delay persistent. Fixes #405 --- .../ui/player/FullScreenPlayer.kt | 10 +++++++ .../cloudstream3/ui/player/GeneratorPlayer.kt | 29 +++++++++---------- .../main/res/layout/fragment_home_head.xml | 28 +++++++----------- app/src/main/res/layout/homepage_parent.xml | 2 -- 4 files changed, 34 insertions(+), 35 deletions(-) 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 d1b2814d..86e21fd6 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 @@ -84,6 +84,7 @@ const val HORIZONTAL_MULTIPLIER = 2.0f const val DOUBLE_TAB_MAXIMUM_HOLD_TIME = 200L const val DOUBLE_TAB_MINIMUM_TIME_BETWEEN = 200L // this also affects the UI show response time const val DOUBLE_TAB_PAUSE_PERCENTAGE = 0.15 // in both directions +private const val SUBTITLE_DELAY_BUNDLE_KEY = "subtitle_delay" // All the UI Logic for the player open class FullScreenPlayer : AbstractPlayerFragment() { @@ -1120,11 +1121,20 @@ open class FullScreenPlayer : AbstractPlayerFragment() { resetRewindText() } + override fun onSaveInstanceState(outState: Bundle) { + // As this is video specific it is better to not do any setKey/getKey + outState.putLong(SUBTITLE_DELAY_BUNDLE_KEY, subtitleDelay) + super.onSaveInstanceState(outState) + } + @SuppressLint("ClickableViewAccessibility") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // init variables setPlayBackSpeed(getKey(PLAYBACK_SPEED_KEY) ?: 1.0f) + savedInstanceState?.getLong(SUBTITLE_DELAY_BUNDLE_KEY)?.let { + subtitleDelay = it + } // handle tv controls playerEventListener = { eventType -> 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 67f58195..46f2bca9 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 @@ -13,7 +13,6 @@ import android.view.View import android.view.ViewGroup import android.widget.* import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AlertDialog import androidx.core.animation.addListener import androidx.core.content.ContextCompat import androidx.core.view.isGone @@ -734,19 +733,17 @@ class GeneratorPlayer : FullScreenPlayer() { } val currentAudioTracks = tracks.allAudioTracks - val trackBuilder = AlertDialog.Builder(ctx, R.style.AlertDialogCustomBlack) - .setView(R.layout.player_select_tracks) - - val tracksDialog = trackBuilder.create() + val trackDialog = Dialog(ctx, R.style.AlertDialogCustomBlack) + trackDialog.setContentView(R.layout.player_select_tracks) + trackDialog.show() // selectTracksDialog = tracksDialog - tracksDialog.show() - val videosList = tracksDialog.video_tracks_list - val audioList = tracksDialog.auto_tracks_list + val videosList = trackDialog.video_tracks_list + val audioList = trackDialog.auto_tracks_list - tracksDialog.video_tracks_holder.isVisible = currentVideoTracks.size > 1 - tracksDialog.audio_tracks_holder.isVisible = currentAudioTracks.size > 1 + trackDialog.video_tracks_holder.isVisible = currentVideoTracks.size > 1 + trackDialog.audio_tracks_holder.isVisible = currentAudioTracks.size > 1 fun dismiss() { if (isPlaying) { @@ -781,7 +778,7 @@ class GeneratorPlayer : FullScreenPlayer() { videosList.setItemChecked(which, true) } - tracksDialog.setOnDismissListener { + trackDialog.setOnDismissListener { dismiss() // selectTracksDialog = null } @@ -811,11 +808,11 @@ class GeneratorPlayer : FullScreenPlayer() { audioList.setItemChecked(which, true) } - tracksDialog.cancel_btt?.setOnClickListener { - tracksDialog.dismissSafe(activity) + trackDialog.cancel_btt?.setOnClickListener { + trackDialog.dismissSafe(activity) } - tracksDialog.apply_btt?.setOnClickListener { + trackDialog.apply_btt?.setOnClickListener { val currentTrack = currentAudioTracks.getOrNull(audioIndexStart) player.setPreferredAudioTrack( currentTrack?.language, currentTrack?.id @@ -828,7 +825,7 @@ class GeneratorPlayer : FullScreenPlayer() { player.setMaxVideoSize(width, height, currentVideo?.id) } - tracksDialog.dismissSafe(activity) + trackDialog.dismissSafe(activity) } } } catch (e: Exception) { @@ -1145,7 +1142,7 @@ class GeneratorPlayer : FullScreenPlayer() { val source = currentSelectedLink?.first?.name ?: currentSelectedLink?.second?.name ?: "NULL" - val title = when (titleRez) { + val title = when (titleRez) { 0 -> "" 1 -> extra 2 -> source diff --git a/app/src/main/res/layout/fragment_home_head.xml b/app/src/main/res/layout/fragment_home_head.xml index 0ee50042..603621f7 100644 --- a/app/src/main/res/layout/fragment_home_head.xml +++ b/app/src/main/res/layout/fragment_home_head.xml @@ -1,7 +1,6 @@ - @@ -148,17 +145,16 @@ + app:drawableTint="?attr/white" /> @@ -184,9 +180,9 @@ + android:layout_height="wrap_content" + android:background="?android:attr/selectableItemBackground"> + app:drawableTint="?attr/white" /> diff --git a/app/src/main/res/layout/homepage_parent.xml b/app/src/main/res/layout/homepage_parent.xml index b2f3e0a7..9c5dc84d 100644 --- a/app/src/main/res/layout/homepage_parent.xml +++ b/app/src/main/res/layout/homepage_parent.xml @@ -23,9 +23,7 @@ android:nextFocusUp="@id/home_child_more_info" android:paddingHorizontal="5dp" android:clipToPadding="false" - android:descendantFocusability="afterDescendants" - app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" android:id="@+id/home_child_recyclerview" android:orientation="horizontal" From 1eaa4620dc7ec00d34486d1e6ce6a7a1c38afe77 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 2 Mar 2023 05:38:39 +0100 Subject: [PATCH 036/570] Translated using Weblate (qt (generated) (qt)) Currently translated at 54.5% (333 of 610 strings) Translated using Weblate (Russian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Croatian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (French) Currently translated at 98.8% (603 of 610 strings) Translated using Weblate (Italian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (German) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Russian) Currently translated at 99.6% (608 of 610 strings) Translated using Weblate (Portuguese) Currently translated at 85.0% (519 of 610 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Vietnamese) Currently translated at 96.8% (591 of 610 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Polish) Currently translated at 100.0% (610 of 610 strings) Co-authored-by: Anarchydr Co-authored-by: Cliff Heraldo <123844876+clxf12@users.noreply.github.com> Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> Co-authored-by: Duc Nguyen Tien Co-authored-by: Felipe Nogueira Co-authored-by: Hosted Weblate Co-authored-by: Julian Co-authored-by: Massimo Pissarello Co-authored-by: Samuel Gadiel Co-authored-by: Sdarfeesh Co-authored-by: Walter H Co-authored-by: eightyy8 Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/qt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/ Translation: Cloudstream/App --- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 8 +- app/src/main/res/values-fr/strings.xml | 17 +- app/src/main/res/values-hr/strings.xml | 1 + app/src/main/res/values-in/strings.xml | 3 +- app/src/main/res/values-it/strings.xml | 3 +- app/src/main/res/values-pl/strings.xml | 3 +- app/src/main/res/values-pt/strings.xml | 217 +++++++++++++++++++------ app/src/main/res/values-qt/strings.xml | 29 ++++ app/src/main/res/values-ru/strings.xml | 13 +- app/src/main/res/values-vi/strings.xml | 23 ++- app/src/main/res/values-zh/strings.xml | 3 +- 12 files changed, 254 insertions(+), 68 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c5e74a60..7cf49de1 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -246,7 +246,7 @@ In Browser wiedergeben Link kopieren Auto-Download - Download-Mirror + Alternativer Download Links neu laden Untertitel herunterladen Qualitätsanzeige diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 18647ef8..0b195275 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -506,11 +506,11 @@ \nInicia sesión en una cuenta de biblioteca o añade series desde tu biblioteca local ¡Se encontró un archivo en modo seguro! \nNo cargar ninguna extensión al inicio hasta que se elimine el archivo. - Jugadora mostrada - buscar cantidad - Jugadora oculta - buscar cantidad + Reproductor visible - buscar cantidad + Reproductor oculto - buscar cantidad Android TV - La cantidad de búsqueda utilizada cuando la jugadora es visible - La cantidad de búsqueda utilizada cuando el jugador está oculto + Tiempo de búsqueda usado (en segundos) cuando el reproductor está visible + Tiempo de búsqueda usado (en segundos) cuando el reproductor está oculto Parar Falló Registro diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 18255b3b..f3d35c19 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -130,7 +130,7 @@ Nouvelle mise à jour trouvée ! \n%s -> %s Épisode spécial - Qualité de visionnage préférée + Qualité de visionnage préférée (WiFi) Taille de la mémoire cache Étendre Non-responsabilité @@ -508,4 +508,19 @@ Mis à jour (ancien vers nouveau) Fichier du mode sans échec trouvé ! \nAucune extension ne sera chargée au démarrage avant que le fichier ne soit enlevé. + Arrêter + Revenir à + Enregistrer + Qualité de visionnage préférée (données mobiles) + Abonné à %s + Démarrer + Test des fournisseurs + Réussi + Désabonné de %s + Redémarrer + Abonné + raw.githubusercontent.com Proxy + Contournements de FAI + L\'épisode %d est sorti ! + Échouer \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index b623ec5d..159542cc 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -553,4 +553,5 @@ raw.githubusercontent.com Proxy Neuspješno dohvaćanje GitHuba, omogućavanje jsdelivr proxyja. Zaobilazi blokiranje GitHuba pomoću jsdelivr, može uzrokovati odgode ažuriranja za nekoliko dana. + Preferirana kvaliteta gledanja (podatkovna mobilna mreža) \ 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 84179352..0e383562 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -243,7 +243,7 @@ Jangan tunjukkan lagi Skip Update ini Update - Kualitas tontonan yang lebih diinginkan + Kualitas tontonan yang lebih diinginkan (WIFI) Karakter maksimal judul pemutar video Resolusi pemutar video Ukuran buffer video @@ -551,4 +551,5 @@ Bypass pemblokiran Github menggunakan JSDeliVR, dapat menyebabkan pembaruan tertunda beberapa hari. Bypass ISP Pulihkan + Nonton dengan kualitas yang di inginkan (Data Seluler) \ 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 d6bdc204..b8e7eb20 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -265,7 +265,7 @@ Non mostrare di nuovo Salta questo aggiornamento Aggiorna - Risoluzione preferita + Qualità di visualizzazione preferita (WiFi) Limita i caratteri del titolo nel player Risoluzione video player Dimensione cache video @@ -550,4 +550,5 @@ Ripristina Aggiornando shows a cui sei iscritto L\'episodio %d è stato rilasciato! + Qualità di visualizzazione preferita (Dati mobili) \ 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 bbaaec57..558a46ed 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -254,7 +254,7 @@ Nie pokazuj ponownie Pomiń tę aktualizację Aktualizacja - Domyślna jakość + Domyślna jakość (WiFi) Maksymalna ilość znaków w tytule odtwarzacza Rozdzielczość odtwarzacza wideo Rozmiar bufora wideo @@ -531,4 +531,5 @@ Został wydany odcinek %d! Obchodzi blokadę GitHuba za pomocą jsdelivr, może spowodować opóźnienie aktualizacji o kilka dni. Nie udało się połączyć z GitHub, włączono serwer pośredniczący jsdelivr. + Domyślna jakość (dane mobilne) \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 3754de8b..0c846361 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -3,63 +3,63 @@ %s Ep %d %dh %dm %dm - Episódio %d será lançado em + O episódio %d será lançado em Poster - Capa do Episódio + Pôster do episódio Poster - Capa Principal + Pôster Principal Próximo Aleatório Voltar - Trocar Provedor - %d dia(s), %d hora(s) e %d mese(s) + Alterar Provedor + %dd %dh %dm Fonte Resolução Extras - Preview Background + Visualizar plano de fundo Velocidade (%.2fx) Classificado: %.1f Nova atualização encontrada! \n%s -> %s - Enchimento + Preenchimento CloudStream - Reproduzir com CloudStream + Assistir com o CloudStream Início - Pesquisa - Transferências - Opções + Pesquisar + Downloads + Configurações Procurar… - Procurar em %s… - Sem Dados - Mais Opções + Pesquisar %s… + Sem dados + Mais opções Próximo episódio - Géneros - Partilhar - Abrir no Navegador - Saltar Carga + Gêneros + Compartilhar + Abrir no navegador + Pular carregamento Carregando… Assistindo - Em Espera + Em espera Concluído - Abandonado - Planeio Assistir - Nenhuma - Assistindo de Novo - Reproduzir Filme - Reproduzir Livestream + Desistido + Pretendo assistir + Nenhum + Reassistindo + Reproduzir filme + Reproduzir transmissão ao vivo Transmitir Torrent Fontes Legendas - Voltar a tentar ligação… - Voltar Atrás - Reproduzir Episódio - Transferir - Transferido - A Transferir - Transferência em Pausa - Transferência Iniciada - Transferência Falhou - Transferência Cancelada - Transferência Completa + Tentar conexão novamente… + Voltar + Reproduzir episódio + Download + Baixado + Baixando + Download Pausado + Download Iniciado + Falha no Download + Download cancelado + Download concluído Stream Erro a Carregar Links Armazenamento Interno @@ -142,7 +142,7 @@ 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 + Permissão de armazenamento não encontrada, por favor tente novamente. Erro no backup de %s Procurar Contas @@ -250,15 +250,15 @@ Não mostrar de novo Saltar esta Atualização Atualizar - Qualidade Preferida - Máximo de caracteres do título de vídeos + Qualidade Preferida (WiFi) + Máximo de caracteres do título no player de video Resolução do player de 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 + Causará travamentos em dispositivos com pouca memória se definido muito alto , como uma Android TV. + Pode causar problemas em sistemas com pouco espaço de armazenamento se definido muito alto, como uma Android TV. DNS sobre HTTPS Útil para contornar bloqueios do fornecedor de internet Clonar site @@ -363,7 +363,7 @@ Plugin Carregado Plugin Apagado Falha ao carregar %s - Iniciada a transferência %d %s + Download iniciado %d %s… Transferido %d %s com sucesso Tudo %s já transferido Transferência em batch @@ -375,18 +375,22 @@ Transferido: %d Desativado: %d Não transferido: %d - Adicionar um repositório para instalar extensões de sites + O CloudStream não possui sites instalados por padrão. Você precisa instalar os sites a partir de repositórios. +\n +\nDevido a uma restrição sem sentido de direitos autorais (DMCA) pela Sky UK Limited 🤮 não podemos vincular o site do repositório no aplicativo. +\n +\nJunte-se ao nosso Discord ou pesquise online. Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas Transferir todos os plugins deste repositório\? %s (Desativado) Instalador APK - %d minuto(s) - Reproduzir trailer + %d min + Assistir Trailer Marcar como visto/não visto Reproduzir - Instalar automaticamente todas as extensões dos repositórios cadastrados. + Instalar automaticamente todos os plugins ainda não instalados dos repositórios adicionados. Baixar extensões automaticamente Refazer o processo de configuração -30 @@ -394,9 +398,9 @@ +30 %s %d%s Elenco: %s - Atualização em andamento + Atualização iniciada Log - Alguns aparelhos não possuem suporte para o novo instalador de pacotes. Use a opção legado caso não esteja conseguindo atualizar. + Alguns aparelhos não suportam o novo instalador de pacotes. Use a opção legado caso não esteja conseguindo atualizar. %d-%d %d %s Iniciar @@ -408,4 +412,121 @@ Ova Anime Player visível - Procurar valor + Instalando atualização do app… + Você tem certeza que deseja sair\? + Versão + Encerramento + Limpar histórico + Abertura + Não + Ordenar por + Sim + Baixando atualização do app… + Episódio %d lançado! + Créditos + Descrição + Tamanho + Parar + Modo seguro ligado + Histórico + Ordenar + Player interno + Autores + Suportado + Idioma + Instalar a extensão primeiro + Playlist HLS + Player de vídeo preferido + Estado + Gestos + Faixas + WP + Cam + Abertura + Selecionar Biblioteca + Contorna o bloqueio do GitHub ao usar jsdelivr, pode atrasar atualizações em alguns dias. + VLC + Todas as linguagens + Atualizado (Novo para Antigo) + Inscrito + HDR + Reiniciar + Navegador Web + Atualizado (Antigo para Novo) + Web Video Cast + DVD + Instalador de pacotes + MPV + Remover dos assistidos + Não foi possível instalar a nova versão do aplicativo + Inscrição cancelada em %s + Final misto + Avaliações (Decrescente) + Aplicar ao reiniciar + Referente + Player oculto - Quantidade de Busca + raw.githubusercontent.com Proxy + Blu-ray + Aparência + 1000 ms + SDR + 18+ + Abrir com + Teste de provedor + UHD + Ver informações sobre falha + Aplicativo não encontrado + Reverter + Link para transmitir + Plugins baixados + %d plugins atualizados + Pular %s + Abertura mista + Alfabético (Z a A) + Parece que esta lista está vazia, tente trocar para outra + Inscrito em %s + 4K + Faixas de vídeo + O aplicativo será atualizado ao sair + Atualizando shows inscritos + Alfabético (A a Z) + Avaliações (Crescente) + Parece que a sua biblioteca está vazia :( +\nFaça login em uma conta de biblioteca ou adicione shows à sua biblioteca local + Arquivo de modo de segurança encontrado! +\nNenhuma extensão será carregada na inicialização do app até que o arquivo seja removido. + Contorno do provedor de serviço de internet (ISP) + Links + Recursos do Player + Recursos + Atualizações de aplicativos + Qualidade Preferida (Dados Móveis) + Quantidade de busca (em segundos) usada quando o player de video está visível + Quantidade de busca (em segundos) usada quando o player de video está oculto + Falha ao conectar com GitHub, ativando proxy jsdelivr. + Cache + Android TV + Legendas + %s %s + TS + Cam + Cam + HQ + HD + TC + Web + Nota: %s + Legado + Todas as extensões foram desativadas devido a uma falha para ajudá-lo a encontrar a que está causando o problema. + Recapitular + Mostrar pop-ups para pular abertura/encerramento + Muito texto. Não é possível salvar na área de transferência. + Marcar como assistido + Backup + Extensões + Ações + Layout + Configurações padrão + SD + Faixas de áudio \ No newline at end of file diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index c1119bfc..76852ca4 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -219,4 +219,33 @@ uuuuhhhoouuooog ooaaahhhh uuu ugggg ooo guggg ooh + auuuooohaaaaagh + uuuuuuuh aaaoo o + ooooooouuuua aa aaagh agh + AAAAUUUOH + aoughoooaaaa + oooouuuh + ahaough aaouuuuh-h + auughooo + ooooooa aauoh + aaaaagh oouoo aaaaaaa + aaaaaagh uuohuoh + aaaaaauo agghhhhhhaoouu + uuuuuuuuh + ouaaahh + ooough aaoough aooou %s aaaa + ouooooouuuu oooooo + aaaaaaaaaaahhhgh-aooohoooo + aau aooooghaao + aagh aaaaaaaaaaaa oooh, aaough, ooga oguuu aaaaaaaaaaa ooooooohghh a-a-aaauo + %dmmmmmm.. +\naaaaooughugh + aooohuohaaaa ooooagh + oooooogh-aaaaaogh + guuuaaaahhhhhhhaaa + woooaaahh ahahaaaauu 🦍 + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOOOGGAGHAGHAAA + aoaaaaaoooghhh + oooooh uuaagh + \@string/home_play \ 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 2812667a..e613cee4 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -318,7 +318,7 @@ Титры Отметить как просмотренное Разрешение видеоплеера - Желаемое качество видео + Предпочтительное качество видео (WiFi) Максимум символов Длинна буфера Кеш видео на диске @@ -518,4 +518,15 @@ Неудачный Прекратить Перезапустить + Вернуться + Подписался на %s + Предпочтительное качество видео (Мобильный интернет) + raw.githubusercontent.com Прокси-сервер + Не удалось подключиться к GitHub. Будет выполнен прокси jsdelivr. + Эпизод %d выпущен! + Обходы провайдера + Обновление подписки на фильмы и сериалы + Обход ограничения доступа к GitHub с помощью jsdelivr может задержать обновления на несколько дней. + Подписные + Отказались от подписки на %s \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index db647b5d..59c65916 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -10,7 +10,7 @@ %dm Poster - \@string/result_poster_img_des + Ảnh bìa Episode Poster Main Poster Next Random @@ -260,18 +260,18 @@ Kiểm tra cập nhật Khóa Thu Phóng - Tuỳ chọn - Tua nhanh + Nguồn + Bỏ qua OP Không hiện lại - Bỏ qua + Bỏ qua bản cập nhật này Cập nhật - Tự động chọn chất lượng phim + Chất lượng xem ưu tiên (WiFi) Kí tự tối đa trên tiêu đề - Định dạng trình phát - Dung lượng video cache + Độ phân giải trình phát video + Kích thước bộ nhớ đệm video Thời lượng bộ nhớ đệm - Dung lượng video cache - Xoá hình ảnh và video + Lưu bộ nhớ đệm video trên ổ cứng + Xoá bộ nhớ đệm hình ảnh và video 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 @@ -519,4 +519,9 @@ 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 + Nhật ký + Chất lượng xem ưu tiên (Dữ liệu di động) + Thất bại + Thành công + Bắt đầu \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 626cc0fe..72d62a04 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -286,7 +286,7 @@ 不再显示 跳过此更新 更新 - 首选播放画质 + 首选播放画质(WiFi) 视频播放器标题最多字符 视频播放器标题 视频缓冲大小 @@ -576,4 +576,5 @@ 使用 jsdelivr 绕过对 Github 的封锁,可能导致更新延迟几天。 ISP 绕过 还原 + 首选播放画质(移动数据) \ No newline at end of file From e85b31c35ddac86e03cfbd495e01a69cedfe56ff Mon Sep 17 00:00:00 2001 From: Lag <> Date: Tue, 7 Mar 2023 17:36:53 +0100 Subject: [PATCH 037/570] Fixing rouge pixels in settings --- app/src/main/res/values/styles.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 78c62c69..b9648162 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -40,7 +40,6 @@ ?attr/textColor ?attr/grayTextColor - ?attr/grayTextColor ?attr/grayTextColor ?attr/textColor From 37244ab0f74392af6861a4f894ebb1a3de77806a Mon Sep 17 00:00:00 2001 From: PokerFace <117321707+pokerface-bad@users.noreply.github.com> Date: Sat, 11 Mar 2023 02:45:11 +0700 Subject: [PATCH 038/570] Intertal Player: Added MPD support (#402) * added isDash in ExtractorLink --- .../cloudstream3/ui/player/CS3IPlayer.kt | 10 +++++----- .../cloudstream3/utils/ExtractorApi.kt | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 782e3fa4..cb8efe92 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -1196,10 +1196,10 @@ class CS3IPlayer : IPlayer { HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) } - val mime = if (link.isM3u8) { - MimeTypes.APPLICATION_M3U8 - } else { - MimeTypes.VIDEO_MP4 + val mime = when { + link.isM3u8 -> MimeTypes.APPLICATION_M3U8 + link.isDash -> MimeTypes.APPLICATION_MPD + else -> MimeTypes.VIDEO_MP4 } val mediaItems = if (link is ExtractorLinkPlayList) { @@ -1249,4 +1249,4 @@ class CS3IPlayer : IPlayer { loadOfflinePlayer(context, it) } } -} \ No newline at end of file +} 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 0bced6b2..b03c9fb7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -52,7 +52,7 @@ data class ExtractorLinkPlayList( ) -open class ExtractorLink( +open class ExtractorLink constructor( open val source: String, open val name: String, override val url: String, @@ -62,7 +62,24 @@ open class ExtractorLink( override val headers: Map = mapOf(), /** Used for getExtractorVerifierJob() */ open val extractorData: String? = null, + open val isDash: Boolean = false, ) : VideoDownloadManager.IDownloadableMinimum { + /** + * Old constructor without isDash, allows for backwards compatibility with extensions. + * Should be removed after all extensions have updated their cloudstream.jar + **/ + constructor( + source: String, + name: String, + url: String, + referer: String, + quality: Int, + isM3u8: Boolean = false, + headers: Map = mapOf(), + /** Used for getExtractorVerifierJob() */ + extractorData: String? = null + ) : this(source, name, url, referer, quality, isM3u8, headers, extractorData, false) + override fun toString(): String { return "ExtractorLink(name=$name, url=$url, referer=$referer, isM3u8=$isM3u8)" } From 8b2881f5f64fad7eb29c93af0a3f696798b93d39 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 10 Mar 2023 20:45:19 +0100 Subject: [PATCH 039/570] Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Dutch) Currently translated at 74.0% (452 of 610 strings) Translated using Weblate (Czech) Currently translated at 100.0% (610 of 610 strings) Added translation using Weblate (Malay) Translated using Weblate (Russian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Turkish) Currently translated at 99.1% (605 of 610 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (English) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (English) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Russian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Czech) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (English) Currently translated at 100.0% (610 of 610 strings) Co-authored-by: Cliff Heraldo <123844876+clxf12@users.noreply.github.com> Co-authored-by: Dan Co-authored-by: Felipe Nogueira Co-authored-by: Fjuro Co-authored-by: Frank Gerritsen Mulkes Co-authored-by: Hosted Weblate Co-authored-by: Rex_sa Co-authored-by: Samuel Gadiel Co-authored-by: Skrripy Co-authored-by: TZVS Co-authored-by: Tang Yin Co-authored-by: Walter H Co-authored-by: eightyy8 Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/ Translation: Cloudstream/App --- app/src/main/res/values-ar/strings.xml | 18 +-- app/src/main/res/values-cs/strings.xml | 18 +-- app/src/main/res/values-es/strings.xml | 18 +-- app/src/main/res/values-in/strings.xml | 18 +-- app/src/main/res/values-ms/strings.xml | 2 + app/src/main/res/values-nl/strings.xml | 11 +- app/src/main/res/values-pt/strings.xml | 14 +- app/src/main/res/values-ru/strings.xml | 16 +-- app/src/main/res/values-tr/strings.xml | 173 ++++++++++++++----------- app/src/main/res/values-uk/strings.xml | 101 +++++++-------- app/src/main/res/values-zh/strings.xml | 14 +- app/src/main/res/values/strings.xml | 20 +-- 12 files changed, 225 insertions(+), 198 deletions(-) create mode 100644 app/src/main/res/values-ms/strings.xml diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index cfd761e3..ae45465b 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -119,16 +119,16 @@ وضع إيغنغرافي يضيف خيار السرعة في المُشغل السحب لتقديم - إسحب إلى اليسار أو اليمين للتحكم في الوقت في مُشغل الفيديو + اسحب من جانب إلى آخر للتحكم في موضعك في مقطع فيديو السحب لتغيير الإعدادات - إسحب على الجانب الأيسر أو الأيمن لتغيير السطوع أو مستوى الصوت + مرر لأعلى أو لأسفل على الجانب الأيسر أو الأيمن لتغيير السطوع أو مستوى الصوت تشغيل الحلقة التالية تلقائيًا تبدأ الحلقة التالية عندما تنتهي الحالية النقر مرتان للتقديم للأمام أو للخلف الضغط مرتان لإيقاف مؤقت - التحكم في مدى تقديم المُشغل + التحكم في مدى تقديم المُشغل(ثوان) إضغط مرتين على الجانب الأيمن أو الأيسر للتقديم للأمام أو للخلف - إضغط في الوسط لإيقاف مؤقت + اضغط مرتين في المنتصف للتوقف استخدم سطوع النظام استخدم سطوع النظام في مُشغل التطبيق بدلاً من التراكب الداكن تحديث تقدم المشاهدة @@ -155,7 +155,7 @@ تحديث الإضافات تلقائيًا تنزيل الإضافات تلقائيًا التحديث التلقائي - البحث تلقائيًا عن التحديثات الجديدة عند البداية + ابحث تلقائيا عن التحديثات الجديدة بعد بدء التطبيق. التحديث إلى الاصدارات التجريبية (بيتا) البحث عن التحديثات التجريبية بدلاً من الإصدارات الكاملة فقط غيت هاب @@ -218,8 +218,8 @@ فيلم مسلسل كرتون - أنمي - اوفا + أنيمي + أوفا تورنت وثائقي دراما آسيوية @@ -284,7 +284,7 @@ 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. عام زر العشوائي - إظهار زر العشوائي على الصفحة الرئيسية + يظهر الزر على الصفحة الرئيسية والذي يمكنه اختيار فيلم عشوائي أو مسلسل تلفزيوني من الصفحة الرئيسية لغات المزود واجهة التطبيق المحتوى المفضل @@ -558,7 +558,7 @@ تجاوز مزود خدمة الإنترنت استرجاع فشل الوصول إلى GitHub ، وتمكين وكيل jsdelivr. - تجاوز حظر GitHub باستخدام jsdelivr ، قد يتسبب في تأخير التحديثات لبضعة أيام. + باستخدام jsdelivr ، يمكن تجاوز حظر GitHub. قد يؤخر التحديثات لبضعة أيام. وكيل raw.githubusercontent.com جودة المشاهدة المفضلة (بيانات الجوال) \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index e99e1010..67179b46 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -122,14 +122,14 @@ Rychlostní režim Přidá do přehrávače možnost rychlosti Přejet pro posun - Přejeďte prstem vlevo nebo vpravo pro ovládání času v přehrávači + Přejeďte prstem ze strany na stranu pro ovládání své pozice ve videu Přejet pro změnu nastavení - Přejeďte prstem na levé nebo pravé straně pro změnu jasu nebo hlasitosti + Přejeďte prstem nahoru nebo dolů na levé nebo pravé straně pro změnu jasu nebo hlasitosti Dvojité klepnutí pro posun Dvojité klepnutí pro pozastavení - Množství času k posunu + Množství času k posunu (sekundy) Klepněte dvakrát vpravo nebo vlevo pro posun vpřed nebo vzad - Klepněte doprostřed pro pozastavení + Klepněte dvakrát doprostřed pro pozastavení Použít systémový jas V přehrávači použít systémov překrytí Aktualizovat postup sledování @@ -151,7 +151,7 @@ Nebude odesílat žádná data Zobrazit výplňové epizody u anime Zobrazit aktualizace aplikace - Při spuštění automaticky zkontrolovat nové aktualizace + Při spuštění aplikace automaticky zkontrolovat nové aktualizace. Aktualizovat na předběžná vydání Kontrolovat aktualizace předběžných vydání, místo normálních plných vydání GitHub @@ -211,8 +211,8 @@ Film Seriál Animovaný - \@string/anime - \@string/ova + Anime + OVA Torrent Dokument Asijské drama @@ -266,7 +266,7 @@ 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 + Zobrazit na domovské stránce tlačítko, kterým lze vybrat náhodný film nebo seriál z domovské stránky Jazyk poskytovatelů Rozložení aplikace Preferovaná média @@ -551,6 +551,6 @@ Nepodařilo se připojit ke GitHubu, povolování proxy jsdelivr. Upřednostněná kvalita sledování (mobilní data) Vrátit zpět - Obchází blokování GitHubu pomocí jsdelivr, může způsobit zpoždění aktualizací o několik dní. + Pomocí jsdelivr lze obejít blokování GitHubu. Může dojít ke zpoždění aktualizací o několik dní. Obcházení ISP \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 0b195275..5c8ac532 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -51,10 +51,10 @@ Elevado Use esto si los subtítulos se muestran %d ms muy pronto Use esto si los subtítulos se muestran %d ms tarde - Desliza el dedo hacia la izquierda o hacia la derecha para controlar el tiempo en el reproductor de video + Desliza el dedo de lado a lado para controlar la posición en un video Filtrar por idioma de medios preferido Eliminar Closed Captions (CC) de los subtítulos - Cantidad de tiempo de búsqueda en el reproductor (en segundos) + Cantidad de búsquedas del reproductor (segundos) Use el brillo del sistema en el reproductor de la app en lugar de una superposición oscura Resolución del reproductor de video MPV @@ -205,16 +205,16 @@ Modo Eigengravy Deslice para avanzar/retroceder Deslice para cambiar la configuración - Deslice el dedo hacia la izquierda o hacia la derecha para cambiar el brillo o el volumen + Deslice hacia arriba o hacia abajo en el lado izquierdo o derecho para cambiar el brillo o el volumen Toca dos veces para buscar Tocar dos veces para pausar Toque dos veces en el lado derecho o izquierdo para buscar hacia adelante o hacia atrás - Toque en el medio para pausar + Toque dos veces en el medio para hacer una pausa Usar brillo del sistema Restaurar datos desde el backup Hacer copia de los datos (backup) Archivo de backup cargado - Buscar automáticamente nuevas actualizaciones al inicio + Busque automáticamente nuevas actualizaciones después de iniciar la aplicación. Rehacer el proceso de configuración inicial Mostrar episodio de relleno para Anime Reproducir Episodio @@ -306,7 +306,7 @@ Aspecto Características Botón de Al azar - Muestra un botón de reproducción \"al azar\" en la página de inicio + Muestra un botón de reproducción \"al azar\" en la página de inicio para poelículas y series cuenta Cerrar sesión Cambiar cuenta @@ -363,8 +363,8 @@ Película Serie Dibujo animado - \@string/anime - \@string/ova + Anime + OVA Torrent Documental Drama asiático @@ -525,7 +525,7 @@ ¡Episodio %d publicado! Proxy raw.githubusercontent.com No se ha podido acceder a GitHub, activando el proxy jsdelivr. - Evita el bloqueo de GitHub usando jsdelivr, puede causar que las actualizaciones se retrasen unos días. + Con jsdelivr, se puede omitir el bloqueo de GitHub. Puede retrasar las actualizaciones unos días. Revertir ISP Bypasses Calidad de visualización preferida (Datos móviles) diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 0e383562..1913868a 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -120,14 +120,14 @@ Mode Eigengravy Menambahkan opsi kecepatan di pemutar Geser untuk mengubah waktu - Geser ke kiri atau kanan untuk mengontrol waktu di pemutar video + Geser dari sisi ke sisi untuk mengontrol posisi dalam video Geser untuk mengubah pengaturan - Geser ke sisi kiri atau kanan untuk mengubah pencerahan atau volume + Geser ke atas atau ke bawah di sisi kiri atau kanan untuk mengubah kecerahan atau volume Tekan dua kali untuk mengubah waktu Tekan dua kali untuk menjeda - Jumlah pengubah waktu pemutar + Jumlah pengubah waktu pemutar (Detik) Tekan dua kali di sisi kanan atau kiri untuk mengubah waktu ke depan atau ke belakang - Tekan di tengah untuk menjeda + Tekan dua kali di tengah untuk menjeda Gunakan pencerahan sistem Gunakan pencerahan sistem di pemutar aplikasi dari pada hamparan gelap Update progres tontonan @@ -149,7 +149,7 @@ Tidak mengirim data Tampilkan episode filler untuk anime Tampilkan update aplikasi - Secara otomatis mencari update terbaru saat aplikasi dibuka + Secara otomatis mencari update terbaru setelah aplikasi dibuka. Update ke prarilis Hanya mencari update prarilis daripada rilis penuh Github @@ -209,8 +209,8 @@ Movie Seri Kartun - \@string/anime - \@string/ova + Anime + OVA Torrent Film Dokumenter Drama Asia @@ -264,7 +264,7 @@ 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 + Tampilkan tombol di halaman utama yang dapat memilih seri film atau TV acak dari halaman utama Bahasa provider Tata Letak Aplikasi Media yang lebih diinginkan @@ -548,7 +548,7 @@ Episode %d telah rilis! raw.githubusercontent.com Proksi Gagal mencapai GitHub, mengaktifkan proksi jsdelivr. - Bypass pemblokiran Github menggunakan JSDeliVR, dapat menyebabkan pembaruan tertunda beberapa hari. + Mengunakan jsdelivers, bisa melewati pemblokiran GitHub. Mungkin dapat menyebabkan pembaruan tertunda dalam beberapa hari. Bypass ISP Pulihkan Nonton dengan kualitas yang di inginkan (Data Seluler) diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-ms/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index c2561914..dd89c34a 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 + Poster Aflevering Poster Hoofdposter Volgende willekeurig @@ -128,14 +128,14 @@ Eigengravy Modus Voegt een snelheidsoptie toe in de speler Swipe to seek - Veeg naar links of rechts om de tijd in de videoplayer te regelen + Veeg naar links of rechts om de tijd in de videospeler te regelen Veeg om instellingen te wijzigen Veeg naar links of rechts om de helderheid of het volume te wijzigen Dubbeltik om te zien Dubbeltik om te pauzeren - Speler zoeken bedrag + Videospeler aantal zoeken Tik twee keer aan de rechter- of linkerkant om vooruit of achteruit te zoeken - Tik in het midden om te pauzeren + Tik twee keer in het midden om te pauzeren Systeemhelderheid gebruiken Gebruik systeemhelderheid in de app-speler in plaats van een donkere overlay Kijkvoortgang bijwerken @@ -405,4 +405,7 @@ Start de volgende episode wanneer deze afgelopen is Volgende episode automatisch afspelen De update is gestart + Bibliotheek + Browser + Logboek \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 0c846361..64ccb903 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -123,16 +123,16 @@ 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 os lados para controlar a posição em um vídeo Deslize para mudar as configurações - Deslize do lado esq. ou dir. para ajustar brilho ou volume + Deslize para cima ou para baixo, no lado esquerdo ou direito, para ajustar brilho ou volume 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 + Tempo de busca no player (Segundos) Toque duplo no lado esq. ou dir. para andar para trás ou para a frente - Toque no meio para pôr em pausa + Toque duas vezes no meio para pausar Usar brilho da sistema Usar brilho do sistema no player em vez de uma sobreposição escura Atualizar progresso @@ -158,7 +158,7 @@ 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 + Procurar automaticamente por novas atualizações depois de iniciar o app. Atualizar para pré-lançamentos Procura atualizações de pré-lançamento em vez de só lançamentos oficiais Github @@ -273,7 +273,7 @@ Aviso Legal Geral Botão Aleatório - Mostra o botão Aleatório na página inicial + Mostra o botão Aleatório na página inicial, que pode escolher aleatoriamente um filme ou série Idioma dos fornecedores Layout da App Mídia preferida @@ -444,7 +444,7 @@ Cam Abertura Selecionar Biblioteca - Contorna o bloqueio do GitHub ao usar jsdelivr, pode atrasar atualizações em alguns dias. + Usando jsdelivr o bloqueio do GitHub pode ser contornado. Pode atrasar atualizações em alguns dias. VLC Todas as linguagens Atualizado (Novo para Antigo) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e613cee4..5295bd35 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -142,10 +142,10 @@ Добавляет опцию скорости в проигрывателе Проведите пальцем для поиска Проведите пальцем для изменения настроек - Проведите пальцем по левой или правой стороне для изменения яркости или громкости + Проведите вверх или вниз по левой или правой стороне, чтобы изменить яркость или громкость Автопроиграть следующего серия Поток торрент - Проведите пальцем влево или вправо, чтобы управлять временем в видеоплеере + Проведите пальцем из стороны в сторону, чтобы управлять свое место в видеоролике Начните следующий серию, когда закончится текущий Загружена резервная копия Не удалось восстановить данные из %s @@ -159,7 +159,7 @@ Автоматическое обновление плагинов Автоматическая загрузка плагинов Показать обновления приложения - Автоматически проверять обновления при старте + Автоматически проверять обновления при старте приложения. Обновится до пре-релиза APK установщик Github @@ -227,7 +227,7 @@ Использовано Двойное нажатие для паузы Коснитесь дважды правой или левой стороны для поиска вперед или назад - Нажмите в центре для паузы + Нажмите дважды в центре, чтобы сделать паузу Использовать системную яркость Автоматически синхронизировать текущий прогресс эпизода Ошибка резервного копирования %s @@ -408,8 +408,8 @@ Съешь ещё этих мягких французских булок, да выпей же чаю Рекомендуется Загружено %s - \@нить/аниме - \@нить/ova + Аниме + OVA Этикетка Dub Сайт Функции @@ -493,7 +493,7 @@ Фильтровать по предпочитаемому языку медиа Неверный ID Ссылка на стрим - Отображать рандомную кнопку на Главной странице + Показывает кнопку на главной странице, с помощью которой можно выбрать случайный фильм или сериал с главной страницы Рандомная кнопка Legacy (старый) Веб видеокаст @@ -501,7 +501,7 @@ Перезагрузить ссылки Предпочтительные медиа Опущенные - Объем перемотки плеера + Объем перемотки плеера (секундах) Объем перемотка, используемый, когда плеер виден Плеер показан - Перемотки объем Плеер спрятан - Перемотки объем diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 807716d8..f53bb69d 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -117,47 +117,47 @@ Hiç muz verilmedi Otomatik seçilecek dil İndirilecek diller - Alt yazı dili - Varsayılana döndürmek için basılı tut + Altyazı dili + Sıfırlamak için basılı tut Fontları içe aktarmak için %s konumuna yerleştirin İzlemeye devam et Kaldır Daha fazla bilgi \@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 + Bu sağlayıcı torrent kullanıyor, bir VPN önerilir Metadata site tarafından sağlanmamış, veri site\'de bulunmuyorsa video yüklenmesi başarısız olacak. Açıklama Konu bulunamadı Açıklama bulunamadı - Logcat\'i göster 🐈 - Resim-içinde-resim - Diğer uygulamaların üzerinde minyatür bir oynatıcıda oynatmaya devam eder + Logcat\'i görüntüle 🐈 + Görüntü içinde görüntü + İçerik diğer uygulamaların üzerinde küçük bir pencerede oynatılmaya devam eder Oynatıcı yeniden boyutlandırma butonu - Siyah sınırları kaldır - Alt yazı + Siyah sınır çizgilerini kaldır + Alt yazılar Oynatıcı alt yazı ayarları - Chromecast alt yazı + Chromecast alt yazıları Chromecast alt yazı ayarları - Eigengravy modu - Oynatıcıya bir hız seçeneği ekle - Gözlemek için kaydır - Zamanı ayarlamak için sağa veya sola kaydır + Eigengrau modu + Oynatıcıya hız seçeneği ekler + Atlamak için kaydır + Zamanı ayarlamak için yanlardan kaydır Ayarları değiştirmek için kaydır - Sol ve sağ taraftan kaydırarak parlaklık ve sesi ayarla + Sol ve sağ taraftan yukarı kaydırarak ekran parlaklığı ve sesi ayarla Sonraki bölümü otomatik oynat Mevcut bölüm bittiğinde sonraki bölüme başla - Gözlemek için çift tıkla - Durdurmak için çift tıkla - Oynatıcı gözleme miktarı - İleri ve geri atlamak için sağa ve sola çift tıkla - Durdurmak için ortaya tıkla + Çift dokunarak atla + İki kez dokunarak duraklat + Atlanacak süre (Saniye) + İleri ve geri atlamak için sağa ve sola iki kez dokun + Durdurmak için ekranın ortasına çift dokun Sistem parlaklığını kullan - Oynatıcıda karanlık kaplama yerine sistem parlaklığını kullan + Oynatıcıyı karartmak yerine sistem parlaklığını kullan İzleme ilerlemesini güncelle Mevcut bölüm ilerlemesini otomatik güncelle - Yedekten geri yükle - Verileri yedekleyin + Verileri yedekten geri yükle + Verileri yedekle Yedek dosyası yüklendi Geri yükleme başarısız oldu: %s Başarıyla yedeklendi @@ -165,21 +165,21 @@ %s yedeklenirken hata Ara Hesaplar - Güncellemeler ve yedek + Güncellemeler ve yedekleme Bilgi Gelişmiş arama - Sağlayıcılara göre ayrılmış arama sonuçlarını ver + Arama sonuçlarını sağlayıcıya göre ayırır Yalnızca çökmelerle ilgili verileri gönderir - Hiç veri göndermez - Anime için filler bölümleri gösterir + Veri göndermez + Anime için filler bölümleri göster Fragmanları göster Kitsu\'dan posterleri göster - Seçilen video kalitelerini arama sonuçlarında gizle + Seçilen video kalitelerini arama sonuçlarında gösterme Otomatik eklenti güncellemeleri Uygulama güncellemelerini göster - Başlangıçta yeni güncellemeleri otomatik olarak ara - Ön sürümlere güncelle - Sadece tam sürümler yerine ön sürüm güncellemelerini de ara + Uygulama başlatıldıktan sonra güncellemeleri otomatik olarak kontrol et. + Deneysel sürümlere güncelle + Yalnızca tam sürümler yerine deneysel güncellemeleri de ara GitHub Aynı geliştiriciler tarafından LightNovel uygulaması Aynı geliştiriciler tarafından anime uygulaması @@ -191,8 +191,8 @@ Bağlantı bulunamadı Bağlantı panoya kopyalandı Bölümü oynat - Varsayılana sıfırla - Üzgünüz, uygulama çöktü. Geliştiricilere isimsiz bir hata raporu gönderilecek + Varsayılan değere sıfırla + Üzgünüz, uygulama çöktü. Geliştiricilere anonim bir hata raporu gönderilecek Sezon %s %d%s Sezon yok @@ -210,8 +210,8 @@ Sürdür -30 +30 - %s dosyası tamamen silinecek -\nEmins misiniz\? + %s tamamen silinecek +\nEmin misiniz\? %dm \nkaldı Devam ediyor @@ -236,9 +236,9 @@ Torrentler Belgeseller OVA - Asya dramaları + Asya dizileri Canlı yayınlar - NSFW + +18 Diğerleri Film @@ -248,9 +248,9 @@ \@string/ova Torrent Belgesel - Asya draması + Asya dizisi Canlı yayın - NSFW + +18 Video Kaynak hatası Sunucu hatası @@ -259,10 +259,10 @@ İndirme hatası, depolama izinlerini kontrol edin Bölümü Chromecast ile yayınla Bağlantıyı Chromecast ile yayınla - Uygulamada oynat - %s\'deda oynat + Burada oynat + %s üzerinden oynat Tarayıcıda oynat - Linki kopyala + Bağlantıyı kopyala Otomatik indir Şu kaynaktan indir Bağlantıları yenile @@ -281,22 +281,22 @@ Kilitle Yeniden boyutlandır Kaynak - OP\'yi geç + Jeneriği geç Bir daha gösterme Bu güncellemeyi atla Güncelle - Tercih edilen izleme kalitesi - Oynatıcıdaki maksimum başlık karakter sayısı - Oynatıcının üst tarafındaki öğeler + Tercih edilen görüntü kalitesi (WiFi) + Video oynatıcı başlığı karakter üst sınırı + Oynatıcının çözünürlüğü Video arabelleği boyutu Video arabelleği uzunluğu - Diskteki video önbelleği + Hafızadaki video önbelleği Video ve resim önbelleğini temizle - Android TV gibi düşük belleğe sahip cihazlarda çok yükseğe ayarlanırsa çökmelere neden olur. - Çok yükseğe ayarlanırsa, Android TV cihazları gibi düşük depolama alanına sahip sistemlerde sorunlara neden olabilir. - HTTPS üzerinden DNS - ISP bloklarını atlatmak için kullanışlıdır - Klon site + Çok yükseğe ayarlanırsa düşük belleğe sahip cihazlarda çökmelere neden olur (örn. Android TV). + Çok yükseğe ayarlanırsa düşük depolama alanına sahip sistemlerde sorunlara neden olur (örn. Android TV). + HTTPS üzerinden DNS (DoH) + İnternet Servis Sağlayıcısı (İSS) kısıtlamalarını aşmak için kullanışlıdır + Siteyi kopyala Siteyi kaldır Farklı bir URL ile mevcut bir sitenin klonunu ekleyin İndirme konumu @@ -305,16 +305,16 @@ Ekrana sığdır Uzat Yakınlaştır - Disclaimer + Yasal Uyarı 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. Genel - Rastgele butonu - Ana sayfada rastgele butonunu göster + Rastgele İçerik + Ana sayfada rastgele bir film veya dizi seçen bir tuş gösterir Sağlayıcı dilleri Uygulama düzeni Tercih edilen medya - Desteklenen sağlayıcılarda NSFW\'yi etkinleştir + Desteklenen sağlayıcılarda +18 içeriği etkinleştir Alt yazı kodlaması Sağlayıcılar Düzen @@ -336,7 +336,7 @@ hello@world.com 127.0.0.1 MyCoolSite - example.com + ornek.com Dil kodu (tr) Hiçbiri Normal @@ -376,7 +376,7 @@ Alt yazı senkronu 1000 ms Alt yazı gecikmesi - Alt yazılar %d ms erken gözüküyorsa bunu kullanın + Alt yazılar %d ms erken görüntüleniyorsa bunu kullanın Alt yazılar %d ms geç gözüküyorsa bunu kullanın Alt yazı gecikmesi yok Pijamalı hasta yağız şoföre çabucak güvendi Önerilen - %s yüklendi + %s eklendi Dosyadan yükle İnternetten yükle İndirilen dosya @@ -422,10 +422,10 @@ Geçersiz veri Geçersiz URL Hata - Alt yazılardan seçmeli alt yazıyı kaldır + Alt yazılardan seçmeli alt yazıyı (CC) kaldır Alt yazılardaki şişkinliği kaldır Tercih edilen medya diline göre filtrele - Ekstralar + Ek içerikler Fragman Yayına bağlan Yönlendiren @@ -433,7 +433,7 @@ Videoları bu dillerde izle Geri Kurulumu atla - Cihazınıza uygun görünümü seçin + Cihazınıza uygun uygulama görünümünü seçin Çökme raporları Ne izlemek istiyorsunuz Bitti @@ -445,7 +445,7 @@ Eklenti silindi %s yüklenemedi +18 - %d %s … indirilmeye başlandı + %d %s indirilmeye başlandı… %d %s indirildi %s\'nin tamamı zaten indirildi Toplu indir @@ -477,7 +477,7 @@ Çökme bilgisini göster Puan: %s Açıklama - Versiyon + Sürüm Durum Boyut Geliştiriciler @@ -499,14 +499,14 @@ Fragmanı oynat Eklenen depolardan henüz yüklenmemiş tüm eklentileri otomatik olarak yükleyin. Güncelleme başladı - Bazı cihazlar yeni paket yükleyiciyi desteklemez.. Güncellemele yüklenmezse eski seçeneği deneyin. + Bazı cihazlar yeni paket yükleyiciyi desteklemez.. Güncellemeler yüklenmezse eski seçeneği deneyin. Eklentileri otomatik olarak indir APK indirici - Linkler + Bağlantılar Uygulama güncellemeleri Yedek Oynatıcı özellikleri - Altyazılar + Alt yazılar Düzen Varsayılanlar Eklentiler @@ -531,22 +531,22 @@ İzlenenlerden kaldır Karışık son Karışık başlangıç - Kredi + Katkıda Bulunanlar Giriş Eklenti İndirildi - Aksiyonlar - Açma/bitiş için atlama açılır pencerelerini göster + Eylemler + Açılış/bitiş için atlama açılır pencerelerini göster Çok fazla metin. Panoya kaydedilemiyor. Kütüphane Tarayıcı Görünüşe göre kütüphaneniz boş :( -\nBir kütüphane hesabına giriş yapın veya yerel kütüphanenize gösteri ekleyin +\nBir kütüphane hesabına giriş yapın veya yerel kütüphanenize içerik ekleyin Güvenli mod dosyası bulundu! \nDosya kaldırılana kadar başlangıçta herhangi bir uzantı yüklenmiyor. Sırala Sırala - Güncel (Yeniden Eskiye) - Güncel (Eskiden Yeniye) + Güncellenme (Yeniden Eskiye) + Güncellenme (Eskiden Yeniye) Alfabetik (A\'dan Z’ye) Alfabetik (Z - A) Kütüphane Seçin @@ -554,4 +554,27 @@ Görünüşe göre bu liste boş, başka bir listeye geçmeyi deneyin Derecelendirme (Yüksekten Düşüğe) Derecelendirme (Düşükten Yükseğe) + Yeniden başlat + Oynatıcı gizlenmişken atlanacak süre + İSS Kısıtlamaları + GitHub\'a ulaşılamadı, jsdelivr vekil sunucusu etkinleştiriliyor. + Başlat + Başarılı oldu + raw.githubusercontent.com vekil sunucusu (proxy) + Tercih edilen görüntü kalitesi (Mobil veri) + Oynatıcı görünürken atlanacak süre + Oynatıcı gizli durumdayken atlanacak süre miktarı + jsdelivr kullanarak GitHub kısıtlamasını aşar. Güncellemeler birkaç gün gecikebilir. + Android TV + Yeni bölüm %d yayınlandı! + Sağlayıcıyı kontrol et + Başarısız oldu + Durdur + Geri al + Abone olunan gösteriler güncelleniyor + Abone olunan + %s kanalına abone olundu + %s kanalı aboneliğinden çıkıldı + Günlük + Oynatıcı görünür durumdayken atlanacak süre miktarı \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index a676b583..dc7a452e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1,13 +1,13 @@ Постер - Постер епізоду + Постер до епізоду Завантаження скасовано - Змінити постачальника + Змінити провайдера Назад Рейтинг: %.1f Актори: %s - Епізод %d буде випущено через + Епізод %d вийде через Poster %s Еп. %d %dд %dгод %dхв @@ -15,14 +15,14 @@ %dхв Головний постер Наступний випадковий - Перегляд фону + Попередній перегляд фону Швидкість (%.2fx) - Нове оновлення знайдено! + Знайдено нове оновлення! \n%s -> %s Пошук Завантаження %d хв - Параметри + Налаштування Пошук… Пошук %s… Дані відсутні @@ -37,7 +37,7 @@ Покинуто Переглянути фільм Переглянути трейлер - Переглянути торрент + Трансляція через торрент Повторити підключення… Назад Переглянути епізод @@ -64,7 +64,7 @@ Тип контуру Шрифт Розмір шрифту - Пошук за допомогою постачальників + Пошук за допомогою провайдерів Пошук за типами Бананів немає Автовибір мови @@ -75,25 +75,25 @@ Продовжити перегляд Вилучити Детальніше - Цей постачальник є торрентом, рекомендується VPN + Цей провайдер є торрентом, рекомендується VPN Опис Сюжет не знайдено Опис не знайдено Показати Logcat 🐈 - Продовження відтворення в мініатюрному плеєрі поверх інших програм + Продовження відтворення в мініатюрному плеєрі поверх інших застосунків Прибирає чорні рамки Субтитри Субтитри Chromecast Налаштування субтитрів Chromecast Режим Eigengravy Проведіть пальцем, щоб змінити налаштування - Проведіть пальцем ліворуч або праворуч, щоб змінити яскравість або гучність + Проведіть пальцем вгору або вниз ліворуч або праворуч, щоб змінити яскравість або гучність Відтворення наступного епізоду після закінчення поточного Головна CloudStream Філер Програти в CloudStream - Потік + Трансляція Переглядаю Поділитися Відкладено @@ -121,11 +121,11 @@ Колір тексту Колір контуру Автовідтворення наступного епізоду - Проведіть пальцем ліворуч або праворуч, щоб керувати часом у відеоплеєрі + Проведіть пальцем з боку в бік, щоб керувати своїм положенням у відео %d Бананів для розробників Кнопка зміни розміру плеєра \@string/home_play - Для коректної роботи цього постачальника може знадобитися VPN + Для коректної роботи цього провайдера може знадобитися VPN Метадані не надаються сайтом, завантаження відео не відбудеться, якщо їх немає на сайті. Картинка в картинці Налаштування субтитрів плеєра @@ -133,8 +133,8 @@ Проведіть пальцем, щоб перемотати Двічі торкніться, щоб перемотати Двічі торкніться для паузи - Крок перемотки - Натисніть посередині, щоб поставити на паузу + Крок перемотки (Секунди) + Натисніть двічі посередині, щоб призупинити Використовувати яскравість системи Оновити прогрес перегляду Відновлення даних з резервної копії @@ -147,23 +147,23 @@ Оновлення та резервне копіювання Інформація Розширений пошук - Надає результати пошуку, розділені за постачальниками + Надає результати пошуку, розділені за провайдерами Надсилає дані лише про збої Не надсилає даних - Показати заповнюючий епізод для аніме + Показати філерний епізод для аніме Показати трейлери Приховати вибрану якість відео в результатах пошуку Автоматичне завантаження плагінів - Показати оновлення програми + Показати оновлення застосунку Повторний процес налаштування - Пошук лише попередніх оновлень, а не повних релізів + Пошук лише бета-оновлень, а не повних релізів Встановлювач APK Github Застосунок для легких новел від тих же розробників Застосунок для аніме від тих же розробників Дайте бананів розробникам - Мова програми - Цей постачальник не має підтримки Chromecast + Мова застосунку + Цей провайдер не має підтримки Chromecast Посилань не знайдено Переглянути епізод Скинути до значення за замовчуванням @@ -180,7 +180,7 @@ \nВи впевнені\? %dхв \nзалишилося - Триває + Виходить Завершено Рейтинг Тривалість @@ -189,7 +189,7 @@ За замовчуванням Вільно Зайнято - Програма + Застосунок Телесеріали Мультфільми Аніме @@ -208,7 +208,7 @@ Віддалена помилка Помилка рендеринга Дзеркало Chromecast - Переглянути в програмі + Переглянути в застосунку Переглянути в %s Автозавантаження Завантажити дзеркало @@ -230,7 +230,7 @@ Показати постери від Kitsu Автоматичне оновлення плагінів Автоматично встановлювати всі ще не встановлені плагіни з доданих репозиторіїв. - Автоматичний пошук нових оновлень при запуску + Автоматично шукати нові оновлення після запуску застосунку. Оновлення до бета-версій Посилання скопійовано в буфер обміну Деякі телефони не підтримують новий інсталятор пакетів. Спробуйте стару версію, якщо оновлення не встановлюються. @@ -255,7 +255,7 @@ Документальні фільми NSFW Фільм - \@string/ova + OVA Торрент Мітка якості NSFW @@ -273,7 +273,7 @@ Заповнити Збільшити Доріжки - Оновлення програми + Оновлення застосунку Кеш Жести Особливості плеєра @@ -283,16 +283,16 @@ Особливості Загальне Випадкова кнопка - Показати випадкову кнопку на Головній сторінці - Мови постачальника - Макет програми + Показує кнопку на Головній сторінці, яка може вибрати випадковий фільм або серіал на Головній сторінці + Мови провайдера + Макет застосунку Бажані медіа Авто Макет телевізора Макет телефону Макет емулятора Основний колір - Тема програми + Тема застосунку Розташування назви постера Розмістіть назву під постером пароль123 @@ -363,7 +363,7 @@ Кодування субтитрів Включити NSFW на підтримуваних постачальників Макет - Постачальники + Провайдери example.com %s %s Депресивний @@ -429,7 +429,7 @@ Оновлено %d плагіни За замовчуванням в CloudStream не встановлені сайти. Вам потрібно встановити сайти з репозиторіїв. \n -\nЧерез безмозкий DMCA від Sky UK Limited 🤮 ми не можемо прив\'язати сайт репозиторію в застосунку. +\nЧерез безмозкий DMCA від Sky UK Limited 🤮 ми не можемо прив\'язати сайт репозиторію в застосунок. \n \nПриєднуйтесь до нашого Discord або шукайте в інтернеті. Переглянути репозиторії спільноти @@ -451,28 +451,28 @@ Вбудований плеєр VLC MPV - Відтворення веб-відео - Веб-браузер - Кінець + Відтворення вебвідео + Веббраузер + Ендінґ Коротке повторення Пропустити %s - Змішаний кінець + Змішаний ендінґ Подяки - Опенінг + Опенінґ Вступ Очистити історію Історія - Показувати спливаючі вікна для опенінгу/кінця + Показувати спливаючі вікна для опенінґу/ендінґу Забагато тексту. Не вдалося зберегти в буфер обміну. Позначити як переглянуте Ви впевнені що хочете вийти\? Так Ні - Установлення оновлення програми… - Не вдалося встановити нову версію програми + Встановлення оновлення застосунку… + Не вдалося встановити нову версію застосунку Старий - Інсталятор пакетів - Програму буде оновлено після виходу + Встановлювач пакетів + Застосунок буде оновлено після виходу Це також призведе до видалення всіх плагінів репозиторію Всі мови Назад @@ -484,10 +484,10 @@ Бажаний відеоплеєр Увімкнено безпечний режим Автори - Завантаження оновлення програми… + Завантаження оновлення застосунку… Усі розширення вимкнено через збій, щоб допомогти вам знайти те, що спричиняє проблеми. - Програму не знайдено - Змішаний опенінг + Застосунок не знайдено + Змішаний опенінґ Видалити з переглянутого За оновленням (від старого до нового) За оновленням (від нового до старого) @@ -517,17 +517,16 @@ Журнал Старт Стоп - Тест постачальника + Тест провайдер Оновлення підписаних шоу Підписано Підписано на %s Відписатися від %s Епізод %d випущено! Повернути - raw.githubusercontent.com -\nProxy + raw.githubusercontent.com Проксі Не вдалося зв\'язатися з GitHub, увімкнувши проксі-сервер jsdelivr. Обходи ISP - Обходити блокування GitHub з використанням jsdlitr, може викликати затримку оновлень на кілька днів. + За допомогою jsdelivr можна обійти блокування GitHub. Можлива затримка оновлень на кілька днів. Бажана якість перегляду (Мобільні дані) \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 72d62a04..a14b87cc 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -142,14 +142,14 @@ 倍速模式 在播放器中添加播放速度选项 滑动控制进度 - 左右滑动控制播放进度 + 左右滑动以控制视频中的位置 滑动更改设置 上下滑动修改亮度或音量 自动播放下一集 播放完毕后播放下一集 双击控制进度 双击暂停 - 双击控制进度时间 + 双击控制进度时间 (秒) 在左右侧双击快进或快退 双击中间暂停 使用系统亮度 @@ -178,7 +178,7 @@ 自动更新插件 自动下载插件 显示应用更新 - 启动时自动搜索更新 + 启动应用后自动搜索更新。 更新至预览版 搜索预览版更新替代仅搜索完整版本 Github @@ -245,8 +245,8 @@ 电影 电视剧 卡通 - \@string/anime - \@string/ova + 动漫 + OVA 种子 纪录片 亚洲剧 @@ -311,7 +311,7 @@ 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. 通用 随机按钮 - 在主页中显示随机按钮 + 在主页上显示按钮,可以从主页上随机选择电影或电视剧 片源语言 应用布局 首选类型 @@ -573,7 +573,7 @@ 日志 raw.githubusercontent.com 代理 连接 Github 失败,正在启用 jsdelivr 代理。 - 使用 jsdelivr 绕过对 Github 的封锁,可能导致更新延迟几天。 + 使用jsdelivr,可以绕过GitHub的封锁。可能会延迟几天的更新。 ISP 绕过 还原 首选播放画质(移动数据) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 49380b5e..911c0d07 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -210,17 +210,17 @@ Eigengravy Mode Adds a speed option in the player Swipe to seek - Swipe left or right to control time in the videoplayer + Swipe from side to side to control your position in a video Swipe to change settings - Swipe on the left or right side to change brightness or volume + Slide up or down on the left or right side to change brightness or volume Autoplay next episode Start the next episode when the current one ends Double tap to seek Double tap to pause - Player seek amount + Player seek amount (Seconds) Tap twice on the right or left side to seek forwards or backwards - Tap in the middle to pause + Tap twice in the middle to pause Use system brightness Use system brightness in the app player instead of a dark overlay @@ -251,7 +251,7 @@ Automatically download plugins Automatically install all not yet installed plugins from added repositories. Show app updates - Automatically search for new updates on start + Automatically search for new updates after starting the app. Redo setup process Update to prereleases Search for prerelease updates instead of full releases only @@ -324,8 +324,8 @@ Movie Series Cartoon - @string/anime - @string/ova + Anime + OVA Torrent Documentary Asian Drama @@ -383,7 +383,7 @@ Useful for bypassing ISP blocks raw.githubusercontent.com Proxy Failed to reach GitHub, enabling jsdelivr proxy. - Bypasses blocking of GitHub using jsdelivr, may cause updates to be delayed by few days. + Using jsdelivr, GitHub blocking can be bypassed. May delay updates by a few days. Clone site Remove site Add a clone of an existing site, with a different URL @@ -428,7 +428,7 @@ Features General Random Button - Show random button on Homepage + Shows button on Homepage which can choose a random movie or TV series from the Homepage Provider languages App Layout Preferred media @@ -657,4 +657,4 @@ Subscribed to %s Unsubscribed from %s Episode %d released! - + \ No newline at end of file From fab55d82c480c2de7a630b567c94e44fbc30ac41 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 9 Mar 2023 22:35:44 +0100 Subject: [PATCH 040/570] Translated using Weblate (Portuguese) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Dutch) Currently translated at 74.0% (452 of 610 strings) Translated using Weblate (Czech) Currently translated at 100.0% (610 of 610 strings) Added translation using Weblate (Malay) Translated using Weblate (Russian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Turkish) Currently translated at 99.1% (605 of 610 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (English) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (English) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Russian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Czech) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (English) Currently translated at 100.0% (610 of 610 strings) Co-authored-by: Cliff Heraldo <123844876+clxf12@users.noreply.github.com> Co-authored-by: Dan Co-authored-by: Felipe Nogueira Co-authored-by: Fjuro Co-authored-by: Frank Gerritsen Mulkes Co-authored-by: Hosted Weblate Co-authored-by: Rex_sa Co-authored-by: Samuel Gadiel Co-authored-by: Skrripy Co-authored-by: TZVS Co-authored-by: Tang Yin Co-authored-by: Walter H Co-authored-by: eightyy8 Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/ Translation: Cloudstream/App --- app/src/main/res/values-ar/strings.xml | 18 +-- app/src/main/res/values-cs/strings.xml | 18 +-- app/src/main/res/values-es/strings.xml | 18 +-- app/src/main/res/values-in/strings.xml | 18 +-- app/src/main/res/values-ms/strings.xml | 2 + app/src/main/res/values-nl/strings.xml | 11 +- app/src/main/res/values-pt/strings.xml | 14 +- app/src/main/res/values-ru/strings.xml | 16 +-- app/src/main/res/values-tr/strings.xml | 173 ++++++++++++++----------- app/src/main/res/values-uk/strings.xml | 33 +++-- app/src/main/res/values-zh/strings.xml | 14 +- app/src/main/res/values/strings.xml | 20 +-- 12 files changed, 191 insertions(+), 164 deletions(-) create mode 100644 app/src/main/res/values-ms/strings.xml diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index cfd761e3..ae45465b 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -119,16 +119,16 @@ وضع إيغنغرافي يضيف خيار السرعة في المُشغل السحب لتقديم - إسحب إلى اليسار أو اليمين للتحكم في الوقت في مُشغل الفيديو + اسحب من جانب إلى آخر للتحكم في موضعك في مقطع فيديو السحب لتغيير الإعدادات - إسحب على الجانب الأيسر أو الأيمن لتغيير السطوع أو مستوى الصوت + مرر لأعلى أو لأسفل على الجانب الأيسر أو الأيمن لتغيير السطوع أو مستوى الصوت تشغيل الحلقة التالية تلقائيًا تبدأ الحلقة التالية عندما تنتهي الحالية النقر مرتان للتقديم للأمام أو للخلف الضغط مرتان لإيقاف مؤقت - التحكم في مدى تقديم المُشغل + التحكم في مدى تقديم المُشغل(ثوان) إضغط مرتين على الجانب الأيمن أو الأيسر للتقديم للأمام أو للخلف - إضغط في الوسط لإيقاف مؤقت + اضغط مرتين في المنتصف للتوقف استخدم سطوع النظام استخدم سطوع النظام في مُشغل التطبيق بدلاً من التراكب الداكن تحديث تقدم المشاهدة @@ -155,7 +155,7 @@ تحديث الإضافات تلقائيًا تنزيل الإضافات تلقائيًا التحديث التلقائي - البحث تلقائيًا عن التحديثات الجديدة عند البداية + ابحث تلقائيا عن التحديثات الجديدة بعد بدء التطبيق. التحديث إلى الاصدارات التجريبية (بيتا) البحث عن التحديثات التجريبية بدلاً من الإصدارات الكاملة فقط غيت هاب @@ -218,8 +218,8 @@ فيلم مسلسل كرتون - أنمي - اوفا + أنيمي + أوفا تورنت وثائقي دراما آسيوية @@ -284,7 +284,7 @@ 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. عام زر العشوائي - إظهار زر العشوائي على الصفحة الرئيسية + يظهر الزر على الصفحة الرئيسية والذي يمكنه اختيار فيلم عشوائي أو مسلسل تلفزيوني من الصفحة الرئيسية لغات المزود واجهة التطبيق المحتوى المفضل @@ -558,7 +558,7 @@ تجاوز مزود خدمة الإنترنت استرجاع فشل الوصول إلى GitHub ، وتمكين وكيل jsdelivr. - تجاوز حظر GitHub باستخدام jsdelivr ، قد يتسبب في تأخير التحديثات لبضعة أيام. + باستخدام jsdelivr ، يمكن تجاوز حظر GitHub. قد يؤخر التحديثات لبضعة أيام. وكيل raw.githubusercontent.com جودة المشاهدة المفضلة (بيانات الجوال) \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index e99e1010..67179b46 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -122,14 +122,14 @@ Rychlostní režim Přidá do přehrávače možnost rychlosti Přejet pro posun - Přejeďte prstem vlevo nebo vpravo pro ovládání času v přehrávači + Přejeďte prstem ze strany na stranu pro ovládání své pozice ve videu Přejet pro změnu nastavení - Přejeďte prstem na levé nebo pravé straně pro změnu jasu nebo hlasitosti + Přejeďte prstem nahoru nebo dolů na levé nebo pravé straně pro změnu jasu nebo hlasitosti Dvojité klepnutí pro posun Dvojité klepnutí pro pozastavení - Množství času k posunu + Množství času k posunu (sekundy) Klepněte dvakrát vpravo nebo vlevo pro posun vpřed nebo vzad - Klepněte doprostřed pro pozastavení + Klepněte dvakrát doprostřed pro pozastavení Použít systémový jas V přehrávači použít systémov překrytí Aktualizovat postup sledování @@ -151,7 +151,7 @@ Nebude odesílat žádná data Zobrazit výplňové epizody u anime Zobrazit aktualizace aplikace - Při spuštění automaticky zkontrolovat nové aktualizace + Při spuštění aplikace automaticky zkontrolovat nové aktualizace. Aktualizovat na předběžná vydání Kontrolovat aktualizace předběžných vydání, místo normálních plných vydání GitHub @@ -211,8 +211,8 @@ Film Seriál Animovaný - \@string/anime - \@string/ova + Anime + OVA Torrent Dokument Asijské drama @@ -266,7 +266,7 @@ 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 + Zobrazit na domovské stránce tlačítko, kterým lze vybrat náhodný film nebo seriál z domovské stránky Jazyk poskytovatelů Rozložení aplikace Preferovaná média @@ -551,6 +551,6 @@ Nepodařilo se připojit ke GitHubu, povolování proxy jsdelivr. Upřednostněná kvalita sledování (mobilní data) Vrátit zpět - Obchází blokování GitHubu pomocí jsdelivr, může způsobit zpoždění aktualizací o několik dní. + Pomocí jsdelivr lze obejít blokování GitHubu. Může dojít ke zpoždění aktualizací o několik dní. Obcházení ISP \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 0b195275..5c8ac532 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -51,10 +51,10 @@ Elevado Use esto si los subtítulos se muestran %d ms muy pronto Use esto si los subtítulos se muestran %d ms tarde - Desliza el dedo hacia la izquierda o hacia la derecha para controlar el tiempo en el reproductor de video + Desliza el dedo de lado a lado para controlar la posición en un video Filtrar por idioma de medios preferido Eliminar Closed Captions (CC) de los subtítulos - Cantidad de tiempo de búsqueda en el reproductor (en segundos) + Cantidad de búsquedas del reproductor (segundos) Use el brillo del sistema en el reproductor de la app en lugar de una superposición oscura Resolución del reproductor de video MPV @@ -205,16 +205,16 @@ Modo Eigengravy Deslice para avanzar/retroceder Deslice para cambiar la configuración - Deslice el dedo hacia la izquierda o hacia la derecha para cambiar el brillo o el volumen + Deslice hacia arriba o hacia abajo en el lado izquierdo o derecho para cambiar el brillo o el volumen Toca dos veces para buscar Tocar dos veces para pausar Toque dos veces en el lado derecho o izquierdo para buscar hacia adelante o hacia atrás - Toque en el medio para pausar + Toque dos veces en el medio para hacer una pausa Usar brillo del sistema Restaurar datos desde el backup Hacer copia de los datos (backup) Archivo de backup cargado - Buscar automáticamente nuevas actualizaciones al inicio + Busque automáticamente nuevas actualizaciones después de iniciar la aplicación. Rehacer el proceso de configuración inicial Mostrar episodio de relleno para Anime Reproducir Episodio @@ -306,7 +306,7 @@ Aspecto Características Botón de Al azar - Muestra un botón de reproducción \"al azar\" en la página de inicio + Muestra un botón de reproducción \"al azar\" en la página de inicio para poelículas y series cuenta Cerrar sesión Cambiar cuenta @@ -363,8 +363,8 @@ Película Serie Dibujo animado - \@string/anime - \@string/ova + Anime + OVA Torrent Documental Drama asiático @@ -525,7 +525,7 @@ ¡Episodio %d publicado! Proxy raw.githubusercontent.com No se ha podido acceder a GitHub, activando el proxy jsdelivr. - Evita el bloqueo de GitHub usando jsdelivr, puede causar que las actualizaciones se retrasen unos días. + Con jsdelivr, se puede omitir el bloqueo de GitHub. Puede retrasar las actualizaciones unos días. Revertir ISP Bypasses Calidad de visualización preferida (Datos móviles) diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 0e383562..1913868a 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -120,14 +120,14 @@ Mode Eigengravy Menambahkan opsi kecepatan di pemutar Geser untuk mengubah waktu - Geser ke kiri atau kanan untuk mengontrol waktu di pemutar video + Geser dari sisi ke sisi untuk mengontrol posisi dalam video Geser untuk mengubah pengaturan - Geser ke sisi kiri atau kanan untuk mengubah pencerahan atau volume + Geser ke atas atau ke bawah di sisi kiri atau kanan untuk mengubah kecerahan atau volume Tekan dua kali untuk mengubah waktu Tekan dua kali untuk menjeda - Jumlah pengubah waktu pemutar + Jumlah pengubah waktu pemutar (Detik) Tekan dua kali di sisi kanan atau kiri untuk mengubah waktu ke depan atau ke belakang - Tekan di tengah untuk menjeda + Tekan dua kali di tengah untuk menjeda Gunakan pencerahan sistem Gunakan pencerahan sistem di pemutar aplikasi dari pada hamparan gelap Update progres tontonan @@ -149,7 +149,7 @@ Tidak mengirim data Tampilkan episode filler untuk anime Tampilkan update aplikasi - Secara otomatis mencari update terbaru saat aplikasi dibuka + Secara otomatis mencari update terbaru setelah aplikasi dibuka. Update ke prarilis Hanya mencari update prarilis daripada rilis penuh Github @@ -209,8 +209,8 @@ Movie Seri Kartun - \@string/anime - \@string/ova + Anime + OVA Torrent Film Dokumenter Drama Asia @@ -264,7 +264,7 @@ 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 + Tampilkan tombol di halaman utama yang dapat memilih seri film atau TV acak dari halaman utama Bahasa provider Tata Letak Aplikasi Media yang lebih diinginkan @@ -548,7 +548,7 @@ Episode %d telah rilis! raw.githubusercontent.com Proksi Gagal mencapai GitHub, mengaktifkan proksi jsdelivr. - Bypass pemblokiran Github menggunakan JSDeliVR, dapat menyebabkan pembaruan tertunda beberapa hari. + Mengunakan jsdelivers, bisa melewati pemblokiran GitHub. Mungkin dapat menyebabkan pembaruan tertunda dalam beberapa hari. Bypass ISP Pulihkan Nonton dengan kualitas yang di inginkan (Data Seluler) diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-ms/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index c2561914..dd89c34a 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 + Poster Aflevering Poster Hoofdposter Volgende willekeurig @@ -128,14 +128,14 @@ Eigengravy Modus Voegt een snelheidsoptie toe in de speler Swipe to seek - Veeg naar links of rechts om de tijd in de videoplayer te regelen + Veeg naar links of rechts om de tijd in de videospeler te regelen Veeg om instellingen te wijzigen Veeg naar links of rechts om de helderheid of het volume te wijzigen Dubbeltik om te zien Dubbeltik om te pauzeren - Speler zoeken bedrag + Videospeler aantal zoeken Tik twee keer aan de rechter- of linkerkant om vooruit of achteruit te zoeken - Tik in het midden om te pauzeren + Tik twee keer in het midden om te pauzeren Systeemhelderheid gebruiken Gebruik systeemhelderheid in de app-speler in plaats van een donkere overlay Kijkvoortgang bijwerken @@ -405,4 +405,7 @@ Start de volgende episode wanneer deze afgelopen is Volgende episode automatisch afspelen De update is gestart + Bibliotheek + Browser + Logboek \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 0c846361..64ccb903 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -123,16 +123,16 @@ 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 os lados para controlar a posição em um vídeo Deslize para mudar as configurações - Deslize do lado esq. ou dir. para ajustar brilho ou volume + Deslize para cima ou para baixo, no lado esquerdo ou direito, para ajustar brilho ou volume 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 + Tempo de busca no player (Segundos) Toque duplo no lado esq. ou dir. para andar para trás ou para a frente - Toque no meio para pôr em pausa + Toque duas vezes no meio para pausar Usar brilho da sistema Usar brilho do sistema no player em vez de uma sobreposição escura Atualizar progresso @@ -158,7 +158,7 @@ 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 + Procurar automaticamente por novas atualizações depois de iniciar o app. Atualizar para pré-lançamentos Procura atualizações de pré-lançamento em vez de só lançamentos oficiais Github @@ -273,7 +273,7 @@ Aviso Legal Geral Botão Aleatório - Mostra o botão Aleatório na página inicial + Mostra o botão Aleatório na página inicial, que pode escolher aleatoriamente um filme ou série Idioma dos fornecedores Layout da App Mídia preferida @@ -444,7 +444,7 @@ Cam Abertura Selecionar Biblioteca - Contorna o bloqueio do GitHub ao usar jsdelivr, pode atrasar atualizações em alguns dias. + Usando jsdelivr o bloqueio do GitHub pode ser contornado. Pode atrasar atualizações em alguns dias. VLC Todas as linguagens Atualizado (Novo para Antigo) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e613cee4..5295bd35 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -142,10 +142,10 @@ Добавляет опцию скорости в проигрывателе Проведите пальцем для поиска Проведите пальцем для изменения настроек - Проведите пальцем по левой или правой стороне для изменения яркости или громкости + Проведите вверх или вниз по левой или правой стороне, чтобы изменить яркость или громкость Автопроиграть следующего серия Поток торрент - Проведите пальцем влево или вправо, чтобы управлять временем в видеоплеере + Проведите пальцем из стороны в сторону, чтобы управлять свое место в видеоролике Начните следующий серию, когда закончится текущий Загружена резервная копия Не удалось восстановить данные из %s @@ -159,7 +159,7 @@ Автоматическое обновление плагинов Автоматическая загрузка плагинов Показать обновления приложения - Автоматически проверять обновления при старте + Автоматически проверять обновления при старте приложения. Обновится до пре-релиза APK установщик Github @@ -227,7 +227,7 @@ Использовано Двойное нажатие для паузы Коснитесь дважды правой или левой стороны для поиска вперед или назад - Нажмите в центре для паузы + Нажмите дважды в центре, чтобы сделать паузу Использовать системную яркость Автоматически синхронизировать текущий прогресс эпизода Ошибка резервного копирования %s @@ -408,8 +408,8 @@ Съешь ещё этих мягких французских булок, да выпей же чаю Рекомендуется Загружено %s - \@нить/аниме - \@нить/ova + Аниме + OVA Этикетка Dub Сайт Функции @@ -493,7 +493,7 @@ Фильтровать по предпочитаемому языку медиа Неверный ID Ссылка на стрим - Отображать рандомную кнопку на Главной странице + Показывает кнопку на главной странице, с помощью которой можно выбрать случайный фильм или сериал с главной страницы Рандомная кнопка Legacy (старый) Веб видеокаст @@ -501,7 +501,7 @@ Перезагрузить ссылки Предпочтительные медиа Опущенные - Объем перемотки плеера + Объем перемотки плеера (секундах) Объем перемотка, используемый, когда плеер виден Плеер показан - Перемотки объем Плеер спрятан - Перемотки объем diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 807716d8..f53bb69d 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -117,47 +117,47 @@ Hiç muz verilmedi Otomatik seçilecek dil İndirilecek diller - Alt yazı dili - Varsayılana döndürmek için basılı tut + Altyazı dili + Sıfırlamak için basılı tut Fontları içe aktarmak için %s konumuna yerleştirin İzlemeye devam et Kaldır Daha fazla bilgi \@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 + Bu sağlayıcı torrent kullanıyor, bir VPN önerilir Metadata site tarafından sağlanmamış, veri site\'de bulunmuyorsa video yüklenmesi başarısız olacak. Açıklama Konu bulunamadı Açıklama bulunamadı - Logcat\'i göster 🐈 - Resim-içinde-resim - Diğer uygulamaların üzerinde minyatür bir oynatıcıda oynatmaya devam eder + Logcat\'i görüntüle 🐈 + Görüntü içinde görüntü + İçerik diğer uygulamaların üzerinde küçük bir pencerede oynatılmaya devam eder Oynatıcı yeniden boyutlandırma butonu - Siyah sınırları kaldır - Alt yazı + Siyah sınır çizgilerini kaldır + Alt yazılar Oynatıcı alt yazı ayarları - Chromecast alt yazı + Chromecast alt yazıları Chromecast alt yazı ayarları - Eigengravy modu - Oynatıcıya bir hız seçeneği ekle - Gözlemek için kaydır - Zamanı ayarlamak için sağa veya sola kaydır + Eigengrau modu + Oynatıcıya hız seçeneği ekler + Atlamak için kaydır + Zamanı ayarlamak için yanlardan kaydır Ayarları değiştirmek için kaydır - Sol ve sağ taraftan kaydırarak parlaklık ve sesi ayarla + Sol ve sağ taraftan yukarı kaydırarak ekran parlaklığı ve sesi ayarla Sonraki bölümü otomatik oynat Mevcut bölüm bittiğinde sonraki bölüme başla - Gözlemek için çift tıkla - Durdurmak için çift tıkla - Oynatıcı gözleme miktarı - İleri ve geri atlamak için sağa ve sola çift tıkla - Durdurmak için ortaya tıkla + Çift dokunarak atla + İki kez dokunarak duraklat + Atlanacak süre (Saniye) + İleri ve geri atlamak için sağa ve sola iki kez dokun + Durdurmak için ekranın ortasına çift dokun Sistem parlaklığını kullan - Oynatıcıda karanlık kaplama yerine sistem parlaklığını kullan + Oynatıcıyı karartmak yerine sistem parlaklığını kullan İzleme ilerlemesini güncelle Mevcut bölüm ilerlemesini otomatik güncelle - Yedekten geri yükle - Verileri yedekleyin + Verileri yedekten geri yükle + Verileri yedekle Yedek dosyası yüklendi Geri yükleme başarısız oldu: %s Başarıyla yedeklendi @@ -165,21 +165,21 @@ %s yedeklenirken hata Ara Hesaplar - Güncellemeler ve yedek + Güncellemeler ve yedekleme Bilgi Gelişmiş arama - Sağlayıcılara göre ayrılmış arama sonuçlarını ver + Arama sonuçlarını sağlayıcıya göre ayırır Yalnızca çökmelerle ilgili verileri gönderir - Hiç veri göndermez - Anime için filler bölümleri gösterir + Veri göndermez + Anime için filler bölümleri göster Fragmanları göster Kitsu\'dan posterleri göster - Seçilen video kalitelerini arama sonuçlarında gizle + Seçilen video kalitelerini arama sonuçlarında gösterme Otomatik eklenti güncellemeleri Uygulama güncellemelerini göster - Başlangıçta yeni güncellemeleri otomatik olarak ara - Ön sürümlere güncelle - Sadece tam sürümler yerine ön sürüm güncellemelerini de ara + Uygulama başlatıldıktan sonra güncellemeleri otomatik olarak kontrol et. + Deneysel sürümlere güncelle + Yalnızca tam sürümler yerine deneysel güncellemeleri de ara GitHub Aynı geliştiriciler tarafından LightNovel uygulaması Aynı geliştiriciler tarafından anime uygulaması @@ -191,8 +191,8 @@ Bağlantı bulunamadı Bağlantı panoya kopyalandı Bölümü oynat - Varsayılana sıfırla - Üzgünüz, uygulama çöktü. Geliştiricilere isimsiz bir hata raporu gönderilecek + Varsayılan değere sıfırla + Üzgünüz, uygulama çöktü. Geliştiricilere anonim bir hata raporu gönderilecek Sezon %s %d%s Sezon yok @@ -210,8 +210,8 @@ Sürdür -30 +30 - %s dosyası tamamen silinecek -\nEmins misiniz\? + %s tamamen silinecek +\nEmin misiniz\? %dm \nkaldı Devam ediyor @@ -236,9 +236,9 @@ Torrentler Belgeseller OVA - Asya dramaları + Asya dizileri Canlı yayınlar - NSFW + +18 Diğerleri Film @@ -248,9 +248,9 @@ \@string/ova Torrent Belgesel - Asya draması + Asya dizisi Canlı yayın - NSFW + +18 Video Kaynak hatası Sunucu hatası @@ -259,10 +259,10 @@ İndirme hatası, depolama izinlerini kontrol edin Bölümü Chromecast ile yayınla Bağlantıyı Chromecast ile yayınla - Uygulamada oynat - %s\'deda oynat + Burada oynat + %s üzerinden oynat Tarayıcıda oynat - Linki kopyala + Bağlantıyı kopyala Otomatik indir Şu kaynaktan indir Bağlantıları yenile @@ -281,22 +281,22 @@ Kilitle Yeniden boyutlandır Kaynak - OP\'yi geç + Jeneriği geç Bir daha gösterme Bu güncellemeyi atla Güncelle - Tercih edilen izleme kalitesi - Oynatıcıdaki maksimum başlık karakter sayısı - Oynatıcının üst tarafındaki öğeler + Tercih edilen görüntü kalitesi (WiFi) + Video oynatıcı başlığı karakter üst sınırı + Oynatıcının çözünürlüğü Video arabelleği boyutu Video arabelleği uzunluğu - Diskteki video önbelleği + Hafızadaki video önbelleği Video ve resim önbelleğini temizle - Android TV gibi düşük belleğe sahip cihazlarda çok yükseğe ayarlanırsa çökmelere neden olur. - Çok yükseğe ayarlanırsa, Android TV cihazları gibi düşük depolama alanına sahip sistemlerde sorunlara neden olabilir. - HTTPS üzerinden DNS - ISP bloklarını atlatmak için kullanışlıdır - Klon site + Çok yükseğe ayarlanırsa düşük belleğe sahip cihazlarda çökmelere neden olur (örn. Android TV). + Çok yükseğe ayarlanırsa düşük depolama alanına sahip sistemlerde sorunlara neden olur (örn. Android TV). + HTTPS üzerinden DNS (DoH) + İnternet Servis Sağlayıcısı (İSS) kısıtlamalarını aşmak için kullanışlıdır + Siteyi kopyala Siteyi kaldır Farklı bir URL ile mevcut bir sitenin klonunu ekleyin İndirme konumu @@ -305,16 +305,16 @@ Ekrana sığdır Uzat Yakınlaştır - Disclaimer + Yasal Uyarı 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. Genel - Rastgele butonu - Ana sayfada rastgele butonunu göster + Rastgele İçerik + Ana sayfada rastgele bir film veya dizi seçen bir tuş gösterir Sağlayıcı dilleri Uygulama düzeni Tercih edilen medya - Desteklenen sağlayıcılarda NSFW\'yi etkinleştir + Desteklenen sağlayıcılarda +18 içeriği etkinleştir Alt yazı kodlaması Sağlayıcılar Düzen @@ -336,7 +336,7 @@ hello@world.com 127.0.0.1 MyCoolSite - example.com + ornek.com Dil kodu (tr) Hiçbiri Normal @@ -376,7 +376,7 @@ Alt yazı senkronu 1000 ms Alt yazı gecikmesi - Alt yazılar %d ms erken gözüküyorsa bunu kullanın + Alt yazılar %d ms erken görüntüleniyorsa bunu kullanın Alt yazılar %d ms geç gözüküyorsa bunu kullanın Alt yazı gecikmesi yok Pijamalı hasta yağız şoföre çabucak güvendi Önerilen - %s yüklendi + %s eklendi Dosyadan yükle İnternetten yükle İndirilen dosya @@ -422,10 +422,10 @@ Geçersiz veri Geçersiz URL Hata - Alt yazılardan seçmeli alt yazıyı kaldır + Alt yazılardan seçmeli alt yazıyı (CC) kaldır Alt yazılardaki şişkinliği kaldır Tercih edilen medya diline göre filtrele - Ekstralar + Ek içerikler Fragman Yayına bağlan Yönlendiren @@ -433,7 +433,7 @@ Videoları bu dillerde izle Geri Kurulumu atla - Cihazınıza uygun görünümü seçin + Cihazınıza uygun uygulama görünümünü seçin Çökme raporları Ne izlemek istiyorsunuz Bitti @@ -445,7 +445,7 @@ Eklenti silindi %s yüklenemedi +18 - %d %s … indirilmeye başlandı + %d %s indirilmeye başlandı… %d %s indirildi %s\'nin tamamı zaten indirildi Toplu indir @@ -477,7 +477,7 @@ Çökme bilgisini göster Puan: %s Açıklama - Versiyon + Sürüm Durum Boyut Geliştiriciler @@ -499,14 +499,14 @@ Fragmanı oynat Eklenen depolardan henüz yüklenmemiş tüm eklentileri otomatik olarak yükleyin. Güncelleme başladı - Bazı cihazlar yeni paket yükleyiciyi desteklemez.. Güncellemele yüklenmezse eski seçeneği deneyin. + Bazı cihazlar yeni paket yükleyiciyi desteklemez.. Güncellemeler yüklenmezse eski seçeneği deneyin. Eklentileri otomatik olarak indir APK indirici - Linkler + Bağlantılar Uygulama güncellemeleri Yedek Oynatıcı özellikleri - Altyazılar + Alt yazılar Düzen Varsayılanlar Eklentiler @@ -531,22 +531,22 @@ İzlenenlerden kaldır Karışık son Karışık başlangıç - Kredi + Katkıda Bulunanlar Giriş Eklenti İndirildi - Aksiyonlar - Açma/bitiş için atlama açılır pencerelerini göster + Eylemler + Açılış/bitiş için atlama açılır pencerelerini göster Çok fazla metin. Panoya kaydedilemiyor. Kütüphane Tarayıcı Görünüşe göre kütüphaneniz boş :( -\nBir kütüphane hesabına giriş yapın veya yerel kütüphanenize gösteri ekleyin +\nBir kütüphane hesabına giriş yapın veya yerel kütüphanenize içerik ekleyin Güvenli mod dosyası bulundu! \nDosya kaldırılana kadar başlangıçta herhangi bir uzantı yüklenmiyor. Sırala Sırala - Güncel (Yeniden Eskiye) - Güncel (Eskiden Yeniye) + Güncellenme (Yeniden Eskiye) + Güncellenme (Eskiden Yeniye) Alfabetik (A\'dan Z’ye) Alfabetik (Z - A) Kütüphane Seçin @@ -554,4 +554,27 @@ Görünüşe göre bu liste boş, başka bir listeye geçmeyi deneyin Derecelendirme (Yüksekten Düşüğe) Derecelendirme (Düşükten Yükseğe) + Yeniden başlat + Oynatıcı gizlenmişken atlanacak süre + İSS Kısıtlamaları + GitHub\'a ulaşılamadı, jsdelivr vekil sunucusu etkinleştiriliyor. + Başlat + Başarılı oldu + raw.githubusercontent.com vekil sunucusu (proxy) + Tercih edilen görüntü kalitesi (Mobil veri) + Oynatıcı görünürken atlanacak süre + Oynatıcı gizli durumdayken atlanacak süre miktarı + jsdelivr kullanarak GitHub kısıtlamasını aşar. Güncellemeler birkaç gün gecikebilir. + Android TV + Yeni bölüm %d yayınlandı! + Sağlayıcıyı kontrol et + Başarısız oldu + Durdur + Geri al + Abone olunan gösteriler güncelleniyor + Abone olunan + %s kanalına abone olundu + %s kanalı aboneliğinden çıkıldı + Günlük + Oynatıcı görünür durumdayken atlanacak süre miktarı \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index a676b583..6dca29b4 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1,13 +1,13 @@ Постер - Постер епізоду + Постер до епізоду Завантаження скасовано Змінити постачальника Назад Рейтинг: %.1f Актори: %s - Епізод %d буде випущено через + Епізод %d вийде через Poster %s Еп. %d %dд %dгод %dхв @@ -15,16 +15,16 @@ %dхв Головний постер Наступний випадковий - Перегляд фону + Попередній перегляд фону Швидкість (%.2fx) - Нове оновлення знайдено! + Знайдено нове оновлення! \n%s -> %s Пошук Завантаження %d хв Параметри Пошук… - Пошук %s… + Пошук на %s… Дані відсутні Більше опцій Наступний епізод @@ -37,7 +37,7 @@ Покинуто Переглянути фільм Переглянути трейлер - Переглянути торрент + Трансляція через торрент Повторити підключення… Назад Переглянути епізод @@ -87,13 +87,13 @@ Налаштування субтитрів Chromecast Режим Eigengravy Проведіть пальцем, щоб змінити налаштування - Проведіть пальцем ліворуч або праворуч, щоб змінити яскравість або гучність + Проведіть пальцем вгору або вниз ліворуч або праворуч, щоб змінити яскравість або гучність Відтворення наступного епізоду після закінчення поточного Головна CloudStream Філер Програти в CloudStream - Потік + Трансляція Переглядаю Поділитися Відкладено @@ -121,7 +121,7 @@ Колір тексту Колір контуру Автовідтворення наступного епізоду - Проведіть пальцем ліворуч або праворуч, щоб керувати часом у відеоплеєрі + Проведіть пальцем з боку в бік, щоб керувати своїм положенням у відео %d Бананів для розробників Кнопка зміни розміру плеєра \@string/home_play @@ -133,8 +133,8 @@ Проведіть пальцем, щоб перемотати Двічі торкніться, щоб перемотати Двічі торкніться для паузи - Крок перемотки - Натисніть посередині, щоб поставити на паузу + Крок перемотки (Секунди) + Натисніть двічі посередині, щоб призупинити Використовувати яскравість системи Оновити прогрес перегляду Відновлення даних з резервної копії @@ -230,7 +230,7 @@ Показати постери від Kitsu Автоматичне оновлення плагінів Автоматично встановлювати всі ще не встановлені плагіни з доданих репозиторіїв. - Автоматичний пошук нових оновлень при запуску + Автоматично шукати нові оновлення після запуску застосунку. Оновлення до бета-версій Посилання скопійовано в буфер обміну Деякі телефони не підтримують новий інсталятор пакетів. Спробуйте стару версію, якщо оновлення не встановлюються. @@ -255,7 +255,7 @@ Документальні фільми NSFW Фільм - \@string/ova + OVA Торрент Мітка якості NSFW @@ -283,7 +283,7 @@ Особливості Загальне Випадкова кнопка - Показати випадкову кнопку на Головній сторінці + Показує кнопку на Головній сторінці, яка може вибрати випадковий фільм або серіал на Головній сторінці Мови постачальника Макет програми Бажані медіа @@ -524,10 +524,9 @@ Відписатися від %s Епізод %d випущено! Повернути - raw.githubusercontent.com -\nProxy + raw.githubusercontent.com Proxy Не вдалося зв\'язатися з GitHub, увімкнувши проксі-сервер jsdelivr. Обходи ISP - Обходити блокування GitHub з використанням jsdlitr, може викликати затримку оновлень на кілька днів. + За допомогою jsdelivr можна обійти блокування GitHub. Можлива затримка оновлень на кілька днів. Бажана якість перегляду (Мобільні дані) \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 72d62a04..a14b87cc 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -142,14 +142,14 @@ 倍速模式 在播放器中添加播放速度选项 滑动控制进度 - 左右滑动控制播放进度 + 左右滑动以控制视频中的位置 滑动更改设置 上下滑动修改亮度或音量 自动播放下一集 播放完毕后播放下一集 双击控制进度 双击暂停 - 双击控制进度时间 + 双击控制进度时间 (秒) 在左右侧双击快进或快退 双击中间暂停 使用系统亮度 @@ -178,7 +178,7 @@ 自动更新插件 自动下载插件 显示应用更新 - 启动时自动搜索更新 + 启动应用后自动搜索更新。 更新至预览版 搜索预览版更新替代仅搜索完整版本 Github @@ -245,8 +245,8 @@ 电影 电视剧 卡通 - \@string/anime - \@string/ova + 动漫 + OVA 种子 纪录片 亚洲剧 @@ -311,7 +311,7 @@ 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. 通用 随机按钮 - 在主页中显示随机按钮 + 在主页上显示按钮,可以从主页上随机选择电影或电视剧 片源语言 应用布局 首选类型 @@ -573,7 +573,7 @@ 日志 raw.githubusercontent.com 代理 连接 Github 失败,正在启用 jsdelivr 代理。 - 使用 jsdelivr 绕过对 Github 的封锁,可能导致更新延迟几天。 + 使用jsdelivr,可以绕过GitHub的封锁。可能会延迟几天的更新。 ISP 绕过 还原 首选播放画质(移动数据) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 49380b5e..911c0d07 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -210,17 +210,17 @@ Eigengravy Mode Adds a speed option in the player Swipe to seek - Swipe left or right to control time in the videoplayer + Swipe from side to side to control your position in a video Swipe to change settings - Swipe on the left or right side to change brightness or volume + Slide up or down on the left or right side to change brightness or volume Autoplay next episode Start the next episode when the current one ends Double tap to seek Double tap to pause - Player seek amount + Player seek amount (Seconds) Tap twice on the right or left side to seek forwards or backwards - Tap in the middle to pause + Tap twice in the middle to pause Use system brightness Use system brightness in the app player instead of a dark overlay @@ -251,7 +251,7 @@ Automatically download plugins Automatically install all not yet installed plugins from added repositories. Show app updates - Automatically search for new updates on start + Automatically search for new updates after starting the app. Redo setup process Update to prereleases Search for prerelease updates instead of full releases only @@ -324,8 +324,8 @@ Movie Series Cartoon - @string/anime - @string/ova + Anime + OVA Torrent Documentary Asian Drama @@ -383,7 +383,7 @@ Useful for bypassing ISP blocks raw.githubusercontent.com Proxy Failed to reach GitHub, enabling jsdelivr proxy. - Bypasses blocking of GitHub using jsdelivr, may cause updates to be delayed by few days. + Using jsdelivr, GitHub blocking can be bypassed. May delay updates by a few days. Clone site Remove site Add a clone of an existing site, with a different URL @@ -428,7 +428,7 @@ Features General Random Button - Show random button on Homepage + Shows button on Homepage which can choose a random movie or TV series from the Homepage Provider languages App Layout Preferred media @@ -657,4 +657,4 @@ Subscribed to %s Unsubscribed from %s Episode %d released! - + \ No newline at end of file From 3a5d8725459a33e9f58a8923e172ef2cae85f0e9 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Fri, 10 Mar 2023 20:01:20 +0000 Subject: [PATCH 041/570] update list of locales --- .../com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 1 + 1 file changed, 1 insertion(+) 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 078419e2..4aa859aa 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 @@ -78,6 +78,7 @@ val appLanguages = arrayListOf( Triple("", "ಕನ್ನಡ", "kn"), Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), + Triple("", "bahasa Melayu", "ms"), Triple("", "Nederlands", "nl"), Triple("", "norsk nynorsk", "nn"), Triple("", "norsk bokmål", "no"), From 13ee8e21d06c34a5f01e476ee133a4acc4b854ea Mon Sep 17 00:00:00 2001 From: Lag <> Date: Fri, 10 Mar 2023 21:33:13 +0100 Subject: [PATCH 042/570] Semi-unfucked VLC on A13+ --- .../lagradost/cloudstream3/MainActivity.kt | 31 ++++++++++++------- .../ui/result/ResultViewModel2.kt | 8 ++++- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a7449255..7818e357 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.content.res.Configuration +import android.os.Build import android.os.Bundle import android.util.AttributeSet import android.util.Log @@ -34,7 +35,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.snackbar.Snackbar import com.jaredrummler.android.colorpicker.ColorPickerDialogListener -import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings @@ -170,7 +170,12 @@ open class ResultResume( val VLC = object : ResultResume( VLC_PACKAGE, - "org.videolan.vlc.player.result", + // Android 13 intent restrictions fucks up specifically launching the VLC player + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + "org.videolan.vlc.player.result" + } else { + Intent.ACTION_VIEW + }, "extra_position", "extra_duration", ) { @@ -733,15 +738,16 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } else { this.setKey(getString(R.string.jsdelivr_proxy_key), true) val parentView: View = findViewById(android.R.id.content) - Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG).let { snackbar -> - snackbar.setAction(R.string.revert) { - setKey(getString(R.string.jsdelivr_proxy_key), false) + Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG) + .let { snackbar -> + snackbar.setAction(R.string.revert) { + setKey(getString(R.string.jsdelivr_proxy_key), false) + } + snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground)) + snackbar.setTextColor(colorFromAttribute(R.attr.textColor)) + snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary)) + snackbar.show() } - snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground)) - snackbar.setTextColor(colorFromAttribute(R.attr.textColor)) - snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary)) - snackbar.show() - } } } @@ -1123,7 +1129,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { suspend fun checkGithubConnectivity(): Boolean { return try { - app.get("https://raw.githubusercontent.com/recloudstream/.github/master/connectivitycheck", timeout = 5).text.trim() == "ok" + app.get( + "https://raw.githubusercontent.com/recloudstream/.github/master/connectivitycheck", + timeout = 5 + ).text.trim() == "ok" } catch (t: Throwable) { false } 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 2983b41d..46a8c9f6 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 @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.result import android.app.Activity import android.content.* import android.net.Uri +import android.os.Build import android.os.Bundle import android.util.Log import android.widget.Toast @@ -1125,7 +1126,12 @@ class ResultViewModel2 : ViewModel() { 1L } - component = VLC_COMPONENT + // Component no longer safe to use in A13 for VLC + // https://code.videolan.org/videolan/vlc-android/-/issues/2776 + // This will likely need to be updated once VLC fixes their documentation. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + component = VLC_COMPONENT + } putExtra("from_start", !resume) putExtra("position", position) From 29174dbb30a2713844199206cc0b2e5723283f6c Mon Sep 17 00:00:00 2001 From: LikDev-256 <81100289+LikDev-256@users.noreply.github.com> Date: Mon, 13 Mar 2023 21:41:35 +0530 Subject: [PATCH 043/570] Feat: fix Streamsb (#417) * Fix Streamsb * feat(StreamSB) stream break: support audiotracks * Revert "feat(StreamSB) stream break: support audiotracks" This reverts commit 078caf9f88dc92bb7416f51458b1bbea73bfb9bf. * Feat: fix Streamsb They normally update source numbers like 50, 51 but instead of 52 they totally dumped everything and just flipped the number into 15 --- .../main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt index b7477242..cac31328 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt @@ -134,7 +134,7 @@ open class StreamSB : ExtractorApi() { it.value.replace(Regex("(embed-|/e/)"), "") }.first() // val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362" - val master = "$mainUrl/sources51/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/" + val master = "$mainUrl/sources15/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/" val headers = mapOf( "watchsb" to "sbstream", ) From 3e2b0f2a17243abbdfddd929544058b7977bc32a Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 10 Mar 2023 20:45:19 +0100 Subject: [PATCH 044/570] Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Dutch) Currently translated at 74.0% (452 of 610 strings) Translated using Weblate (Czech) Currently translated at 100.0% (610 of 610 strings) Added translation using Weblate (Malay) Translated using Weblate (Russian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Turkish) Currently translated at 99.1% (605 of 610 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (English) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (English) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Russian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Czech) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (English) Currently translated at 100.0% (610 of 610 strings) Co-authored-by: Cliff Heraldo <123844876+clxf12@users.noreply.github.com> Co-authored-by: Dan Co-authored-by: Felipe Nogueira Co-authored-by: Fjuro Co-authored-by: Frank Gerritsen Mulkes Co-authored-by: Hosted Weblate Co-authored-by: Rex_sa Co-authored-by: Samuel Gadiel Co-authored-by: Skrripy Co-authored-by: TZVS Co-authored-by: Tang Yin Co-authored-by: Walter H Co-authored-by: eightyy8 Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/ Translation: Cloudstream/App --- app/src/main/res/values-uk/strings.xml | 72 +++++++++++++------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 6dca29b4..48856dbb 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -3,7 +3,7 @@ Постер Постер до епізоду Завантаження скасовано - Змінити постачальника + Змінити провайдера Назад Рейтинг: %.1f Актори: %s @@ -22,7 +22,7 @@ Пошук Завантаження %d хв - Параметри + Налаштування Пошук… Пошук на %s… Дані відсутні @@ -64,7 +64,7 @@ Тип контуру Шрифт Розмір шрифту - Пошук за допомогою постачальників + Пошук за допомогою провайдерів Пошук за типами Бананів немає Автовибір мови @@ -75,12 +75,12 @@ Продовжити перегляд Вилучити Детальніше - Цей постачальник є торрентом, рекомендується VPN + Цей провайдер є торрентом, рекомендується VPN Опис Сюжет не знайдено Опис не знайдено Показати Logcat 🐈 - Продовження відтворення в мініатюрному плеєрі поверх інших програм + Продовження відтворення в мініатюрному плеєрі поверх інших застосунків Прибирає чорні рамки Субтитри Субтитри Chromecast @@ -125,7 +125,7 @@ %d Бананів для розробників Кнопка зміни розміру плеєра \@string/home_play - Для коректної роботи цього постачальника може знадобитися VPN + Для коректної роботи цього провайдера може знадобитися VPN Метадані не надаються сайтом, завантаження відео не відбудеться, якщо їх немає на сайті. Картинка в картинці Налаштування субтитрів плеєра @@ -147,23 +147,23 @@ Оновлення та резервне копіювання Інформація Розширений пошук - Надає результати пошуку, розділені за постачальниками + Надає результати пошуку, розділені за провайдерами Надсилає дані лише про збої Не надсилає даних - Показати заповнюючий епізод для аніме + Показати філерний епізод для аніме Показати трейлери Приховати вибрану якість відео в результатах пошуку Автоматичне завантаження плагінів - Показати оновлення програми + Показати оновлення застосунку Повторний процес налаштування - Пошук лише попередніх оновлень, а не повних релізів + Пошук лише бета-оновлень, а не повних релізів Встановлювач APK Github Застосунок для легких новел від тих же розробників Застосунок для аніме від тих же розробників Дайте бананів розробникам - Мова програми - Цей постачальник не має підтримки Chromecast + Мова застосунку + Цей провайдер не має підтримки Chromecast Посилань не знайдено Переглянути епізод Скинути до значення за замовчуванням @@ -180,7 +180,7 @@ \nВи впевнені\? %dхв \nзалишилося - Триває + Виходить Завершено Рейтинг Тривалість @@ -189,7 +189,7 @@ За замовчуванням Вільно Зайнято - Програма + Застосунок Телесеріали Мультфільми Аніме @@ -208,7 +208,7 @@ Віддалена помилка Помилка рендеринга Дзеркало Chromecast - Переглянути в програмі + Переглянути в застосунку Переглянути в %s Автозавантаження Завантажити дзеркало @@ -273,7 +273,7 @@ Заповнити Збільшити Доріжки - Оновлення програми + Оновлення застосунку Кеш Жести Особливості плеєра @@ -284,15 +284,15 @@ Загальне Випадкова кнопка Показує кнопку на Головній сторінці, яка може вибрати випадковий фільм або серіал на Головній сторінці - Мови постачальника - Макет програми + Мови провайдера + Макет застосунку Бажані медіа Авто Макет телевізора Макет телефону Макет емулятора Основний колір - Тема програми + Тема застосунку Розташування назви постера Розмістіть назву під постером пароль123 @@ -363,7 +363,7 @@ Кодування субтитрів Включити NSFW на підтримуваних постачальників Макет - Постачальники + Провайдери example.com %s %s Депресивний @@ -429,7 +429,7 @@ Оновлено %d плагіни За замовчуванням в CloudStream не встановлені сайти. Вам потрібно встановити сайти з репозиторіїв. \n -\nЧерез безмозкий DMCA від Sky UK Limited 🤮 ми не можемо прив\'язати сайт репозиторію в застосунку. +\nЧерез безмозкий DMCA від Sky UK Limited 🤮 ми не можемо прив\'язати сайт репозиторію в застосунок. \n \nПриєднуйтесь до нашого Discord або шукайте в інтернеті. Переглянути репозиторії спільноти @@ -451,28 +451,28 @@ Вбудований плеєр VLC MPV - Відтворення веб-відео - Веб-браузер - Кінець + Відтворення вебвідео + Веббраузер + Ендінґ Коротке повторення Пропустити %s - Змішаний кінець + Змішаний ендінґ Подяки - Опенінг + Опенінґ Вступ Очистити історію Історія - Показувати спливаючі вікна для опенінгу/кінця + Показувати спливаючі вікна для опенінґу/ендінґу Забагато тексту. Не вдалося зберегти в буфер обміну. Позначити як переглянуте Ви впевнені що хочете вийти\? Так Ні - Установлення оновлення програми… - Не вдалося встановити нову версію програми + Встановлення оновлення застосунку… + Не вдалося встановити нову версію застосунку Старий - Інсталятор пакетів - Програму буде оновлено після виходу + Встановлювач пакетів + Застосунок буде оновлено після виходу Це також призведе до видалення всіх плагінів репозиторію Всі мови Назад @@ -484,10 +484,10 @@ Бажаний відеоплеєр Увімкнено безпечний режим Автори - Завантаження оновлення програми… + Завантаження оновлення застосунку… Усі розширення вимкнено через збій, щоб допомогти вам знайти те, що спричиняє проблеми. - Програму не знайдено - Змішаний опенінг + Застосунок не знайдено + Змішаний опенінґ Видалити з переглянутого За оновленням (від старого до нового) За оновленням (від нового до старого) @@ -517,14 +517,14 @@ Журнал Старт Стоп - Тест постачальника + Тест провайдер Оновлення підписаних шоу Підписано Підписано на %s Відписатися від %s Епізод %d випущено! Повернути - raw.githubusercontent.com Proxy + raw.githubusercontent.com Проксі Не вдалося зв\'язатися з GitHub, увімкнувши проксі-сервер jsdelivr. Обходи ISP За допомогою jsdelivr можна обійти блокування GitHub. Можлива затримка оновлень на кілька днів. From 19dc1a2456b658e85bbd2123e85a5cafdcdc651f Mon Sep 17 00:00:00 2001 From: Lag <> Date: Tue, 14 Mar 2023 12:59:32 +0100 Subject: [PATCH 045/570] Un-bruh-momented some translations --- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-bg/strings.xml | 6 +++--- app/src/main/res/values-bn/strings.xml | 4 ++-- app/src/main/res/values-bp/strings.xml | 8 ++++---- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-el/strings.xml | 6 +++--- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 6 +++--- app/src/main/res/values-hr/strings.xml | 6 +++--- app/src/main/res/values-hu/strings.xml | 8 ++++---- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-iw/strings.xml | 6 +++--- app/src/main/res/values-nl/strings.xml | 6 +++--- app/src/main/res/values-no/strings.xml | 8 ++++---- app/src/main/res/values-pl/strings.xml | 6 +++--- app/src/main/res/values-qt/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-sk/strings.xml | 2 +- app/src/main/res/values-sv/strings.xml | 10 +++++----- app/src/main/res/values-tl/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 16 ++++++++-------- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-ur/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 4 ++-- app/src/main/res/values-zh-rTW/strings.xml | 16 ++++++++-------- app/src/main/res/values-zh/strings.xml | 12 ++++++------ 29 files changed, 77 insertions(+), 77 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index ae45465b..84934288 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -342,7 +342,7 @@ الكل الحد الاقصي الحد الأدنى - \@string/none + @string/none الخطوط المحيطة النمط المنخفض ظل diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index f1f512a1..496512f7 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -105,7 +105,7 @@ Продължете да гледате Премахване Повече информация - \@string/home_play + @string/home_play Може да е необходим VPN, за да работи правилно този доставчик Този доставчик е торент, препоръчва се VPN Метаданните не се предоставят от сайта, зареждането на видео ще бъде неуспешно, ако не съществува на сайта. @@ -223,8 +223,8 @@ Филм Серия Анимационен филм - \@string/anime - \@string/ova + @string/anime + @string/ova Торент Документален филм Азиатска драма diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 7e0448d6..7c37e291 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -143,8 +143,8 @@ হালনাগাদ ও ব্যাকআপ অ্যাপ এর হালনাগাদ দেখান খুঁজতে সোয়াইপ করুন - \@string/result_poster_img_des - \@string/home_play + @string/result_poster_img_des + @string/home_play আগাতে ডবল ট্যাপ করুন আইজেনগ্রাভি মোড আপডেট শুরু হয়েছে diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 2c2e1303..acdf0ae0 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 @@ -108,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. @@ -222,8 +222,8 @@ Filme Série Desenho Animado - \@string/anime - \@string/ova + @string/anime + @string/ova Torrent Documentário Drama Asiático diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 67179b46..1a139511 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -103,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. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7cf49de1..e1093e05 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -115,7 +115,7 @@ Weiterschauen Entfernen Mehr Infos - \@string/home_play + @string/home_play Damit dieser Anbieter korrekt funktioniert, ist möglicherweise ein VPN erforderlich Dieser Anbieter bietet Torrents an, ein VPN wird dringend empfohlen Metadaten werden nicht von der Website bereitgestellt, das Laden des Videos schlägt fehl, wenn sie auf der Website nicht vorhanden sind. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 5e9dafd8..0d45b2c1 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -387,7 +387,7 @@ Κλείσιμο Εκκαθάριση Γλώσσα υποτίτλων - \@string/home_play + @string/home_play Δεν έχουν παρασχεθεί μεταδεδομένα από τον ιστότοπο, η φόρτωση του βίντεο θα αποτύχει αν δεν υπάρχει στον ιστότοπο. Διπλό πάτημα για παύση Μέγεθος αναζήτησης στο πρόγραμμα αναπαραγωγής @@ -452,7 +452,7 @@ Ανάμεικτοι τίτλοι τέλους -30 Κριτική - \@string/ova + @string/ova Ενημερώσεις εφαρμογής Αντίγραφο ασφαλείας Extensions @@ -464,7 +464,7 @@ Προεπιλεγμένα %s %s Μέγεθος γραμματοσειράς - \@string/anime + @string/anime Σύνδεσμοι Εμφάνιση Χαρακτηριστικά diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5c8ac532..f036653f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -194,7 +194,7 @@ Continuar Viendo Remover Más info - \@string/home_play + @string/home_play Una VPN puede ser necesaria para que este proveedor funcione correctamente Este proveedor es un torrent, se recomienda una VPN El sitio no proporciona los metadatos, la carga del video fallará si no existe en el sitio. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f3d35c19..9fee8c3c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -240,7 +240,7 @@ Continuer à regarder Retirer Plus d\'informations - \@string/home_play + @string/home_play Un VPN peut être nécessaire pour que ce fournisseur fonctionne correctement Ce fournisseur est un torrent, un VPN est recommandé Les métadonnées ne sont pas fournies par le site, le chargement de la vidéo échouera si elles n\'existent pas sur le site. @@ -385,8 +385,8 @@ 4K Web -30 - \@string/anime - \@string/ova + @string/anime + @string/ova NSFW %s %s Filtrez par langue préférée diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 159542cc..23fd9624 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -119,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. @@ -238,8 +238,8 @@ Film Serija Crtić - \@string/anime - \@string/ova + @string/anime + @string/ova Torrent Dokumentarac Azijska drama diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 5b42fd6a..66526821 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -57,7 +57,7 @@ Megnyitás böngészőben Betöltés kihagyása Poster - \@string/result_poster_img_des + @string/result_poster_img_des Nézés Befejezve Később megnézés @@ -111,7 +111,7 @@ Betűtípusok importálása %s Eltávolítás Több információ - \@string/home_play + @string/home_play VPN szükséges lehet ehhez a szolgáltató megfelelő működéséhez Ez a szolgáltató torrent, VPN ajánlott Leírás @@ -172,11 +172,11 @@ OVA Egyebek Sorozat - \@string/anime + @string/anime Forráshiba NSFW Rajzfilm - \@string/ova + @string/ova Élőadás NSFW Videó diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 1913868a..f5af3877 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -101,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. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b8e7eb20..4476b4a0 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -108,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. diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 11cf77ce..4ed5ddc0 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -116,7 +116,7 @@ כתוביות כרומקאסט ממשיך ניגון בנגן מינימלי מעל ישומים אחרים כתוביות - \@string/home_play + @string/home_play היסטוריה מורשת לא @@ -164,8 +164,8 @@ משומש סדרת טלוויזיה סדרות/סרטים מצוירים - \@string/אנימה - \@string/אנימציית וידאו מקורית + @string/אנימה + @string/אנימציית וידאו מקורית דרמה אסייתית כרומקאסט את הפרק כרומקאסט את המראה diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index dd89c34a..3595a24a 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -109,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. @@ -222,8 +222,8 @@ Film Serie Tekenfilm - \@string/anime - \@string/ova + @string/anime + @string/ova Torrent Documentaire Aziatisch drama diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 41bf704d..d9feb60c 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 @@ -412,7 +412,7 @@ Slå av/på grensesnittselementer på plakat Hopp over denne oppdateringen Forårsaker tilfeldige krasj hvis satt for høyt. Ikke endre dette hvis du ikke har lite minne. - \@string/home_play + @string/home_play Sikkerhetskopier data Data lagret Kunne ikke logge inn på %s @@ -422,11 +422,11 @@ Sensurerbart Vev Lenke til strøm - \@string/anime + @string/anime Skjul valgt videokvalitet i søkeresultater Lastet inn sikkerhetkopifil Oppdateringer og sikkerhetskopi - \@string/ova + @string/ova Avslutt\? Sensurerbart Alle %s er allerede nedlastet diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 558a46ed..7fc0c887 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -455,7 +455,7 @@ Instalator APK Niektóre telefony nie obsługują nowego instalatora pakietów. Wypróbuj tryb legacy, jeśli aktualizacje nie zostaną zainstalowane. password123 - \@string/ova + @string/ova MojaFajnaWitryna MyCoolUsername 127.0.0.1 @@ -463,9 +463,9 @@ przyklad.pl /\?\? Instalator pakietów - \@string/home_play + @string/home_play hello@world.com - \@string/anime + @string/anime Opening Ending Mixed opening diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index 76852ca4..aee3de91 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -247,5 +247,5 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOOOGGAGHAGHAAA aoaaaaaoooghhh oooooh uuaagh - \@string/home_play + @string/home_play \ 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 42d9b7c8..8cd24a3b 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -106,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. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5295bd35..e9494040 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -363,7 +363,7 @@ Расширения URL репозитория Плагин загружен - \@string/home_play + @string/home_play Перемотка двойным нажатием /\?\? /%d diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 66d8ada9..96fbaff1 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -99,7 +99,7 @@ Tento poskytovateľ je torrent, odporúča sa VPN Importovať písma ich umiestnením do %s Viac informácií - \@string/home_play + @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. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 32336b66..25066d7b 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -33,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 @@ -44,7 +44,7 @@ Inaktivera automatisk felrapportering Mer information Hide - \@string/result_poster_img_des + @string/result_poster_img_des Spela upp Info Nästa @@ -235,7 +235,7 @@ Episod %d kommer släppas om %d min Visa trailers - \@string/home_play + @string/home_play OVA %d-%d %d %s @@ -244,7 +244,7 @@ %dm \nåterstår NSFW - \@string/ova + @string/ova Torrent NSFW +30 @@ -273,7 +273,7 @@ Asiatiska draman Andra Tecknade serier - \@string/anime + @string/anime Dokumentär Asiatisk drama Video diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 9e5b29d4..721c421c 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 diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index f53bb69d..975242b2 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -23,9 +23,9 @@ Bölüm Posteri Ana Poster Sonraki Rastgele - \@string/play_episode + @string/play_episode Geri git - \@string/home_change_provider_img_des + @string/home_change_provider_img_des Change Provider Preview Background @@ -46,7 +46,7 @@ Veri yok Daha fazla seçenek Sonraki bölüm - \@string/synopsis + @string/synopsis Türler Paylaş Tarayıcıda aç @@ -123,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ı torrent kullanıyor, bir VPN önerilir Metadata site tarafından sağlanmamış, veri site\'de bulunmuyorsa video yüklenmesi başarısız olacak. @@ -205,7 +205,7 @@ Bölüm bulunamadı Dosyayı sil Sil - \@string/sort_cancel + @string/sort_cancel Durdur Sürdür -30 @@ -244,8 +244,8 @@ Film Dizi Çizgi film - \@string/anime - \@string/ova + @string/anime + @string/ova Torrent Belgesel Asya dizisi @@ -368,7 +368,7 @@ Hepsi Maksimum Minimum - \@string/none + @string/none Dış hat Çökmüş Gölge diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 6dca29b4..648de819 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -124,7 +124,7 @@ Проведіть пальцем з боку в бік, щоб керувати своїм положенням у відео %d Бананів для розробників Кнопка зміни розміру плеєра - \@string/home_play + @string/home_play Для коректної роботи цього постачальника може знадобитися VPN Метадані не надаються сайтом, завантаження відео не відбудеться, якщо їх немає на сайті. Картинка в картинці diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index f733addc..80081215 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -230,7 +230,7 @@ سلسلہ کارٹون انیمی - \@string/اووا + @string/اووا ٹورینٹ دستاویزی فلم ایشیائی ڈرامے diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 59c65916..74e748a3 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -110,7 +110,7 @@ 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 Thông tin phim @@ -229,7 +229,7 @@ Phim Bộ Hoạt Hình Anime - \@string/ova + @string/ova Torrent Phim Tài Liệu Truyền Hình Châu Á diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8a10208a..6aa41ff3 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -23,9 +23,9 @@ 劇集封面 主封面 隨機下一個 - \@string/play_episode + @string/play_episode 返回 - \@string/home_change_provider_img_des + @string/home_change_provider_img_des 更改片源 預覽背景 @@ -46,7 +46,7 @@ 無資料 更多選項 下一集 - \@string/synopsis + @string/synopsis 類型 分享 在瀏覽器中打開 @@ -123,7 +123,7 @@ 繼續觀看 移除 更多資訊 - \@string/home_play + @string/home_play 此片源可能需要 VPN 才能正常使用 此片源是種子,建議使用 VPN 站點不提供元數據,如果站點上不存在元數據,影片載入將失敗。 @@ -205,7 +205,7 @@ 未找到劇集 刪除文件 刪除 - \@string/sort_cancel + @string/sort_cancel 暫停 繼續 -30 @@ -244,8 +244,8 @@ 電影 電視劇 卡通 - \@string/anime - \@string/ova + @string/anime + @string/ova 種子 紀錄片 亞洲劇 @@ -368,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 a14b87cc..574624bc 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -23,9 +23,9 @@ 剧集封面 主封面 随机下一个 - \@string/play_episode + @string/play_episode 返回 - \@string/home_change_provider_img_des + @string/home_change_provider_img_des 更改片源 预览背景 @@ -46,7 +46,7 @@ 无数据 更多选项 下一集 - \@string/synopsis + @string/synopsis 类型 分享 在浏览器中打开 @@ -123,7 +123,7 @@ 继续观看 移除 更多信息 - \@string/home_play + @string/home_play 此片源可能需要 VPN 才能正常使用 此片源为种子文件,建议使用 VPN 站点不提供元数据,如果站点上不存在元数据,视频加载将失败。 @@ -206,7 +206,7 @@ 未找到剧集 删除文件 删除 - \@string/sort_cancel + @string/sort_cancel 暂停 继续 -30 @@ -369,7 +369,7 @@ 全部 最大 最小 - \@string/none + @string/none 轮廓 凹陷 阴影 From 2d7126d71f3946d072652c4d6e63c938198bdafe Mon Sep 17 00:00:00 2001 From: Lag <> Date: Tue, 14 Mar 2023 13:12:34 +0100 Subject: [PATCH 046/570] Fix for fix for translations --- app/src/main/res/values-iw/strings.xml | 4 ++-- app/src/main/res/values-ur/strings.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 4ed5ddc0..645724fd 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -164,8 +164,8 @@ משומש סדרת טלוויזיה סדרות/סרטים מצוירים - @string/אנימה - @string/אנימציית וידאו מקורית + @string/anime + @string/ova דרמה אסייתית כרומקאסט את הפרק כרומקאסט את המראה diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 80081215..4a8bbf11 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -230,7 +230,7 @@ سلسلہ کارٹون انیمی - @string/اووا + اووا ٹورینٹ دستاویزی فلم ایشیائی ڈرامے From 7bfcf25df4738741c8553cfce5e96fe711cddea6 Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Tue, 14 Mar 2023 18:50:13 +0000 Subject: [PATCH 047/570] add a way to autofix weblate's issue with @string --- .github/locales.py | 15 ++++++++++++++- .github/workflows/update_locales.yml | 9 ++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/locales.py b/.github/locales.py index 1c79c093..04d9cd13 100644 --- a/.github/locales.py +++ b/.github/locales.py @@ -1,6 +1,7 @@ import re import glob import requests +import lxml.etree as ET # builtin library doesn't preserve comments SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt" @@ -45,4 +46,16 @@ open(SETTINGS_PATH, "w+",encoding='utf-8').write( "\n" + END_MARKER + after_src -) \ No newline at end of file +) + +# Go through each values.xml file and fix escaped \@string +for file in glob.glob(f"{XML_NAME}*/strings.xml"): + try: + tree = ET.parse(file) + for child in tree.getroot(): + if child.text.startswith("\\@string/"): + print(f"[{file}] fixing {child.attrib['name']}") + child.text = child.text.replace("\\@string/", "@string/") + tree.write(file, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=True) + except ET.ParseError as ex: + print(f"[{file}] {ex}") \ No newline at end of file diff --git a/.github/workflows/update_locales.yml b/.github/workflows/update_locales.yml index 93cdca44..628e9bc9 100644 --- a/.github/workflows/update_locales.yml +++ b/.github/workflows/update_locales.yml @@ -1,4 +1,4 @@ -name: Update locale lists +name: Fix locale issues on: workflow_dispatch: @@ -9,7 +9,7 @@ on: - master concurrency: - group: "locale-list" + group: "locale" cancel-in-progress: true jobs: @@ -26,6 +26,9 @@ jobs: - uses: actions/checkout@v2 with: token: ${{ steps.generate_token.outputs.token }} + - name: Install dependencies + run: | + pip3 install lxml - name: Edit files run: | python3 .github/locales.py @@ -35,5 +38,5 @@ jobs: git config --local user.name "recloudstream[bot]" git add . # "echo" returns true so the build succeeds, even if no changed files - git commit -m 'update list of locales' || echo + git commit -m 'chore(locales): fix locale issues' || echo git push From 8ebf5185a3fe95db8adabedf483e34ccda1fbdcb Mon Sep 17 00:00:00 2001 From: Lag <> Date: Fri, 17 Mar 2023 15:46:11 +0100 Subject: [PATCH 048/570] Add ffmpeg audio decoding --- app/build.gradle.kts | 2 + .../cloudstream3/ui/player/CS3IPlayer.kt | 44 +++++++++++-------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9cbccbe5..f70a575f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -159,6 +159,8 @@ dependencies { implementation("com.google.android.exoplayer:extension-cast:2.18.2") implementation("com.google.android.exoplayer:extension-mediasession:2.18.2") implementation("com.google.android.exoplayer:extension-okhttp:2.18.2") + // Use the Jellyfin ffmpeg extension for easy ffmpeg audio decoding in exoplayer. Thank you Jellyfin <3 + implementation("org.jellyfin.exoplayer:exoplayer-ffmpeg-extension:2.18.2+1") //implementation("com.google.android.exoplayer:extension-leanback:2.14.0") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index cb8efe92..2aaa3619 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -9,8 +9,11 @@ import android.widget.FrameLayout import androidx.preference.PreferenceManager import com.google.android.exoplayer2.* import com.google.android.exoplayer2.C.* +import com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON +import com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER import com.google.android.exoplayer2.database.StandaloneDatabaseProvider import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource +import com.google.android.exoplayer2.mediacodec.MediaCodecSelector import com.google.android.exoplayer2.source.* import com.google.android.exoplayer2.text.TextRenderer import com.google.android.exoplayer2.trackselection.DefaultTrackSelector @@ -538,7 +541,8 @@ class CS3IPlayer : IPlayer { } // Do no include empty referer, if the provider wants those they can use the header map. - val refererMap = if (link.referer.isBlank()) emptyMap() else mapOf("referer" to link.referer) + val refererMap = + if (link.referer.isBlank()) emptyMap() else mapOf("referer" to link.referer) val headers = mapOf( "accept" to "*/*", "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", @@ -669,23 +673,27 @@ class CS3IPlayer : IPlayer { val exoPlayerBuilder = ExoPlayer.Builder(context) .setRenderersFactory { eventHandler, videoRendererEventListener, audioRendererEventListener, textRendererOutput, metadataRendererOutput -> - DefaultRenderersFactory(context).createRenderers( - eventHandler, - videoRendererEventListener, - audioRendererEventListener, - textRendererOutput, - metadataRendererOutput - ).map { - if (it is TextRenderer) { - currentTextRenderer = CustomTextRenderer( - subtitleOffset, - textRendererOutput, - eventHandler.looper, - CustomSubtitleDecoderFactory() - ) - currentTextRenderer!! - } else it - }.toTypedArray() + DefaultRenderersFactory(context).apply { + setEnableDecoderFallback(true) + // Enable Ffmpeg extension + setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON) + }.createRenderers( + eventHandler, + videoRendererEventListener, + audioRendererEventListener, + textRendererOutput, + metadataRendererOutput + ).map { + if (it is TextRenderer) { + currentTextRenderer = CustomTextRenderer( + subtitleOffset, + textRendererOutput, + eventHandler.looper, + CustomSubtitleDecoderFactory() + ) + currentTextRenderer!! + } else it + }.toTypedArray() } .setTrackSelector( trackSelector ?: getTrackSelector( From 288c5ffa39d60e0285cef573b20fe6ad4ecf7c29 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 16 Mar 2023 13:00:08 +0100 Subject: [PATCH 049/570] Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Croatian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (German) Currently translated at 100.0% (610 of 610 strings) Co-authored-by: Anarchydr Co-authored-by: Hosted Weblate Co-authored-by: Julian Co-authored-by: Sdarfeesh Co-authored-by: Skrripy Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/ Translation: Cloudstream/App --- app/src/main/res/values-de/strings.xml | 16 ++++++++-------- app/src/main/res/values-hr/strings.xml | 18 +++++++++--------- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e1093e05..911705d5 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -41,7 +41,7 @@ Suche %s… Keine Daten vorhanden Mehr Optionen - Nächste Epsisode + Nächste Episode Genres Teilen In Browser öffnen @@ -136,14 +136,14 @@ Wischen zum vor- und zurückspulen Nach links oder rechts wischen, um die Zeit im Videoplayer zu steuern Wischen, um Einstellungen zu ändern - Links oder rechts wischen, um die Helligkeit oder Lautstärke zu ändern + Links oder rechts nach oben oder unten wischen, um die Helligkeit oder Lautstärke zu ändern Nächste Episode automatisch abspielen Nächste Episode wird gestartet, sobald die aktuelle Episode endet Doppeltippen zum vor- und zurückspulen Doppeltippen zum Pausieren - Zeit für vor- und zurückspulen im Player + Zeit für vor- und zurückspulen im Player (Sekunden) Zweimal auf die rechte oder linke Seite tippen, um vor- oder zurückzuspulen - In die Mitte tippen, um zu pausieren + Doppelt in die Mitte tippen, um zu pausieren Systemhelligkeit verwenden Systemhelligkeit anstelle eines dunklen Overlay im Player verwenden Episodenfortschritt aktualisieren @@ -166,7 +166,7 @@ Ausgewählte Videoqualität bei Suchergebnissen ausblenden Automatische Plugin-Updates App-Updates anzeigen - Automatisches Suchen nach neuen Updates beim Start + Automatisches Suchen nach neuen Updates nach dem Start Auf Vorabversionen updaten Suche nach Vorabversionen statt nur nach Vollversionen Github @@ -286,7 +286,7 @@ Haftungsausschluss Allgemein Zufalls-Button - Zufallsbutton auf der Startseite anzeigen + Zeigt einen Zufallsbutton auf der Startseite an, mit welchem eine Serie oder ein Film von der Website zufällig ausgewählt wird Anbieter-Sprachen App-Layout Bevorzugte Medien @@ -519,13 +519,13 @@ Start Neustarten Bevorzugte Videoqualität (mobile Daten) - Umgehung der GitHub Sperre mit jsdelivr, kann zu einigen Tagen Verzögerung bei Updates führen. + Umgehung der GitHub Sperre mit jsdelivr. Kann zu einigen Tagen Verzögerung bei Updates führen. %s abonniert %s deabonniert Episode %d erschienen! raw.githubusercontent.com Proxy GitHub kann nicht erreicht werden, der jsdelivr-Proxy wird aktiviert. - Aktualisierung abonnierter Sendungen + Abonnierte Serien werden aktualisiert Rückgängig Abonniert ISP-Umgehungen diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 23fd9624..5366fe34 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -138,16 +138,16 @@ Eigengravy način Dodaje opciju brzine u playeru Prijeđi prstom za traženje - Prijeđi prstom ulijevo ili udesno za kontrolu vremena u videoplayeru + Prijeđite prstom ulijevo ili udesno kako biste kontrolirali player Klizni za promjenu postavki - Prijeđi prstom ulijevo ili udesno za promjenu svjetline ili glasnoće + Kliznite prstom ulijevo ili udesno za promjenu svjetline ili glasnoće Automatski započni sljedeću epizodu Započne sljedeću epizodu kad trenutna završi Dodirni dvaput za traženje Dodirni dvaput za pauziranje - Iznos preskakanja u playeru + Iznos preskakanja u playeru (Sekunde) Dvaput dodirni desnu ili lijevu stranu ekrana za pomicanje naprijed ili natrag - Dodirni u sredinu zaslona za pauziranje + Dodirnite dvaput u sredinu zaslona za pauziranje Koristi svijetlinu u sustavu Koristi svjetlinu sustava u playeru aplikacija umjesto tamnog preklopa Ažuriraj napredak gledanja @@ -173,7 +173,7 @@ Sakrij odabranu kvalitetu videozapisa u rezultatima pretraživanja Automatsko ažuriranje dodataka Prikaži ažuriranja aplikacije - Automatski traži nova ažuriranja pri pokretanju aplikacije + Automatski traži nova ažuriranja nakon pokretanja aplikacije Ažuriranje na predizdanja Tražite ažuriranja prije izdanja umjesto samo potpunih izdanja Github @@ -238,8 +238,8 @@ Film Serija Crtić - @string/anime - @string/ova + Anime + OVA Torrent Dokumentarac Azijska drama @@ -299,7 +299,7 @@ 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 + Prikazuje gumb na početnoj stranici koji može odabrati nasumični film ili TV seriju s početne stranice Jezici pružatelja usluga Izgled aplikacije Preferirani mediji @@ -552,6 +552,6 @@ ISP zaobilaznice raw.githubusercontent.com Proxy Neuspješno dohvaćanje GitHuba, omogućavanje jsdelivr proxyja. - Zaobilazi blokiranje GitHuba pomoću jsdelivr, može uzrokovati odgode ažuriranja za nekoliko dana. + Koristeći jsdelivr, GitHub blokiranje se može zaobići. Može odgoditi ažuriranja za nekoliko dana. Preferirana kvaliteta gledanja (podatkovna mobilna mreža) \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 648de819..d9ec76bb 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -22,7 +22,7 @@ Пошук Завантаження %d хв - Параметри + Налаштування Пошук… Пошук на %s… Дані відсутні diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 574624bc..47807259 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -573,7 +573,7 @@ 日志 raw.githubusercontent.com 代理 连接 Github 失败,正在启用 jsdelivr 代理。 - 使用jsdelivr,可以绕过GitHub的封锁。可能会延迟几天的更新。 + 使用 jsdelivr,可以绕过 GitHub 的封锁。可能会延迟几天的更新。 ISP 绕过 还原 首选播放画质(移动数据) From 67318a62a37673f1acef39dc60684a0b9e005def Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Fri, 17 Mar 2023 15:04:00 +0000 Subject: [PATCH 050/570] chore(locales): fix locale issues --- app/src/main/res/values-ar/strings.xml | 4 ++-- app/src/main/res/values-bg/strings.xml | 7 ++++--- app/src/main/res/values-bn/strings.xml | 4 ++-- app/src/main/res/values-bp/strings.xml | 7 ++++--- app/src/main/res/values-cs/strings.xml | 7 ++++--- app/src/main/res/values-de/strings.xml | 4 ++-- app/src/main/res/values-el/strings.xml | 7 ++++--- app/src/main/res/values-eo/strings.xml | 4 ++-- app/src/main/res/values-es/strings.xml | 4 ++-- app/src/main/res/values-fa/strings.xml | 4 ++-- app/src/main/res/values-fr/strings.xml | 7 ++++--- app/src/main/res/values-hi/strings.xml | 7 ++++--- app/src/main/res/values-hr/strings.xml | 7 ++++--- app/src/main/res/values-hu/strings.xml | 4 ++-- app/src/main/res/values-in/strings.xml | 7 ++++--- app/src/main/res/values-it/strings.xml | 7 ++++--- app/src/main/res/values-iw/strings.xml | 4 ++-- app/src/main/res/values-ja/strings.xml | 4 ++-- app/src/main/res/values-kn/strings.xml | 4 ++-- app/src/main/res/values-mk/strings.xml | 7 ++++--- app/src/main/res/values-ml/strings.xml | 7 ++++--- app/src/main/res/values-ms/strings.xml | 4 ++-- app/src/main/res/values-nl/strings.xml | 7 ++++--- app/src/main/res/values-nn/strings.xml | 4 ++-- app/src/main/res/values-no/strings.xml | 4 ++-- app/src/main/res/values-pl/strings.xml | 7 ++++--- app/src/main/res/values-pt/strings.xml | 4 ++-- app/src/main/res/values-qt/strings.xml | 4 ++-- app/src/main/res/values-ro/strings.xml | 7 ++++--- app/src/main/res/values-ru/strings.xml | 4 ++-- app/src/main/res/values-sk/strings.xml | 4 ++-- app/src/main/res/values-so/strings.xml | 4 ++-- app/src/main/res/values-sv/strings.xml | 7 ++++--- app/src/main/res/values-ta/strings.xml | 4 ++-- app/src/main/res/values-tl/strings.xml | 7 ++++--- app/src/main/res/values-tr/strings.xml | 7 ++++--- app/src/main/res/values-uk/strings.xml | 4 ++-- app/src/main/res/values-ur/strings.xml | 4 ++-- app/src/main/res/values-vi/strings.xml | 7 ++++--- app/src/main/res/values-zh-rTW/strings.xml | 7 ++++--- app/src/main/res/values-zh/strings.xml | 7 ++++--- 41 files changed, 122 insertions(+), 102 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 84934288..2a356812 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1,4 +1,4 @@ - + ملصق @@ -561,4 +561,4 @@ باستخدام jsdelivr ، يمكن تجاوز حظر GitHub. قد يؤخر التحديثات لبضعة أيام. وكيل raw.githubusercontent.com جودة المشاهدة المفضلة (بيانات الجوال) - \ 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 496512f7..301242cd 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -1,5 +1,6 @@ - - + + + %s еп. %d Актьори: %s @@ -497,4 +498,4 @@ Приложението ще се актуализира при изход от него Започна Актуализация Премахване от гледани - \ No newline at end of file + diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 7c37e291..71d5d6d0 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -1,4 +1,4 @@ - + পোস্টার ক্লাউডস্ট্রিম দিয়ে চালান @@ -148,4 +148,4 @@ আগাতে ডবল ট্যাপ করুন আইজেনগ্রাভি মোড আপডেট শুরু হয়েছে - \ 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 acdf0ae0..13b34872 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -1,5 +1,6 @@ - - + + + %s Ep %d @@ -428,4 +429,4 @@ Começa o próximo episódio quando o atual termina Ativar NSFW em fornecedores compatíveis Fornecedores - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1a139511..1501a5d9 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -1,5 +1,6 @@ - - + + + %s Ep %d @@ -553,4 +554,4 @@ Vrátit zpět Pomocí jsdelivr lze obejít blokování GitHubu. Může dojít ke zpoždění aktualizací o několik dní. Obcházení ISP - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 911705d5..8fbcc2d0 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,4 +1,4 @@ - + %s Ep %d Besetzung: %s @@ -529,4 +529,4 @@ Rückgängig Abonniert ISP-Umgehungen - \ No newline at end of file + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 0d45b2c1..f07ce43c 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -1,5 +1,6 @@ - - + + + CloudStream Αρχική Αναζήτηση @@ -508,4 +509,4 @@ \nΣυνδέσου σε έναν λογαριασμό που έχει βιβλιοθήκη, ή πρόσθεσε σειρές στην τοπική βιβλιοθήκη σου Βρέθηκε αρχείο Ασφαλούς Λειτουργίας! \nΔεν πρόκειται να φορτωθούν extensions κατά το ξεκίνημα μέχρι να διαγραφεί το αρχείο. - \ No newline at end of file + diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 09e6941d..5eac8686 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -1,4 +1,4 @@ - + Reen Hejmo @@ -78,4 +78,4 @@ Rapido (%.2fx) Serĉi… Elŝuti - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index f036653f..06c20aa5 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,4 +1,4 @@ - + Extensiones Descargue la lista de sitios que quiera utilizar @@ -529,4 +529,4 @@ Revertir ISP Bypasses Calidad de visualización preferida (Datos móviles) - \ No newline at end of file + diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 81853674..e4c23628 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -1,4 +1,4 @@ - + حذف مکث @@ -33,4 +33,4 @@ %dساعت %dدقیقه %dدقیقه پوستر اصلی - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9fee8c3c..b96ff0cd 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,5 +1,6 @@ - - + + + CloudStream Accueil Rechercher @@ -523,4 +524,4 @@ Contournements de FAI L\'épisode %d est sorti ! Échouer - \ No newline at end of file + diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index f33a2336..833b76f4 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -1,5 +1,6 @@ - - + + + रफ्तार (%.2fx) नया अपडेट आया है! @@ -146,4 +147,4 @@ %dh %dm %dm विज्ञापन - \ No newline at end of file + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 5366fe34..b4931377 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -1,5 +1,6 @@ - - + + + %d %s | %s %s • %s @@ -554,4 +555,4 @@ Neuspješno dohvaćanje GitHuba, omogućavanje jsdelivr proxyja. Koristeći jsdelivr, GitHub blokiranje se može zaobići. Može odgoditi ažuriranja za nekoliko dana. Preferirana kvaliteta gledanja (podatkovna mobilna mreža) - \ No newline at end of file + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 66526821..1389dff0 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -1,4 +1,4 @@ - + Stáblista: %s %dn %dó%dp @@ -275,4 +275,4 @@ Minőségi jelzés Szinkroncímke Alcímke - \ 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 f5af3877..02234c49 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -1,5 +1,6 @@ - - + + + %s Ep %d Pemeran: %s @@ -552,4 +553,4 @@ Bypass ISP Pulihkan Nonton dengan kualitas yang di inginkan (Data Seluler) - \ 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 4476b4a0..eca60da1 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,5 +1,6 @@ - - + + + %s Ep %d Cast: %s @@ -551,4 +552,4 @@ Aggiornando shows a cui sei iscritto L\'episodio %d è stato rilasciato! Qualità di visualizzazione preferita (Dati mobili) - \ 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 645724fd..b24f0c60 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -1,4 +1,4 @@ - + הרקע של ההצגה לפני צוות שחקנים: %s @@ -506,4 +506,4 @@ אלפביתי (ת\' עד א\') פתח עם נראה שהרשימה הזו ריקה, נסו לעבור לרשימה אחרת - \ No newline at end of file + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index a3d1d434..20641b20 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1,4 +1,4 @@ - + %d分 ダウンロード @@ -182,4 +182,4 @@ アップデートを確認 作品名 アプリのアップデートをインストール中… - \ 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 242653be..4b7b6869 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -1,4 +1,4 @@ - + %sಎಪಿ%d ಕ್ಯಾಸ್ಟ್:%s @@ -125,4 +125,4 @@ ಡೌನ್‌ಲೋಡ್ ಪ್ರಾರಂಭವಾಗಿದೆ ಡೌನ್‌ಲೋಡ್ ರದ್ದುಗೊಳಿಸಲಾಗಿದೆ ಮುಂದಿನ ರಾಂಡಮ್ - \ 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 7251d0d7..811a09c5 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -1,5 +1,6 @@ - - + + + Брзина (%.2fx) Оценето: %.1f @@ -213,4 +214,4 @@ Сенка Подигнат Историја - \ No newline at end of file + diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index b6ad3a80..d430d7cc 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -1,5 +1,6 @@ - - + + + വേഗം (%.2fx) റേറ്റിംഗ്: %.1f @@ -169,4 +170,4 @@ ഔചിത്യ വീഡിയോ ക്വാളിറ്റി ചരിത്രം കണ്ടതാണെന്ന് അടയാളപ്പെടുത്തുക - \ No newline at end of file + diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index a6b3daec..c757504a 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -1,2 +1,2 @@ - - \ No newline at end of file + + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 3595a24a..766bcdc7 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1,5 +1,6 @@ - - + + + %s Ep %d Cast: %s @@ -408,4 +409,4 @@ Bibliotheek Browser Logboek - \ No newline at end of file + diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index b5132028..43738665 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -1,4 +1,4 @@ - + Fleire val Heim @@ -183,4 +183,4 @@ Varigheit Direktesendingar Programoppdateringar - \ 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 d9feb60c..fddd4919 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -1,4 +1,4 @@ - + Plakat @@ -492,4 +492,4 @@ 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 7fc0c887..a2a07dd7 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1,5 +1,6 @@ - - + + + Prędkość (%.2fx) Ocena: %.1f Znaleziono nową aktualizację! @@ -532,4 +533,4 @@ Obchodzi blokadę GitHuba za pomocą jsdelivr, może spowodować opóźnienie aktualizacji o kilka dni. Nie udało się połączyć z GitHub, włączono serwer pośredniczący jsdelivr. Domyślna jakość (dane mobilne) - \ No newline at end of file + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 64ccb903..dd722f62 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,4 +1,4 @@ - + %s Ep %d %dh %dm @@ -529,4 +529,4 @@ Configurações padrão SD Faixas de áudio - \ No newline at end of file + diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index aee3de91..eee28785 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -1,4 +1,4 @@ - + aauugghhaauuh @@ -248,4 +248,4 @@ aoaaaaaoooghhh oooooh uuaagh @string/home_play - \ 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 8cd24a3b..aa443783 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1,5 +1,6 @@ - - + + + %s Ep %d Distribuție: %s @@ -388,4 +389,4 @@ Log Browser Joacă cu CloudStream - \ 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 e9494040..9d8f6895 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,4 +1,4 @@ - + История Нет @@ -529,4 +529,4 @@ Обход ограничения доступа к GitHub с помощью jsdelivr может задержать обновления на несколько дней. Подписные Отказались от подписки на %s - \ 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 index 96fbaff1..a1afd6d9 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -1,4 +1,4 @@ - + Našla sa nová aktualizácia! \n%s -> %s @@ -104,4 +104,4 @@ 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 index b944b6b3..ce7d557a 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -1,4 +1,4 @@ - + Metalaya: %s %dm %ds %dd @@ -487,4 +487,4 @@ 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 25066d7b..0b7ba89e 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -1,5 +1,6 @@ - - + + + Betygsatt: %.1f Hastighet (%.2fx) Ny uppdatering hittad! @@ -368,4 +369,4 @@ Titta på videor på dessa språk Föregående Spår - \ No newline at end of file + diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index b2334c5f..4370e760 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -1,4 +1,4 @@ - + தேடுக தேடல் %s… @@ -107,4 +107,4 @@ இடைநிறுத்துவதற்கு இருமுறை தட்டவும் Chromecast வசன அமைப்புகள் இருண்ட மேலடுக்குக்குப் பதிலாக ஆப் பிளேயரில் சிஸ்டம் பிரகாசத்தைப் பயன்படுத்தவும் - \ No newline at end of file + diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 721c421c..cf3b1263 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -1,5 +1,6 @@ - - + + + %s Ep %d @@ -263,4 +264,4 @@ Magdagdag ng Account Kasaysayan I-tanda bilang napanood na - \ No newline at end of file + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 975242b2..74754008 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,5 +1,6 @@ - - + + + %d %s | %s %s • %s @@ -577,4 +578,4 @@ %s kanalı aboneliğinden çıkıldı Günlük Oynatıcı görünür durumdayken atlanacak süre miktarı - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d9ec76bb..bd062394 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1,4 +1,4 @@ - + Постер Постер до епізоду @@ -529,4 +529,4 @@ Обходи ISP За допомогою jsdelivr можна обійти блокування GitHub. Можлива затримка оновлень на кілька днів. Бажана якість перегляду (Мобільні дані) - \ No newline at end of file + diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 4a8bbf11..c19c6472 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -1,4 +1,4 @@ - + کاسٹ: %s قسط %d جاری کیا جائے گا @@ -356,4 +356,4 @@ %d / 10 اٹھایا اگر سب ٹائٹلز %d ms بہت جلد دکھائے جائیں تو اسے استعمال کریں - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 74e748a3..520cfaa4 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,5 +1,6 @@ - - + + + %s Tập %d @@ -524,4 +525,4 @@ Thất bại Thành công Bắt đầu - \ 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 6aa41ff3..3364ea86 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1,5 +1,6 @@ - - + + + %d %s | %s %s • %s @@ -534,4 +535,4 @@ 外觀 功能 瀏覽器 - \ No newline at end of file + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 47807259..44b93430 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1,5 +1,6 @@ - - + + + %d %s | %s %s • %s @@ -577,4 +578,4 @@ ISP 绕过 还原 首选播放画质(移动数据) - \ No newline at end of file + From 8fff809b792dc8f9885f71509bdde11427d9e378 Mon Sep 17 00:00:00 2001 From: Lag <> Date: Fri, 17 Mar 2023 16:07:28 +0100 Subject: [PATCH 051/570] Revert ffmpeg as it causes issues with subtitles :( --- app/build.gradle.kts | 2 +- .../java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f70a575f..0bd56fe7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -160,7 +160,7 @@ dependencies { implementation("com.google.android.exoplayer:extension-mediasession:2.18.2") implementation("com.google.android.exoplayer:extension-okhttp:2.18.2") // Use the Jellyfin ffmpeg extension for easy ffmpeg audio decoding in exoplayer. Thank you Jellyfin <3 - implementation("org.jellyfin.exoplayer:exoplayer-ffmpeg-extension:2.18.2+1") +// implementation("org.jellyfin.exoplayer:exoplayer-ffmpeg-extension:2.18.2+1") //implementation("com.google.android.exoplayer:extension-leanback:2.14.0") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 2aaa3619..e0885671 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -674,9 +674,9 @@ class CS3IPlayer : IPlayer { ExoPlayer.Builder(context) .setRenderersFactory { eventHandler, videoRendererEventListener, audioRendererEventListener, textRendererOutput, metadataRendererOutput -> DefaultRenderersFactory(context).apply { - setEnableDecoderFallback(true) +// setEnableDecoderFallback(true) // Enable Ffmpeg extension - setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON) +// setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON) }.createRenderers( eventHandler, videoRendererEventListener, From 019399952f4516a1478875c0ca1c3918e55f0788 Mon Sep 17 00:00:00 2001 From: Lag <> Date: Fri, 17 Mar 2023 16:23:03 +0100 Subject: [PATCH 052/570] Better subtitle decoding :) --- .../ui/player/CustomSubtitleDecoderFactory.kt | 68 +++++++++++-------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt index 690d3706..974a5d26 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt @@ -4,13 +4,16 @@ import android.content.Context import android.util.Log import androidx.preference.PreferenceManager import com.google.android.exoplayer2.Format -import com.google.android.exoplayer2.text.SubtitleDecoder -import com.google.android.exoplayer2.text.SubtitleDecoderFactory -import com.google.android.exoplayer2.text.SubtitleInputBuffer -import com.google.android.exoplayer2.text.SubtitleOutputBuffer +import com.google.android.exoplayer2.text.* +import com.google.android.exoplayer2.text.cea.Cea608Decoder +import com.google.android.exoplayer2.text.cea.Cea708Decoder +import com.google.android.exoplayer2.text.dvb.DvbDecoder +import com.google.android.exoplayer2.text.pgs.PgsDecoder import com.google.android.exoplayer2.text.ssa.SsaDecoder import com.google.android.exoplayer2.text.subrip.SubripDecoder import com.google.android.exoplayer2.text.ttml.TtmlDecoder +import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder +import com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder import com.google.android.exoplayer2.text.webvtt.WebvttDecoder import com.google.android.exoplayer2.util.MimeTypes import com.lagradost.cloudstream3.R @@ -19,7 +22,11 @@ import org.mozilla.universalchardet.UniversalDetector import java.nio.ByteBuffer import java.nio.charset.Charset -class CustomDecoder : SubtitleDecoder { +/** + * @param fallbackFormat used to create a decoder based on mimetype if the subtitle string is not + * enough to identify the subtitle format. + **/ +class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { companion object { fun updateForcedEncoding(context: Context) { val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) @@ -139,7 +146,7 @@ class CustomDecoder : SubtitleDecoder { val inputString = getStr(inputBuffer) if (realDecoder == null && !inputString.isNullOrBlank()) { var str: String = inputString - // this way we read the subtitle file and decide what decoder to use instead of relying on mimetype + // this way we read the subtitle file and decide what decoder to use instead of relying fully on mimetype Log.i(TAG, "Got data from queueInputBuffer") //https://github.com/LagradOst/CloudStream-2/blob/ddd774ee66810137ff7bd65dae70bcf3ba2d2489/CloudStreamForms/CloudStreamForms/Script/MainChrome.cs#L388 realDecoder = when { @@ -148,8 +155,31 @@ class CustomDecoder : SubtitleDecoder { (str.startsWith( "[Script Info]", ignoreCase = true - ) || str.startsWith("Title:", ignoreCase = true)) -> SsaDecoder() + ) || str.startsWith("Title:", ignoreCase = true)) -> SsaDecoder(fallbackFormat?.initializationData) str.startsWith("1", ignoreCase = true) -> SubripDecoder() + fallbackFormat != null -> { + when (val mimeType = fallbackFormat.sampleMimeType) { + MimeTypes.TEXT_VTT -> WebvttDecoder() + MimeTypes.TEXT_SSA -> SsaDecoder(fallbackFormat.initializationData) + MimeTypes.APPLICATION_MP4VTT -> Mp4WebvttDecoder() + MimeTypes.APPLICATION_TTML -> TtmlDecoder() + MimeTypes.APPLICATION_SUBRIP -> SubripDecoder() + MimeTypes.APPLICATION_TX3G -> Tx3gDecoder(fallbackFormat.initializationData) + MimeTypes.APPLICATION_CEA608, MimeTypes.APPLICATION_MP4CEA608 -> Cea608Decoder( + mimeType, + fallbackFormat.accessibilityChannel, + Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS + ) + MimeTypes.APPLICATION_CEA708 -> Cea708Decoder( + fallbackFormat.accessibilityChannel, + fallbackFormat.initializationData + ) + MimeTypes.APPLICATION_DVBSUBS -> DvbDecoder(fallbackFormat.initializationData) + MimeTypes.APPLICATION_PGS -> PgsDecoder() + MimeTypes.TEXT_EXOPLAYER_CUES -> ExoplayerCuesDecoder() + else -> null + } + } else -> null } Log.i( @@ -246,28 +276,6 @@ class CustomSubtitleDecoderFactory : SubtitleDecoderFactory { } override fun createDecoder(format: Format): SubtitleDecoder { - return CustomDecoder() - //return when (val mimeType = format.sampleMimeType) { - // MimeTypes.TEXT_VTT -> WebvttDecoder() - // MimeTypes.TEXT_SSA -> SsaDecoder(format.initializationData) - // MimeTypes.APPLICATION_MP4VTT -> Mp4WebvttDecoder() - // MimeTypes.APPLICATION_TTML -> TtmlDecoder() - // MimeTypes.APPLICATION_SUBRIP -> SubripDecoder() - // MimeTypes.APPLICATION_TX3G -> Tx3gDecoder(format.initializationData) - // MimeTypes.APPLICATION_CEA608, MimeTypes.APPLICATION_MP4CEA608 -> return Cea608Decoder( - // mimeType, - // format.accessibilityChannel, - // Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS - // ) - // MimeTypes.APPLICATION_CEA708 -> Cea708Decoder( - // format.accessibilityChannel, - // format.initializationData - // ) - // MimeTypes.APPLICATION_DVBSUBS -> DvbDecoder(format.initializationData) - // MimeTypes.APPLICATION_PGS -> PgsDecoder() - // MimeTypes.TEXT_EXOPLAYER_CUES -> ExoplayerCuesDecoder() - // // Default WebVttDecoder - // else -> WebvttDecoder() - //} + return CustomDecoder(format) } } \ No newline at end of file From 9c40abc4d32f2003d84361828435683b031dc0e0 Mon Sep 17 00:00:00 2001 From: Lag <> Date: Fri, 17 Mar 2023 22:15:25 +0100 Subject: [PATCH 053/570] Added player intent --- app/src/main/AndroidManifest.xml | 10 +++++++++ .../lagradost/cloudstream3/MainActivity.kt | 21 +++++++++++++++++++ .../syncproviders/AccountManager.kt | 1 + .../ui/download/DownloadFragment.kt | 4 ++-- .../ui/player/DownloadedPlayerActivity.kt | 2 +- .../cloudstream3/ui/player/LinkGenerator.kt | 17 ++++++++++----- 6 files changed, 47 insertions(+), 8 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 871c4f69..563c82f8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -98,6 +98,16 @@ + + + + + + + + + + diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 7818e357..d054f504 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.content.res.Configuration +import android.net.Uri import android.os.Build import android.os.Bundle import android.util.AttributeSet @@ -57,6 +58,7 @@ import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch @@ -65,6 +67,9 @@ 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.player.BasicLink +import com.lagradost.cloudstream3.ui.player.GeneratorPlayer +import com.lagradost.cloudstream3.ui.player.LinkGenerator import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST import com.lagradost.cloudstream3.ui.result.setImage @@ -274,6 +279,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { isWebview: Boolean ): Boolean = with(activity) { + // TODO MUCH BETTER HANDLING + // Invalid URIs can crash fun safeURI(uri: String) = normalSafeApiCall { URI(uri) } @@ -329,6 +336,20 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // It might be better to use the QuickSearch. nav_view?.selectedItemId = R.id.navigation_search nav_rail_view?.selectedItemId = R.id.navigation_search + } else if (safeURI(str)?.scheme == appStringPlayer) { + val uri = Uri.parse(str) + val name = uri.getQueryParameter("name") + val url = URLDecoder.decode(uri.authority, "UTF-8") + + navigate( + R.id.global_to_navigation_player, + GeneratorPlayer.newInstance( + LinkGenerator( + listOf(BasicLink(url, name)), + extract = true, + ) + ) + ) } else if (safeURI(str)?.scheme == appStringResumeWatching) { val id = str.substringAfter("$appStringResumeWatching://").toIntOrNull() 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 f17086c1..8ce6bae2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -45,6 +45,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { const val appString = "cloudstreamapp" const val appStringRepo = "cloudstreamrepo" + const val appStringPlayer = "cloudstreamplayer" // Instantly start the search given a query const val appStringSearch = "cloudstreamsearch" diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index f0340845..e80a8fa5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -24,7 +24,6 @@ import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.LinkGenerator -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE @@ -40,6 +39,7 @@ import kotlinx.android.synthetic.main.stream_input.* import android.text.format.Formatter.formatShortFileSize import androidx.core.widget.doOnTextChanged import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.ui.player.BasicLink import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import java.net.URI @@ -225,7 +225,7 @@ class DownloadFragment : Fragment() { R.id.global_to_navigation_player, GeneratorPlayer.newInstance( LinkGenerator( - listOf(url), + listOf(BasicLink(url)), extract = true, referer = referer, isM3u8 = dialog.hls_switch?.isChecked diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index dc1bbba3..6f40e145 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -42,7 +42,7 @@ class DownloadedPlayerActivity : AppCompatActivity() { R.id.global_to_navigation_player, GeneratorPlayer.newInstance( LinkGenerator( listOf( - url + BasicLink(url) ) ) ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index 1f242481..0b560857 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -5,8 +5,15 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.utils.* import java.net.URI +/** + * Used to open the player more easily with the LinkGenerator + **/ +data class BasicLink( + val url: String, + val name: String? = null, +) class LinkGenerator( - private val links: List, + private val links: List, private val extract: Boolean = true, private val referer: String? = null, private val isM3u8: Boolean? = null @@ -47,7 +54,7 @@ class LinkGenerator( offset: Int ): Boolean { links.amap { link -> - if (!extract || !loadExtractor(link, referer, { + if (!extract || !loadExtractor(link.url, referer, { subtitleCallback(PlayerSubtitleHelper.getSubtitleData(it)) }) { callback(it to null) @@ -57,11 +64,11 @@ class LinkGenerator( callback( ExtractorLink( "", - link, - unshortenLinkSafe(link), // unshorten because it might be a raw link + link.name ?: link.url, + unshortenLinkSafe(link.url), // unshorten because it might be a raw link referer ?: "", Qualities.Unknown.value, isM3u8 ?: normalSafeApiCall { - URI(link).path?.substringAfterLast(".")?.contains("m3u") + URI(link.url).path?.substringAfterLast(".")?.contains("m3u") } ?: false ) to null ) From 5245eff6e12a781bb2e072e75d7e610252c4135d Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Sat, 18 Mar 2023 09:22:07 +0100 Subject: [PATCH 054/570] [skip ci] fix xml header being slightly wrong --- .github/locales.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/locales.py b/.github/locales.py index 04d9cd13..9ab272b9 100644 --- a/.github/locales.py +++ b/.github/locales.py @@ -56,6 +56,8 @@ for file in glob.glob(f"{XML_NAME}*/strings.xml"): if child.text.startswith("\\@string/"): print(f"[{file}] fixing {child.attrib['name']}") child.text = child.text.replace("\\@string/", "@string/") - tree.write(file, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=True) + with open(file, 'w') as fp: + fp.write('\n') + tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False) except ET.ParseError as ex: - print(f"[{file}] {ex}") \ No newline at end of file + print(f"[{file}] {ex}") From 4235c826a5f150c69af0f601e76855bbf12e9971 Mon Sep 17 00:00:00 2001 From: Lag <> Date: Sat, 18 Mar 2023 23:55:58 +0100 Subject: [PATCH 055/570] Better focus on Android TV (Thank you ocean for reporting) --- .../ui/home/HomeParentItemAdapter.kt | 2 +- .../ui/result/LinearListLayout.kt | 18 +++++++++---- .../cloudstream3/ui/result/ResultFragment.kt | 20 ++++++++++++++ .../ui/settings/SettingsAccount.kt | 22 +++++++++++++++ .../main/res/layout/fragment_result_tv.xml | 9 +++---- .../main/res/layout/homepage_parent_tv.xml | 27 +++++++++---------- 6 files changed, 71 insertions(+), 27 deletions(-) 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 e6999c9e..58c6dbe0 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 @@ -185,7 +185,7 @@ open class ParentItemAdapter( ) : RecyclerView.ViewHolder(itemView) { val title: TextView = itemView.home_child_more_info - val recyclerView: RecyclerView = itemView.home_child_recyclerview + private val recyclerView: RecyclerView = itemView.home_child_recyclerview fun update(expand: HomeViewModel.ExpandableHomepageList) { val info = expand.list diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt index 59a46264..affbcbb4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt @@ -7,13 +7,13 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.mvvm.logError fun RecyclerView?.setLinearListLayout(isHorizontal: Boolean = true) { - if(this == null) return + if (this == null) return this.layoutManager = this.context?.let { LinearListLayout(it).apply { if (isHorizontal) setHorizontal() else setVertical() } } ?: this.layoutManager } -class LinearListLayout(context: Context?) : +open class LinearListLayout(context: Context?) : LinearLayoutManager(context) { fun setHorizontal() { @@ -24,7 +24,8 @@ class LinearListLayout(context: Context?) : orientation = VERTICAL } - private fun getCorrectParent(focused: View): View? { + private fun getCorrectParent(focused: View?): View? { + if (focused == null) return null var current: View? = focused val last: ArrayList = arrayListOf(focused) while (current != null && current !is RecyclerView) { @@ -54,10 +55,17 @@ class LinearListLayout(context: Context?) : linearSmoothScroller.targetPosition = position startSmoothScroll(linearSmoothScroller) }*/ - override fun onInterceptFocusSearch(focused: View, direction: Int): View? { val dir = if (orientation == HORIZONTAL) { - if (direction == View.FOCUS_DOWN || direction == View.FOCUS_UP) return null + if (direction == View.FOCUS_DOWN || direction == View.FOCUS_UP) { + // This scrolls the recyclerview before doing focus search, which + // allows the focus search to work better. + + // Without this the recyclerview focus location on the screen + // would change when scrolling between recyclerviews. + (focused.parent as? RecyclerView)?.focusSearch(direction) + return null + } if (direction == View.FOCUS_RIGHT) 1 else -1 } else { if (direction == View.FOCUS_RIGHT || direction == View.FOCUS_LEFT) return null 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 bdef14b5..5a3e28b4 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 @@ -22,6 +22,7 @@ import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.RecyclerView import com.discord.panels.OverlappingPanelsLayout import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable @@ -531,6 +532,25 @@ open class ResultFragment : ResultTrailerPlayer() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + result_cast_items?.layoutManager = object : LinearListLayout(view.context) { + override fun onRequestChildFocus( + parent: RecyclerView, + state: RecyclerView.State, + child: View, + focused: View? + ): Boolean { + // Make the cast always focus the first visible item when focused + // from somewhere else. Otherwise it jumps to the last item. + return if (parent.focusedChild == null) { + scrollToPosition(this.findFirstCompletelyVisibleItemPosition()) + true + } else { + super.onRequestChildFocus(parent, state, child, focused) + } + } + }.apply { + this.orientation = RecyclerView.HORIZONTAL + } result_cast_items?.adapter = ActorAdaptor() updateUIListener = ::updateUI diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index f9627e46..1ef3cb55 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -157,6 +157,28 @@ class SettingsAccount : PreferenceFragmentCompat() { ) dialog.dismissSafe() } + + val displayedItems = listOf( + dialog.login_username_input, + dialog.login_email_input, + dialog.login_server_input, + dialog.login_password_input + ).filter { it.isVisible } + + displayedItems.foldRight(displayedItems.firstOrNull()) { item, previous -> + item?.id?.let { previous?.nextFocusDownId = it } + previous?.id?.let { item?.nextFocusUpId = it } + item + } + + displayedItems.firstOrNull()?.let { + dialog.create_account?.nextFocusDownId = it.id + it.nextFocusUpId = dialog.create_account.id + } + dialog.apply_btt?.id?.let { + displayedItems.lastOrNull()?.nextFocusDownId = it + } + dialog.text1?.text = api.name if (api.storesPasswordInPlainText) { diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index a29dc192..5eacdbe2 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -199,17 +199,13 @@ android:id="@+id/result_back" android:layout_width="30dp" android:layout_height="30dp" - android:layout_gravity="center_vertical" android:layout_marginEnd="10dp" - android:background="?android:attr/selectableItemBackgroundBorderless" android:clickable="true" android:contentDescription="@string/go_back" - android:focusable="true" android:gravity="center_vertical" - android:nextFocusDown="@id/result_description" android:src="@drawable/ic_baseline_arrow_back_24" app:tint="?attr/white" /> @@ -385,8 +381,8 @@ @@ -423,11 +419,11 @@ @@ -568,6 +564,7 @@ android:layout_weight="1" android:minWidth="250dp" android:nextFocusLeft="@id/result_movie_progress_downloaded_holder" + android:nextFocusRight="@id/result_bookmark_button" android:nextFocusDown="@id/result_resume_series_button_play" android:text="@string/type_none" android:visibility="visible" /> diff --git a/app/src/main/res/layout/homepage_parent_tv.xml b/app/src/main/res/layout/homepage_parent_tv.xml index d0c88c39..9dcf0bae 100644 --- a/app/src/main/res/layout/homepage_parent_tv.xml +++ b/app/src/main/res/layout/homepage_parent_tv.xml @@ -2,33 +2,30 @@ + android:layout_height="wrap_content" + android:orientation="vertical"> \ No newline at end of file From 0cbee7068326a8f215f53c45d9c85d3601eac468 Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Sun, 19 Mar 2023 12:51:54 +0100 Subject: [PATCH 056/570] [skip ci] Update locales.py --- .github/locales.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/locales.py b/.github/locales.py index 9ab272b9..7d6d6b90 100644 --- a/.github/locales.py +++ b/.github/locales.py @@ -56,8 +56,8 @@ for file in glob.glob(f"{XML_NAME}*/strings.xml"): if child.text.startswith("\\@string/"): print(f"[{file}] fixing {child.attrib['name']}") child.text = child.text.replace("\\@string/", "@string/") - with open(file, 'w') as fp: - fp.write('\n') + with open(file, 'wb') as fp: + fp.write(b'\n') tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False) except ET.ParseError as ex: print(f"[{file}] {ex}") From 52d495f425fa3a305a2c4018c36b93e9542751b5 Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Tue, 21 Mar 2023 20:50:13 +0000 Subject: [PATCH 057/570] Update README.md --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 3430d626..e3d033ba 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,7 @@ + Download and stream movies, tv-shows and anime + Chromecast -### Screenshots: - - - - ### Supported languages: Translation status - \ No newline at end of file + From 67b0549fd2a3fe4b94d0a6f03f490bfa8956258e Mon Sep 17 00:00:00 2001 From: LagradOst <46196380+Blatzar@users.noreply.github.com> Date: Tue, 21 Mar 2023 21:01:47 +0000 Subject: [PATCH 058/570] remove images --- .github/downloads.jpg | Bin 59461 -> 0 bytes .github/home.jpg | Bin 139384 -> 0 bytes .github/player.jpg | Bin 49418 -> 0 bytes .github/results.jpg | Bin 98562 -> 0 bytes .github/search.jpg | Bin 152135 -> 0 bytes 5 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .github/downloads.jpg delete mode 100644 .github/home.jpg delete mode 100644 .github/player.jpg delete mode 100644 .github/results.jpg delete mode 100644 .github/search.jpg diff --git a/.github/downloads.jpg b/.github/downloads.jpg deleted file mode 100644 index ca14a664a2ce2b07cdc366343d54c690d9bacc01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59461 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ℜ5F2>W2mS%`!tt>Y2ggw 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

6R?OyVHq`Nv6g|; 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 T_iJ8Mtj&4^JWxLkp~e3JSo6U! diff --git a/.github/search.jpg b/.github/search.jpg deleted file mode 100644 index 784bec892550387282d3551e7ed8d830aa0a1cd8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 152135 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 !Xcj4QAvn?KT(Ckvd+4 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ξ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 Date: Wed, 29 Mar 2023 10:12:10 +0700 Subject: [PATCH 059/570] fixed streamsb --- .../main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt index cac31328..993ef156 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt @@ -134,7 +134,7 @@ open class StreamSB : ExtractorApi() { it.value.replace(Regex("(embed-|/e/)"), "") }.first() // val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362" - val master = "$mainUrl/sources15/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/" + val master = "$mainUrl/sources16/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/" val headers = mapOf( "watchsb" to "sbstream", ) From 7317278f57ead97ddc643dc8e95edf647c24d66f Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 29 Mar 2023 14:14:59 +0200 Subject: [PATCH 060/570] Translated using Weblate (Macedonian) Currently translated at 97.7% (596 of 610 strings) Translated using Weblate (Dutch) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Romanian) Currently translated at 71.4% (436 of 610 strings) Translated using Weblate (Dutch) Currently translated at 90.1% (550 of 610 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Croatian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Italian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Czech) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Hungarian) Currently translated at 83.9% (512 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Polish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (English) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Polish) Currently translated at 99.6% (608 of 610 strings) Translated using Weblate (Italian) Currently translated at 99.6% (608 of 610 strings) Translated using Weblate (English) Currently translated at 100.0% (610 of 610 strings) Update translation files Updated by "Remove blank strings" hook in Weblate. Translated using Weblate (Hungarian) Currently translated at 60.9% (372 of 610 strings) Co-authored-by: Alexandru Co-authored-by: Alexthegib Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> Co-authored-by: Clxff Heraldo <123844876+clxf12@users.noreply.github.com> Co-authored-by: Dan Co-authored-by: FastAct Co-authored-by: Fjuro Co-authored-by: Hosted Weblate Co-authored-by: Massimo Pissarello Co-authored-by: Milo Ivir Co-authored-by: Rex_sa Co-authored-by: ZsoltiHUB Co-authored-by: gallegonovato Co-authored-by: stojkovskistefan Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ro/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translation: Cloudstream/App --- app/src/main/res/values-ar/strings.xml | 6 +- app/src/main/res/values-cs/strings.xml | 11 +- app/src/main/res/values-es/strings.xml | 8 +- app/src/main/res/values-hr/strings.xml | 11 +- app/src/main/res/values-hu/strings.xml | 230 +++++++++++++++++- app/src/main/res/values-in/strings.xml | 11 +- app/src/main/res/values-it/strings.xml | 19 +- app/src/main/res/values-mk/strings.xml | 324 ++++++++++++++++++++++++- app/src/main/res/values-nl/strings.xml | 188 ++++++++++++-- app/src/main/res/values-pl/strings.xml | 21 +- app/src/main/res/values-pt/strings.xml | 8 +- app/src/main/res/values-ro/strings.xml | 9 +- app/src/main/res/values-uk/strings.xml | 8 +- app/src/main/res/values/strings.xml | 4 +- 14 files changed, 760 insertions(+), 98 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 2a356812..d7fecfd1 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1,4 +1,4 @@ - + ملصق @@ -284,7 +284,7 @@ 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. عام زر العشوائي - يظهر الزر على الصفحة الرئيسية والذي يمكنه اختيار فيلم عشوائي أو مسلسل تلفزيوني من الصفحة الرئيسية + إظهار زر العشوائي على الصفحة الرئيسية لغات المزود واجهة التطبيق المحتوى المفضل @@ -561,4 +561,4 @@ باستخدام jsdelivr ، يمكن تجاوز حظر GitHub. قد يؤخر التحديثات لبضعة أيام. وكيل raw.githubusercontent.com جودة المشاهدة المفضلة (بيانات الجوال) - + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1501a5d9..1dc2ebce 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -1,6 +1,5 @@ - - - + + %s Ep %d @@ -267,7 +266,7 @@ 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 tlačítko, kterým lze vybrat náhodný film nebo seriál z domovské stránky + Zobrazit na domovské stránce náhodné tlačítko Jazyk poskytovatelů Rozložení aplikace Preferovaná média @@ -552,6 +551,6 @@ Nepodařilo se připojit ke GitHubu, povolování proxy jsdelivr. Upřednostněná kvalita sledování (mobilní data) Vrátit zpět - Pomocí jsdelivr lze obejít blokování GitHubu. Může dojít ke zpoždění aktualizací o několik dní. + Obchází blokování GitHubu pomocí jsdelivr, může způsobit zpoždění aktualizací o několik dní. Obcházení ISP - + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 06c20aa5..289de2a1 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,4 +1,4 @@ - + Extensiones Descargue la lista de sitios que quiera utilizar @@ -306,7 +306,7 @@ Aspecto Características Botón de Al azar - Muestra un botón de reproducción \"al azar\" en la página de inicio para poelículas y series + Mostrar el botón aleatorio en la página de inicio cuenta Cerrar sesión Cambiar cuenta @@ -525,8 +525,8 @@ ¡Episodio %d publicado! Proxy raw.githubusercontent.com No se ha podido acceder a GitHub, activando el proxy jsdelivr. - Con jsdelivr, se puede omitir el bloqueo de GitHub. Puede retrasar las actualizaciones unos días. + Omite el bloqueo de GitHub mediante jsdelivr, lo que puede provocar que las actualizaciones se retrasen unos días. Revertir ISP Bypasses Calidad de visualización preferida (Datos móviles) - + \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index b4931377..e38a6225 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -1,6 +1,5 @@ - - - + + %d %s | %s %s • %s @@ -300,7 +299,7 @@ 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 - Prikazuje gumb na početnoj stranici koji može odabrati nasumični film ili TV seriju s početne stranice + Prikaži gumb za slučajni odabir reprodukcija na početnoj stranici Jezici pružatelja usluga Izgled aplikacije Preferirani mediji @@ -553,6 +552,6 @@ ISP zaobilaznice raw.githubusercontent.com Proxy Neuspješno dohvaćanje GitHuba, omogućavanje jsdelivr proxyja. - Koristeći jsdelivr, GitHub blokiranje se može zaobići. Može odgoditi ažuriranja za nekoliko dana. + Zaobilazi GitHub blokiranje koristeći jsdelivr. Može odgoditi ažuriranja za nekoliko dana. Preferirana kvaliteta gledanja (podatkovna mobilna mreža) - + \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 1389dff0..396c514b 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -1,4 +1,4 @@ - + Stáblista: %s %dn %dó%dp @@ -101,11 +101,11 @@ Körvonal szín Háttér szín Ablak szín - Edge típus + Él típusa Betűtípus Keresés típusok szerint Keresés szolgáltatók szerint - %d Banán a fejlesztőknek + %d Banán adva a fejlesztőknek Nyelvek letöltése Tartsa lenyomva az alapértelmezett érték visszaállításához Betűtípusok importálása %s @@ -269,10 +269,230 @@ Forrás Bevezető intro átugrása Ne mutasd újra - Az %d epizód ekkor jelenik meg: + A(z) %d epizód ekkor jelenik meg: Szüneteltetve Elvetve Minőségi jelzés Szinkroncímke Alcímke - + Műveletek + Random gomb + DNS HTTPS-en keresztül + Böngésző + Android TV + kézmozdulatok + frissítés kihagyása + Alkalmazásfrissítések + Szolgáltatók + Funkciók + Előnyben részesített videóminőség (mobilinternet) + Videolejátszó cím max karakterek + Nem sikerült elérni a GitHubot, a jsdelivr proxy engedélyezése. + Bővítmények + Általános + Felirat kódolása + Elsődleges szín + Alkalmazástéma + Szolgáltató teszt + Sikertelen + Problémákat okoz, ha túl magasra van állítva az alacsony tárhellyel rendelkező eszközökön, például az Android TV-n. + Korhatáros tartalmak engedélyezése a támogatott szolgáltatóknál + Elrendezés + raw.githubusercontent.com Proxy + A lejátszó funkciói + Előnyben részesített videóminőség (WiFi-n) + Hasznos az internetszolgáltató blokkjainak megkerüléséhez + Elrendezés + Sikerült + NGINX szerver URL-címe + Szinkronizált/feliratozott animék megjelenítése + Alapértelmezettek + Megjelenít egy gombot a Kezdőlapon, amely egy véletlenszerű filmet vagy TV sorozatot választ a Kezdőlapról + Letöltési útvonal + Gyorsítótár + Szolgáltatók nyelvei + Napló + Könyvtár + internetszolgáltató-kikerülések + Videó buffer méret + Videó buffer hossza + Videolejátszó felbontása + Videó gyorsítótár a lemezen + Biztonsági mentés + Feliratok + Előnyben részesített média + Hivatkozások + Videó és kép gyorsítótár törlése + A jsdelivr használatával a GitHub blokkolása megkerülhető. Néhány nappal késleltetheti a frissítéseket. + Összeomlást okoz, ha túl magasra van állítva a kevés memóriával rendelkező eszközökön, például az Android TV-n. + Betöltés az internetről + Videósávok + Alkalmazás újraindításkor + Az összes bővítményt kikapcsoltuk egy összeomlás miatt, hogy segítsünk megtalálni a problémát okozót. + Szerzők + Támogatott + Alkalmazásfrissítés letöltése… + Frissítve (újabbtól a régebbihez) + Úgy tűnik, a könyvtárad üres :( +\nJelentkezz be egy könyvtár fiókba, vagy adj hozzá műsorokat a helyi könyvtárodhoz + Úgy tűnik, ez a lista üres, próbálj meg egy másikra váltani + Max + 4K + SDR + Fiók létrehozása + pelda.com + Feliratok szinkronizálása + Alkalmazásfrissítés telepítése… + Túl sok szöveg. Nem lehet a vágólapra menteni. + bővítmény + Nincs felirat késleltetés + Leírás + Frissítés + /\?\? + Árnyék + Filmelőzetes + Mit szeretnél látni + Minden %s már letöltött + Először telepítse a bővítményt + Webböngésző + Kinézet + Alkalmazás elrendezés + Szinkronizálás + Nem sikerült bejelentkezni a következőként: %s + Min + 1000 ms + Ajánlott + + Érvénytelen adatok + Link a streamhez + Nem sikerült betölteni: %s + Elkezdődött a(z) %d %s letöltése… + Töltse le az összes bővítményt ebből a tárolóból\? + Biztonságos mód bekapcsolva + Méret + MPV + Alkalmazás nem található + PackageInstaller + Rendezés e szerint: + Feliratkozott a következőre: %s + MenőWeboldalam + DVD + %d plugin frissítve + Értékelés: %s + Előzmények törlése + Nem + Feliratkozva + Használd ezt, ha a feliratok %d ms-sel korábban jelennek meg. + Lejátszó + Felbontás és cím + Előnyben részesített videolejátszó + Értékelés (alacsonyabbtól a magasabbig) + Felirat késleltetése + Blu-ray + Érvénytelen azonosító + Videók megtekintése ezeken a nyelveken + Előző + %d%s letöltve + Batch letöltés + bővítmények + Legacy + Értékelés (magasabbtól az alacsonyig) + Feliratkozott műsorok frissítése + Megjelent a(z) %d epizód! + SD + Nyelvkód (hu) + /%d + Emelt + HD + HLS lejátszási lista + VLC + Nem sikerült telepíteni az alkalmazás új verzióját + %s hitelesítve + Körvonal + Betöltés fájlból + HDR + Az alkalmazás megjelenésének módosítása, hogy az megfeleljen az eszközödnek + Összeomlás jelentése + Nyilvános lista + Állapot + Összefoglaló + %d / 10 + Megnyitás a következővel + Minden felirat nagybetűs + Intro + Leiratkozott a következőről: %s + Bloat eltávolítása a feliratokról + Szűrés előnyben részesített médianyelv szerint + Biztos vagy benne, hogy ki akarsz lépni\? + Rendezés + Visszaállít + Érvénytelen URL + Zárt feliratok eltávolítása a feliratokból + 18+ + Ez az összes tároló bővítményt is törli + A CloudStream alapértelmezés szerint nem telepített webhelyeket. A webhelyeket a tárolókból kell telepítenie. +\n +\nA Sky UK Limited agyatlan DMCA letiltása miatt 🤮 nem tudjuk az alkalmazásban linkelni az adattár oldalát. +\n +\nCsatlakozz a Discordunkhoz vagy keress online. + Verzió + Megjelölés megtekintettként + Eltávolítás a megnézettek közül + Web + Következő + UHD + Felbontás + Újraindítás + Stop + Nincs letöltve: %d + Hiba + Webhely eltávolítása + hello@vilag.com + Töltse le a használni kívánt webhelyek listáját + Közösségi tárolók megtekintése + %s (Letiltva) + Hangsávok + Összeomlási információk megtekintése + Belső lejátszó + Minden nyelv + %s hozzáadva + Bővítmény törölve + Nyelv + Fiók + Extrák + Tároló törlése + %d letiltva + Igen + Az alkalmazás kilépéskor frissül + Betűrendben (A-tól a Z-ig) + Frissítve (régebbitől az újabbig) + jelszó123 + AzÉnMenőFelhasználónevem + 127.0.0.1 + Fiókváltás + Fiók hozzáadása + Árvíztűrő tükörfúrógép + Letöltött fájl + Támogató + Háttér + Forrás + Lepj meg + Hamarosan… + Kész + Bővítmények + Tároló hozzáadása + Tároló neve + Tárhely URL címe + Bővítmény betöltve + Bővítmény letöltve + Közreműködők + Betűrendben (Z-től az A-ig) + Könyvtár kiválasztása + Biztonságos módú fájl található! +\nNem tölt be semmilyen kiterjesztést indításkor, amíg a fájl el nem lesz távolítva. + Normál + %s betöltve + Beállítás kihagyása + HQ + %d letöltve + \ 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 02234c49..e6da6195 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -1,6 +1,5 @@ - - - + + %s Ep %d Pemeran: %s @@ -265,7 +264,7 @@ 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 di halaman utama yang dapat memilih seri film atau TV acak dari halaman utama + Tampilkan tombol acak di Beranda Bahasa provider Tata Letak Aplikasi Media yang lebih diinginkan @@ -549,8 +548,8 @@ Episode %d telah rilis! raw.githubusercontent.com Proksi Gagal mencapai GitHub, mengaktifkan proksi jsdelivr. - Mengunakan jsdelivers, bisa melewati pemblokiran GitHub. Mungkin dapat menyebabkan pembaruan tertunda dalam beberapa hari. + Bisa melewati pemblokiran GitHub mengunakan jsdelivers. Mungkin dapat menyebabkan tertunda dalam beberapa hari. Bypass ISP Pulihkan Nonton dengan kualitas yang di inginkan (Data Seluler) - + \ 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 eca60da1..52a354c7 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,6 +1,5 @@ - - - + + %s Ep %d Cast: %s @@ -128,16 +127,16 @@ Modalità Eigengravy Aggiungi opzione velocità nel player Scorri per mandare avanti/indietro - Scorri a sinistra o a destra per controllare il tempo del video + Scorri da un lato all\'altro per controllare la tua posizione in un video Scorri per cambiare le impostazioni - Passa il dito sul lato sinistro o destro per cambiare la luminosità o il volume + Scorri verso l\'alto o verso il basso sul lato sinistro o destro per modificare la luminosità o il volume Riproduci automaticamente l\'episodio successivo Avvia l\'episodio successivo al termine di quello in corso Doppio tocco per andare avanti/indietro Doppio tocco per mettere in pausa Tocca due volte il lato destro o sinistro dello schermo per mandare avanti o indietro il video - Tocca due volte il centro dello schermo per mettere in pausa il video - Player seek + Tocca due volte al centro per mettere in pausa + Intervallo di ricerca lettore (secondi) Utilizzare la luminosità del sistema Utilizzare la luminosità del sistema al posto di una sovrapposizione scura @@ -163,7 +162,7 @@ Nascondi la qualità video selezionata dai risultati di ricerca Aggiorna automaticamente i plugin Mostra gli aggiornamenti dell\'app - Cerca automaticamente nuovi aggiornamenti all\'avvio + Cerca automaticamente nuovi aggiornamenti dopo aver avviato l\'app. Aggiorna alle prerelease Cerca per aggiornamenti alle prerelease invice di cercare solo le release complete GitHub @@ -546,10 +545,10 @@ Iscritto Iscritto a %s Impossibile contattare GitHub, abilitazione proxy jsdelivr avviata. - Bypassa il blocco di GitHub utilizzando jsdelivr, potrebbe causare un ritardo di alcuni giorni. + Ignora il blocco di GitHub utilizzando jsdelivr, può causare il ritardo degli aggiornamenti di alcuni giorni. Baypass ISP Ripristina Aggiornando shows a cui sei iscritto L\'episodio %d è stato rilasciato! Qualità di visualizzazione preferita (Dati mobili) - + \ 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 811a09c5..29fd4be8 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -1,6 +1,5 @@ - - - + + Брзина (%.2fx) Оценето: %.1f @@ -95,9 +94,9 @@ Режим на Eigengravy Додава можност за брзина на снимка во плеерот Повлечете за да барате - Повлечете лево или десно за да го контролирате времето во видеоплеерот + Повлечете од страна на страна за да ја контролирате вашата позиција во видеото Повлечете за да ги промените поставките - Повлечете на левата или десната страна за да ја промените осветленоста или јачината на звукот + Лизгајте нагоре или надолу на левата или десната страна за да ја промените осветленоста или јачината на звукот Допрете двапати за да барате Допрете двапати на десната или левата страна за да барате напред или назад Користете ја осветленоста на системот @@ -110,7 +109,7 @@ Не испраќа податоци Прикажи епизода за полнење за аниме Прикажи ажурирања на апликации - Автоматски пребарувајте нови ажурирања на вклучување на апликацијата + Автоматски пребарувајте нови ажурирања откако ќе ја стартувате апликацијата. Ажурирање на пред официјални верзии Пребарајте пред официјални верзии наместо ажурирања на само официјални верзии Github @@ -179,7 +178,7 @@ Прескокни ОП Не прикажувај повторно Ажурирај - Префериран квалитет на гледање + Префериран квалитет на гледање (WiFi) DNS преку HTTPS Корисно за заобиколување на блоковите на интернет провајдерите Патека на превземање @@ -199,8 +198,8 @@ Тема на апликацијата %s %s Корисничко име - Одјавување - Логирај Се + Одјави се + Најавете се Промени корисничка сметка Додади корисничка сметка @@ -214,4 +213,309 @@ Сенка Подигнат Историја - + Голема буква на сите преводи + Автоматски инсталирајте ги сите сè уште неинсталирани приклучоци од додадените складишта. + %d-%d + %d %s + Пренос во живо + NSFW + Други + Отстранете ја страницата + password123 + Јазичен код (мк) + Одложување на титловите + Вчитан %s + Извор + Случајно + Грешка + Прикажи складишта на заедницата + Прескокнете го ова ажурирање + Групно преземање + Позадина + Документарни филмови + Следно + URL на серверот NGINX + Стоп + Подознака + Изглед на емулатор + Видео + Исчисти + Положен + MyCoolSite + Неважечки податоци + Поддршка + Функции на плеерот + Серија + Сите %s веќе се преземени + Дејства + Јазик на преводот + Опис + Апликацијата ќе се ажурира по излегувањето + Отпишана е од %s + прокси raw.githubusercontent.com + TC + Претплатен на %s + Преводи + Да се преземат сите приклучоци од ова складиште\? + Недостасуваат дозволи за складирање. Обидете се повторно. + Зачувај + Вчитај од датотека + Ажурирања на апликацијата + Прелистувач + Вчитана резервна датотека + Гестови + Двоен допир за да паузирате + Прескокни %s + Најдена е датотека во безбеден режим! +\nНе се вчитуваат екстензии при стартување додека датотеката не се отстрани. + Врати + Подреди + Внатрешен плеер + Резолуција + Кредити + Пребарај %s… + Приклучокот е избришан + Статус + Автори + Започни + Изглед + Без доцнење на титловите + Ажурирање претплатени емисии + Синхронизирај + Вчитај од Интернет + %s (оневозможено) + SD + Затвори + Наскоро… + Верзија + Ознака за квалитет + приклучок + %d / 10 + Гледајте видеа на овие јазици + Прво инсталирајте ја наставката + Ажурирајте го напредокот на часовникот + Библиотека + Износот на барањето што се користи кога плеерот е скриен + Преземи преводи + Јавна листа + MPV + Инсталатор на пакети + ОВА + Ажурирања и резервни копии + Изгледа дека вашата библиотека е празна :( +\nНајавете се на сметка на библиотеката или додајте серии во вашата локална библиотека + Не се пронајдени епизоди + Брзата кафеава лисица го прескокнува мрзливото куче + Слика на постер + Должина на видео баферот + Избриши складиште + Клонирајте ја страницата + Ставете го насловот под постерот + Прикажи информации за падот на апликацијата + Јазик + Торент + Скриен плеер - Износ за пребарување + Автоматски синхронизирајте го напредокот на вашата тековна епизода + Бајпас на интернет провајдерот + Препорачано + Наслов + Префериран квалитет на гледање (мобилни податоци) + Тест на провајдер + Изберете библиотека + Видео песни + Азиски драми + Приклучокот е вчитан + Remove bloat from subtitles + Не е преземено: %d + Remove closed captions from subtitles + Аудио песни + Вклучете ги елементите на корисничкиот интерфејс на постерот + Оневозможено: %d + Легаси + Автоматска репродукција на следната епизода + Исчистете го кешот на видео и слики + Карактеристики + Азиска драма + Додатоци + Се прикажува копче на почетната страница што може да избере случаен филм или ТВ серија од почетната страница + Поддржано + Сметки + Вовед + Креирај сметка + Отстрани од гледаното + Допрете двапати во средината за да паузирате + Резервна копија + ОВА + Пренос во живо + Web + Ажурирани %d приклучоци + Мешано отворање + Екстензии + Овозможете NSFW на поддржани провајдери + Не успеа да стигне до GitHub, овозможувајќи jsdelivr прокси. + Филтрирајте по претпочитан медиумски јазик + \@string/home_play + Филм + Додаден %s + приклучоци + Подреди по + Изгледа дека оваа листа е празна, обидете се да се префрлите на друга + Аниме + Износот на барањето што се користи кога плеерот е видлив + Dub + Автоматско преземање приклучоци + Главна + Кеш меморија + Трејлер + Не можев да се најавам на %s + Премногу текст. Не може да се зачува во таблата со исечоци. + Неважечки ID + Преземено %d %s + Користете го ова ако преводите се прикажани %d ms премногу рано + Неважечка URL адреса + Безбедниот режим е вклучен + Blu-ray + Зачувани податоци + Предизвикува падови ако е превисоко поставено на уреди со слаба меморија, како што е Android TV. + Се инсталира ажурирање на апликацијата… + URL на складиштето + Не може да се инсталира новата верзија на апликацијата + Прикажи постери од Kitsu + Дали сте сигурни дека сакате да излезете\? + Предизвикува проблеми ако е превисоко поставено на уреди со мал простор за складирање, како што е Android TV. + Користејќи jsdelivr, блокирањето на GitHub може да се заобиколи. Може да ги одложи ажурирањата за неколку дена. + Да + Азбучно (Ш до А) + WP + Додајте клон на постоечка локација, со различна URL адреса + Преземена датотека + Износ на бараниот плеер (секунди) + Прикажи Logcat 🐈 + Преземено: %d + Резолуција и наслов + Ажурирањето започна + Преводи на Chromecast + Користете го ова ако преводите се прикажуваат %d ms премногу доцна + Случајно копче + Инсталатор на APK + Екстензии + UHD + Референт + Се отвора + 127.0.0.1 + Ова исто така ќе се избрише сите приклучоци за складиште + Направете резервна копија од податоците + Етикета за Dub + Прикажан плеер - Барај износ + Андроид ТВ + Не успеа да ги врати податоците од датотеката %s + Не успеа + Документарец + Стрим + %d мин + Играј со CloudStream + Пушти трејлер + Поставки за преводи на Chromecast + Наслов + Копирај + Плеер + Претплатени + 1000 ms + NSFW + /%d + /\?\? + hello@world.com + +30 + VLC + Рестартирај + Цртан филм + Почна да презема %d %s… + Автоматски ажурирања на приклучоци + -30 + %dm +\nпреостанува + Видео кеш на дискот + Поврзување до пренос + Готово + Додај складиште + 18+ + ХЛС плејлиста + Префериран видео плеер + Прикажи трејлери + Енкодирање на превод + Изглед + Додајте тракинг + Оценет + Камера + Камера + SDR + Веб-прелистувач + Апликацијата не е пронајдена + MyCoolUsername + Отвори со + %s %d%s + Повторете го процесот на поставување + Линкови + Повторување + Sub + Log + Започнете ја следната епизода кога ќе заврши тековната + Грешка при правење резервна копија на %s + Сокриј го избраниот квалитет на видеото во резултатите од пребарувањето + Некои телефони не го поддржуваат новиот инсталатор на пакети. Испробајте ја наследната опција ако ажурирањата не се инсталираат. + Резолуција на видео плеер + Големина на видео баферот + Распоред + Стандардно + Провајдери + Локација на насловот на постерот + HQ + HD + TS + 4K + Претходно + Прескокнете го поставувањето + Променете го изгледот на апликацијата за да одговара на вашиот уред + Известување за пад + Што сакате да видите + Име на складиштето + Приклучокот е преземен + Не може да се вчита %s + Преземете ја листата на сајтови што сакате да ги користите + CloudStream нема стандардно инсталирани локации. Треба да ги инсталирате сајтовите од складиштата. +\n +\nПоради отстранување на DMCA без мозок од страна на Sky UK Limited 🤮 не можеме да ја поврземе локацијата на складиштето во апликацијата. +\n +\nПридружете се на нашиот Discord или барајте онлајн. + Песни + Сите екстензии беа исклучени поради пад за да ви помогнат да ја пронајдете онаа што предизвикува проблеми. + Оцена: %s + Големина + Веб-видео Cast + Сите јазици + Исчисти историја + Обележи како гледано + Прикажи скокачки прозорци за отворање/завршување + Не + Се презема ажурирање на апликацијата… + Оцена (висока до ниска) + Оцена (ниска до висока) + Ажурирано (ново на старо) + Ажурирано (старо во ново) + Азбучно (А до Ш) + Епизодата %d е објавена! + Камера + DVD + Завршува + Измешан крај + HDR + example.com + Синхронизирај преводи + Примени при рестартирање + Наслов на видео плеер максимални знаци + Увезете фонтови ставајќи ги во %s + Врати ги податоците од резервна копија + Поставете статус на пратење + Пушти Livestream + %s е автентициран + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 766bcdc7..792f37e7 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1,6 +1,5 @@ - - - + + %s Ep %d Cast: %s @@ -131,7 +130,7 @@ Swipe to seek Veeg naar links of rechts om de tijd in de videospeler te regelen Veeg om instellingen te wijzigen - Veeg naar links of rechts om de helderheid of het volume te wijzigen + Veeg omhoog of omlaag aan de linker- of rechterkant om de helderheid of het volume te wijzigen Dubbeltik om te zien Dubbeltik om te pauzeren Videospeler aantal zoeken @@ -142,7 +141,7 @@ Kijkvoortgang bijwerken Automatisch synchroniseren van je huidige episode vooruitgang Gegevens herstellen vanaf back-up - Back-up gegevens + Back up gegevens Geladen back-up bestand Kan gegevens uit bestand niet herstellen %s De gegevens zijn opgeslagen @@ -160,7 +159,7 @@ Toon trailers Toon posters van Kitsu App-updates tonen - Automatisch zoeken naar nieuwe updates bij het opstarten + Automatisch zoeken naar nieuwe updates na het starten van de app. Update naar pre-releases Zoeken naar pre-release updates in plaats van alleen volledige releases Github @@ -223,8 +222,8 @@ Film Serie Tekenfilm - @string/anime - @string/ova + Anime + OVA Torrent Documentaire Aziatisch drama @@ -258,22 +257,22 @@ Niet meer weergeven Deze update overslaan Update - Gewenste kijkwaliteit + Voorkeurskwaliteit voor kijken (WiFi) Maximaal aantal tekens voor titel van videospeler Videospeler Resolutie Grootte videobuffer Lengte videobuffer Video cache op schijf Wis video en beeld cache - Zal willekeurige crashes veroorzaken als deze te hoog is ingesteld. Verander niet als je weinig RAM hebt, zoals een Android TV of een oude telefoon - Kan problemen veroorzaken op systemen met weinig opslagruimte, zoals Android TV-apparaten als u deze te hoog instelt + Veroorzaakt storingen indien te hoog ingesteld op toestellen met weinig geheugen, zoals Android TV. + Veroorzaakt problemen indien te hoog ingesteld op toestellen met weinig opslagruimte, zoals Android TV. DNS over HTTPS Handig om ISP-blokkades te omzeilen Kloon site Site verwijderen Voeg een kloon toe van een bestaande site, met een andere URL Downloadpad - Nginx server url + NGINX server URL Weergave Dubbed/Subbed Anime Pas aan het scherm Uitgerekt @@ -313,8 +312,8 @@ --> %s %s account - Logout - Login + Log uit + Log in Wissel account Account toevoegen Maak account @@ -325,8 +324,8 @@ %d / 10 /\?\? /%d - Geauthenticeerd %s - Mislukt om te verifiëren aan %s + %s geverifieerd + Kon niet inloggen op %s Geen normaal @@ -338,10 +337,10 @@ Schaduw Verhoogd Sync subs - 1000ms + 1000 ms Subtitle vertraging - Gebruik dit als de ondertitels %dms te vroeg worden getoond - Gebruik dit als ondertitels %dms te laat worden getoond + Gebruik dit als de ondertitels %d ms te vroeg worden getoond + Gebruik dit als de ondertitels %d ms te laat worden getoond Geen ondertitelvertragin - + + Prędkość (%.2fx) Ocena: %.1f Znaleziono nową aktualizację! @@ -118,16 +117,16 @@ Tryb Eigengravy Ustawienia prędkości Przesuń aby przewinąć - Przesuń w lewo lub prawo aby kontrolować czas + Przesuwaj w lewo lub prawo, aby kontrolować czas filmu Przesuń aby zmienić ustawienia - Przesuń góra-dół z lewej lub prawej aby zmienić jasność i głośność + Przesuwaj góra-dół z lewej lub prawej strony ekranu aby zmienić jasność czy głośność Autoodtwarzanie następnego odcinka Rozpocznij następny odcinek po skończeniu bieżącego - Czas przewinięcia przy podwójnym kliknięciu + Czas przewinięcia przy podwójnym kliknięciu (w sekundach) 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 + Kliknij dwukrotnie 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 @@ -154,7 +153,7 @@ Automatyczne aktualizacje rozszerzeń Automatyczne pobieranie rozszerzeń Pokazuj aktualizacje - Automatycznie wyszukuj aktualizacje przy starcie + Automatycznie wyszukuj aktualizacje przy starcie. Aktualizuj do wersji beta Wyszukuj wersji beta, zamiast oficjalnych wydań Github @@ -456,7 +455,7 @@ Instalator APK Niektóre telefony nie obsługują nowego instalatora pakietów. Wypróbuj tryb legacy, jeśli aktualizacje nie zostaną zainstalowane. password123 - @string/ova + \@string/ova MojaFajnaWitryna MyCoolUsername 127.0.0.1 @@ -466,7 +465,7 @@ Instalator pakietów @string/home_play hello@world.com - @string/anime + \@string/anime Opening Ending Mixed opening @@ -533,4 +532,4 @@ Obchodzi blokadę GitHuba za pomocą jsdelivr, może spowodować opóźnienie aktualizacji o kilka dni. Nie udało się połączyć z GitHub, włączono serwer pośredniczący jsdelivr. Domyślna jakość (dane mobilne) - + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index dd722f62..44615934 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,4 +1,4 @@ - + %s Ep %d %dh %dm @@ -273,7 +273,7 @@ Aviso Legal Geral Botão Aleatório - Mostra o botão Aleatório na página inicial, que pode escolher aleatoriamente um filme ou série + Mostrar botão aleatório na página inicial Idioma dos fornecedores Layout da App Mídia preferida @@ -444,7 +444,7 @@ Cam Abertura Selecionar Biblioteca - Usando jsdelivr o bloqueio do GitHub pode ser contornado. Pode atrasar atualizações em alguns dias. + Ignora o bloqueio do GitHub usando jsdelivr, pode fazer com que as atualizações sejam atrasadas em alguns dias. VLC Todas as linguagens Atualizado (Novo para Antigo) @@ -529,4 +529,4 @@ Configurações padrão SD Faixas de áudio - + \ 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 aa443783..97896b92 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1,6 +1,5 @@ - - - + + %s Ep %d Distribuție: %s @@ -389,4 +388,6 @@ Log Browser Joacă cu CloudStream - + Actualizare plugin automată + Descarcă plugin-uri automat + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index bd062394..2f5e0cb8 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1,4 +1,4 @@ - + Постер Постер до епізоду @@ -283,7 +283,7 @@ Особливості Загальне Випадкова кнопка - Показує кнопку на Головній сторінці, яка може вибрати випадковий фільм або серіал на Головній сторінці + Показати випадкову кнопку на Головній сторінці Мови постачальника Макет програми Бажані медіа @@ -527,6 +527,6 @@ raw.githubusercontent.com Proxy Не вдалося зв\'язатися з GitHub, увімкнувши проксі-сервер jsdelivr. Обходи ISP - За допомогою jsdelivr можна обійти блокування GitHub. Можлива затримка оновлень на кілька днів. + Обхід блокування GitHub за допомогою jsdelivr, може призвести до затримки оновлень на кілька днів. Бажана якість перегляду (Мобільні дані) - + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 911c0d07..ac76e243 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -383,7 +383,7 @@ Useful for bypassing ISP blocks raw.githubusercontent.com Proxy Failed to reach GitHub, enabling jsdelivr proxy. - Using jsdelivr, GitHub blocking can be bypassed. May delay updates by a few days. + Bypasses blocking of GitHub using jsdelivr, may cause updates to be delayed by few days. Clone site Remove site Add a clone of an existing site, with a different URL @@ -428,7 +428,7 @@ Features General Random Button - Shows button on Homepage which can choose a random movie or TV series from the Homepage + Show random button on Homepage Provider languages App Layout Preferred media From 4ed65f8e07533a2bd3209fcac5498e65f7383e00 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Wed, 29 Mar 2023 13:27:53 +0000 Subject: [PATCH 061/570] chore(locales): fix locale issues --- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-bg/strings.xml | 2 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 5 +++-- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-el/strings.xml | 2 +- app/src/main/res/values-eo/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-hi/strings.xml | 2 +- app/src/main/res/values-hr/strings.xml | 5 +++-- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 5 +++-- app/src/main/res/values-it/strings.xml | 5 +++-- app/src/main/res/values-iw/strings.xml | 2 +- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-kn/strings.xml | 2 +- app/src/main/res/values-mk/strings.xml | 7 ++++--- app/src/main/res/values-ml/strings.xml | 2 +- app/src/main/res/values-ms/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 5 +++-- app/src/main/res/values-nn/strings.xml | 2 +- app/src/main/res/values-no/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 9 +++++---- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-qt/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 5 +++-- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-sk/strings.xml | 2 +- app/src/main/res/values-so/strings.xml | 2 +- app/src/main/res/values-sv/strings.xml | 2 +- app/src/main/res/values-ta/strings.xml | 2 +- app/src/main/res/values-tl/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-ur/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- 41 files changed, 60 insertions(+), 52 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index d7fecfd1..f70ca0c7 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -561,4 +561,4 @@ باستخدام jsdelivr ، يمكن تجاوز حظر GitHub. قد يؤخر التحديثات لبضعة أيام. وكيل raw.githubusercontent.com جودة المشاهدة المفضلة (بيانات الجوال) - \ 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 301242cd..d3bb648e 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -1,4 +1,4 @@ - + diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 71d5d6d0..12752938 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -1,4 +1,4 @@ - + পোস্টার ক্লাউডস্ট্রিম দিয়ে চালান diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 13b34872..16df53a6 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -1,4 +1,4 @@ - + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1dc2ebce..622c39ea 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -1,5 +1,6 @@ - + + %s Ep %d @@ -553,4 +554,4 @@ Vrátit zpět Obchází blokování GitHubu pomocí jsdelivr, může způsobit zpoždění aktualizací o několik dní. Obcházení ISP - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8fbcc2d0..3e71b565 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,4 +1,4 @@ - + %s Ep %d Besetzung: %s diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index f07ce43c..67e81957 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -1,4 +1,4 @@ - + CloudStream diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 5eac8686..49f025d0 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -1,4 +1,4 @@ - + Reen Hejmo diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 289de2a1..8a32b77d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -529,4 +529,4 @@ Revertir ISP Bypasses Calidad de visualización preferida (Datos móviles) - \ No newline at end of file + diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index e4c23628..6a6b5243 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -1,4 +1,4 @@ - + حذف مکث diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b96ff0cd..7c26e6b7 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,4 +1,4 @@ - + CloudStream diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 833b76f4..e4b9fe46 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -1,4 +1,4 @@ - + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index e38a6225..4dce3c6f 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -1,5 +1,6 @@ - + + %d %s | %s %s • %s @@ -554,4 +555,4 @@ Neuspješno dohvaćanje GitHuba, omogućavanje jsdelivr proxyja. Zaobilazi GitHub blokiranje koristeći jsdelivr. Može odgoditi ažuriranja za nekoliko dana. Preferirana kvaliteta gledanja (podatkovna mobilna mreža) - \ No newline at end of file + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 396c514b..7c2bbc18 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -495,4 +495,4 @@ Beállítás kihagyása HQ %d letöltve - \ 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 e6da6195..0940c8e2 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -1,5 +1,6 @@ - + + %s Ep %d Pemeran: %s @@ -552,4 +553,4 @@ Bypass ISP Pulihkan Nonton dengan kualitas yang di inginkan (Data Seluler) - \ 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 52a354c7..6b63dd89 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,5 +1,6 @@ - + + %s Ep %d Cast: %s @@ -551,4 +552,4 @@ Aggiornando shows a cui sei iscritto L\'episodio %d è stato rilasciato! Qualità di visualizzazione preferita (Dati mobili) - \ 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 b24f0c60..50e96c7c 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -1,4 +1,4 @@ - + הרקע של ההצגה לפני צוות שחקנים: %s diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 20641b20..2a36c8b4 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1,4 +1,4 @@ - + %d分 ダウンロード diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 4b7b6869..1236dbba 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -1,4 +1,4 @@ - + %sಎಪಿ%d ಕ್ಯಾಸ್ಟ್:%s diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 29fd4be8..7964dce8 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -1,5 +1,6 @@ - + + Брзина (%.2fx) Оценето: %.1f @@ -354,7 +355,7 @@ Овозможете NSFW на поддржани провајдери Не успеа да стигне до GitHub, овозможувајќи jsdelivr прокси. Филтрирајте по претпочитан медиумски јазик - \@string/home_play + @string/home_play Филм Додаден %s приклучоци @@ -518,4 +519,4 @@ Поставете статус на пратење Пушти Livestream %s е автентициран - \ No newline at end of file + diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index d430d7cc..a246cf9c 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -1,4 +1,4 @@ - + diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index c757504a..42eba3cc 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 792f37e7..5cac7dfd 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1,5 +1,6 @@ - + + %s Ep %d Cast: %s @@ -551,4 +552,4 @@ \nWord lid van onze Discord of zoek online. Audiosporen Gesorteerd op - \ No newline at end of file + diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 43738665..b3dda84f 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -1,4 +1,4 @@ - + Fleire val Heim diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index fddd4919..4e7f6abd 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -1,4 +1,4 @@ - + Plakat diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 48b9ab40..863b2c2f 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1,5 +1,6 @@ - + + Prędkość (%.2fx) Ocena: %.1f Znaleziono nową aktualizację! @@ -455,7 +456,7 @@ Instalator APK Niektóre telefony nie obsługują nowego instalatora pakietów. Wypróbuj tryb legacy, jeśli aktualizacje nie zostaną zainstalowane. password123 - \@string/ova + @string/ova MojaFajnaWitryna MyCoolUsername 127.0.0.1 @@ -465,7 +466,7 @@ Instalator pakietów @string/home_play hello@world.com - \@string/anime + @string/anime Opening Ending Mixed opening @@ -532,4 +533,4 @@ Obchodzi blokadę GitHuba za pomocą jsdelivr, może spowodować opóźnienie aktualizacji o kilka dni. Nie udało się połączyć z GitHub, włączono serwer pośredniczący jsdelivr. Domyślna jakość (dane mobilne) - \ No newline at end of file + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 44615934..f34dec8f 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -529,4 +529,4 @@ Configurações padrão SD Faixas de áudio - \ No newline at end of file + diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index eee28785..f763d795 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -1,4 +1,4 @@ - + aauugghhaauuh diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 97896b92..99e112ce 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1,5 +1,6 @@ - + + %s Ep %d Distribuție: %s @@ -390,4 +391,4 @@ Joacă cu CloudStream Actualizare plugin automată Descarcă plugin-uri automat - \ 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 9d8f6895..b5601da3 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,4 +1,4 @@ - + История Нет diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index a1afd6d9..12e580a2 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -1,4 +1,4 @@ - + Našla sa nová aktualizácia! \n%s -> %s diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index ce7d557a..db82d9fa 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -1,4 +1,4 @@ - + Metalaya: %s %dm %ds %dd diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 0b7ba89e..736f27ce 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -1,4 +1,4 @@ - + Betygsatt: %.1f diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 4370e760..affb04bf 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -1,4 +1,4 @@ - + தேடுக தேடல் %s… diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index cf3b1263..a1faf3e1 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -1,4 +1,4 @@ - + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 74754008..5b543915 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,4 +1,4 @@ - + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 2f5e0cb8..2e7f4789 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -529,4 +529,4 @@ Обходи ISP Обхід блокування GitHub за допомогою jsdelivr, може призвести до затримки оновлень на кілька днів. Бажана якість перегляду (Мобільні дані) - \ No newline at end of file + diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index c19c6472..df2e9a8b 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -1,4 +1,4 @@ - + کاسٹ: %s قسط %d جاری کیا جائے گا diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 520cfaa4..8cad60ad 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,4 +1,4 @@ - + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 3364ea86..01b3b682 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1,4 +1,4 @@ - + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 44b93430..71d97abc 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1,4 +1,4 @@ - + From 4f9016713fe3aaaffaff9a6a37f0b3d7f70d96a1 Mon Sep 17 00:00:00 2001 From: Shif-Jess <117321707+Shif-Jess@users.noreply.github.com> Date: Sun, 9 Apr 2023 14:37:27 +0700 Subject: [PATCH 062/570] CS3Player: fixed ERROR_CODE_BEHIND_LIVE_WINDOW (#447) --- .../cloudstream3/ui/player/CS3IPlayer.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index e0885671..9accf15e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -985,12 +985,19 @@ class CS3IPlayer : IPlayer { // If the Network fails then ignore the exception if the duration is set. // This is to switch mirrors automatically if the stream has not been fetched, but // allow playing the buffer without internet as then the duration is fetched. - if (error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED - && exoPlayer?.duration != TIME_UNSET - ) { - exoPlayer?.prepare() - } else { - playerError?.invoke(error) + when { + error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED + && exoPlayer?.duration != TIME_UNSET -> { + exoPlayer?.prepare() + } + error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> { + // Re-initialize player at the current live window default position. + exoPlayer?.seekToDefaultPosition() + exoPlayer?.prepare() + } + else -> { + playerError?.invoke(error) + } } super.onPlayerError(error) From 94e7eb8e9d3e4603ab6d1956a8003b36a9d25095 Mon Sep 17 00:00:00 2001 From: Sarlay <60151189+Sarlay@users.noreply.github.com> Date: Sun, 9 Apr 2023 14:21:41 +0000 Subject: [PATCH 063/570] added a mirror to streamsb (#439) --- .../java/com/lagradost/cloudstream3/extractors/StreamSB.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt index 993ef156..1c6c7b94 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt @@ -81,6 +81,10 @@ class StreamSB11 : StreamSB() { override var mainUrl = "https://sbbrisk.com" } +class Sblongvu : StreamSB() { + override var mainUrl = "https://sblongvu.com" +} + // This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt // The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE open class StreamSB : ExtractorApi() { From b356ad9e613f7d46bae0d9c7e8eca02fbf6e23cf Mon Sep 17 00:00:00 2001 From: Shif-Jess <117321707+Shif-Jess@users.noreply.github.com> Date: Tue, 11 Apr 2023 16:46:39 +0700 Subject: [PATCH 064/570] CS3IPlayer: fix buffer lost when seeked to backward (#448) * CS3IPlayer: fix buffer lost when seeked to backward * changed BUFFER_MS --- .../java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 9accf15e..9ec18b9c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -712,6 +712,10 @@ class CS3IPlayer : IPlayer { if (cacheSize > Int.MAX_VALUE) Int.MAX_VALUE else cacheSize.toInt() } ) + .setBackBuffer( + 30000, + true + ) .setBufferDurationsMs( DefaultLoadControl.DEFAULT_MIN_BUFFER_MS, if (videoBufferMs <= 0) { From 444934759307e8b9cfe6da8f29a806be41cc88f9 Mon Sep 17 00:00:00 2001 From: reduplicated <110570621+reduplicated@users.noreply.github.com> Date: Wed, 15 Mar 2023 22:22:19 +0100 Subject: [PATCH 065/570] bump --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0bd56fe7..fa1b277a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -47,8 +47,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 57 - versionName = "4.0.0" + versionCode = 59 + versionName = "4.0.1" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") From bdb45b69d354226d8dfcac2fbe77986f820f3100 Mon Sep 17 00:00:00 2001 From: reduplicated <110570621+reduplicated@users.noreply.github.com> Date: Tue, 11 Apr 2023 18:04:24 +0200 Subject: [PATCH 066/570] pip fixes --- .../ui/player/AbstractPlayerFragment.kt | 4 +- .../main/res/layout/player_custom_layout.xml | 1267 +++++++++-------- .../res/layout/player_custom_layout_tv.xml | 649 ++++----- 3 files changed, 968 insertions(+), 952 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 21047db3..52f0b760 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -184,7 +184,7 @@ abstract class AbstractPlayerFragment( isInPIPMode = isInPictureInPictureMode if (isInPictureInPictureMode) { // Hide the full-screen UI (controls, etc.) while in picture-in-picture mode. - player_holder?.alpha = 0f + piphide?.isVisible = false pipReceiver = object : BroadcastReceiver() { override fun onReceive( context: Context, @@ -212,7 +212,7 @@ abstract class AbstractPlayerFragment( updateIsPlaying(Pair(isPlayingValue, isPlayingValue)) } else { // Restore the full-screen UI. - player_holder?.alpha = 1f + piphide?.isVisible = true exitedPipMode() pipReceiver?.let { activity?.unregisterReceiver(it) diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 683a1077..54f92d1f 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -8,17 +8,6 @@ android:screenOrientation="landscape" tools:orientation="vertical"> - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:background="@color/black_overlay" /> - + - - - - - - - - - - - - - - - - - - - - - - - - - - + android:gravity="center" + android:shadowColor="@android:color/black" + android:shadowRadius="10.0" + android:textColor="@android:color/white" + android:textSize="30sp" + tools:text="+100" /> + - - android:nextFocusRight="@id/exo_ffwd" - - android:nextFocusUp="@id/player_go_back" - android:nextFocusDown="@id/player_lock" - - android:src="@drawable/netflix_pause" - app:tint="@color/white" - tools:ignore="ContentDescription" /> - + + android:layout_height="match_parent"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + --> + + + + + + diff --git a/app/src/main/res/layout/player_custom_layout_tv.xml b/app/src/main/res/layout/player_custom_layout_tv.xml index 405b606f..62b359b6 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -9,8 +9,9 @@ android:tag="television" tools:orientation="vertical"> + @@ -19,359 +20,365 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/player_gradient_tv" /> - - + + + + - - - + + + + + + + + + + + - ---> - - - - - - - - - - - - - + + android:layout_gravity="center" - + android:clickable="false" + android:focusable="false" + android:focusableInTouchMode="false" - - - - - - - - - - - - - - - - - - - - - - - - - + android:indeterminate="true" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> + android:layout_gravity="bottom" + android:layout_marginBottom="20dp" + android:gravity="center" + android:orientation="horizontal" + android:paddingTop="4dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent"> - - - - + + + + + + + - - - + android:layout_height="0dp" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + - + android:layout_marginStart="64dp" + android:layout_marginEnd="64dp" + android:layout_marginBottom="10dp" + android:gravity="center_vertical" + android:orientation="vertical" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"> - + + android:src="@drawable/netflix_pause" + app:tint="@color/player_button_tv" + tools:ignore="ContentDescription" /> - + + - android:visibility="gone" - app:icon="@drawable/ic_outline_subtitles_24" - tools:visibility="visible" /> + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + From a12d234ef41cf0b1927c2c261d663f33fbeb050a Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Fri, 21 Apr 2023 13:54:58 +0200 Subject: [PATCH 067/570] fix shortcodes --- .../cloudstream3/plugins/RepositoryManager.kt | 14 +++++--------- .../ui/settings/extensions/ExtensionsFragment.kt | 1 - 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt index 742bf308..b80a590e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -95,15 +95,11 @@ object RepositoryManager { } } else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) { suspendSafeApiCall { - app.get("https://l.cloudstream.cf/${fixedUrl}", allowRedirects = false).let { - it.headers["Location"]?.let { url -> - return@suspendSafeApiCall if (!url.startsWith("https://cutt.ly/branded-domains")) url - else null - } - app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false).let { it2 -> - it2.headers["Location"]?.let { url -> - return@suspendSafeApiCall if (url.startsWith("https://cutt.ly/404")) url else null - } + app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false).let { it2 -> + it2.headers["Location"]?.let { url -> + if (url.startsWith("https://cutt.ly/404")) return@suspendSafeApiCall null + if (url.removeSuffix("/") == "https://cutt.ly") return@suspendSafeApiCall null + return@suspendSafeApiCall url } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 7e60910d..045ed92d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.settings.extensions -import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.DialogInterface From 633aef878355c3d1519a65a44f502ed3558849b5 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 20 Apr 2023 23:26:39 +0200 Subject: [PATCH 068/570] Translated using Weblate (Macedonian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Polish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Croatian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Korean) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Korean) Currently translated at 93.6% (571 of 610 strings) Translated using Weblate (Korean) Currently translated at 87.5% (534 of 610 strings) Translated using Weblate (Russian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Korean) Currently translated at 31.4% (192 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Added translation using Weblate (Korean) Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (German) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (French) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Italian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Dutch) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Czech) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (German) Currently translated at 99.6% (608 of 610 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Portuguese) Currently translated at 99.6% (608 of 610 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 90.8% (554 of 610 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (English) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Romanian) Currently translated at 95.4% (582 of 610 strings) Translated using Weblate (Romanian) Currently translated at 75.0% (458 of 610 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Polish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Czech) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 90.4% (552 of 610 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (English) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Malayalam) Currently translated at 37.8% (231 of 610 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (610 of 610 strings) Added translation using Weblate (Arabic (Najdi)) Translated using Weblate (Latvian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Japanese) Currently translated at 46.2% (282 of 610 strings) Translated using Weblate (Macedonian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Latvian) Currently translated at 30.4% (186 of 610 strings) Added translation using Weblate (Latvian) Translated using Weblate (German) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Croatian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (German) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Russian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Macedonian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Turkish) Currently translated at 99.8% (609 of 610 strings) Translated using Weblate (Malayalam) Currently translated at 37.2% (227 of 610 strings) Co-authored-by: AHOHNMYC Co-authored-by: Aitor Salaberria Co-authored-by: Alexandru Co-authored-by: Alexthegib Co-authored-by: Allan Nordhøy Co-authored-by: Anurag Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> Co-authored-by: Clxff Heraldo <123844876+clxf12@users.noreply.github.com> Co-authored-by: Eryk Michalak Co-authored-by: FastAct Co-authored-by: Felipe Nogueira Co-authored-by: Fjuro Co-authored-by: Hosted Weblate Co-authored-by: Julian Co-authored-by: Massimo Pissarello Co-authored-by: Milo Ivir Co-authored-by: Rex_sa Co-authored-by: Sdarfeesh Co-authored-by: Skrripy Co-authored-by: Synertry Co-authored-by: Tang Yin Co-authored-by: The Unbreakable Spirit Co-authored-by: Turgay Doğru Co-authored-by: Vrwi Co-authored-by: Zaki Bouta Co-authored-by: edgolron Co-authored-by: eightyy8 Co-authored-by: gallegonovato Co-authored-by: jinu147 Co-authored-by: stojkovskistefan Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ml/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ro/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/ Translation: Cloudstream/App --- app/src/main/res/values-ar/strings.xml | 14 +- app/src/main/res/values-ars/strings.xml | 2 + app/src/main/res/values-cs/strings.xml | 15 +- app/src/main/res/values-de/strings.xml | 14 +- app/src/main/res/values-es/strings.xml | 12 +- app/src/main/res/values-fr/strings.xml | 28 +- app/src/main/res/values-hr/strings.xml | 19 +- app/src/main/res/values-in/strings.xml | 13 +- app/src/main/res/values-it/strings.xml | 17 +- app/src/main/res/values-ja/strings.xml | 18 +- app/src/main/res/values-ko/strings.xml | 532 ++++++++++++++++++++++++ app/src/main/res/values-lv/strings.xml | 528 +++++++++++++++++++++++ app/src/main/res/values-mk/strings.xml | 31 +- app/src/main/res/values-ml/strings.xml | 14 +- app/src/main/res/values-nl/strings.xml | 17 +- app/src/main/res/values-no/strings.xml | 43 +- app/src/main/res/values-pl/strings.xml | 15 +- app/src/main/res/values-pt/strings.xml | 12 +- app/src/main/res/values-ro/strings.xml | 191 ++++++++- app/src/main/res/values-ru/strings.xml | 14 +- app/src/main/res/values-tr/strings.xml | 9 +- app/src/main/res/values-uk/strings.xml | 18 +- app/src/main/res/values-zh/strings.xml | 17 +- app/src/main/res/values/strings.xml | 9 +- 24 files changed, 1449 insertions(+), 153 deletions(-) create mode 100644 app/src/main/res/values-ars/strings.xml create mode 100644 app/src/main/res/values-ko/strings.xml create mode 100644 app/src/main/res/values-lv/strings.xml diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index f70ca0c7..637e8c15 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -366,7 +366,7 @@ تحميل من الانترنت الملف الذي تم تنزيله رئيسي - مساعد + ممثل مساعد الخلفية مصدر عشوائي @@ -525,12 +525,12 @@ اختر المكتبة المتصفح محدث (من الأحدث إلى الأقدم) - يبدو أن هذه القائمة فارغة ، حاول التبديل إلى قائمة أخرى + هذه القائمة فارغة ، حاول التبديل إلى قائمة أخرى. التقييم (من الأعلى إلى الأدنى) التقييم (من الأدنى إلى الأعلى) الترتيب الأبجدي (من ي إلى أ) - يبدو أن مكتبتك فارغة :( -\nتسجيل الدخول إلى حساب مكتبة أو إضافة عروض إلى مكتبتك المحلية + مكتبتك فارغة :( +\nقم بتسجيل الدخول على حساب مكتبة أو أضف عروضا إلى مكتبتك المحلية. محدث (من القديم إلى الجديد) فرز حسب افرز @@ -557,8 +557,8 @@ مشترك في %s تجاوز مزود خدمة الإنترنت استرجاع - فشل الوصول إلى GitHub ، وتمكين وكيل jsdelivr. - باستخدام jsdelivr ، يمكن تجاوز حظر GitHub. قد يؤخر التحديثات لبضعة أيام. + تعذر الوصول إلى جيثب. تشغيل وكيل jsDelivr … + تجاوز حظر GitHub باستخدام jsdelivr ، قد يتسبب في تأخير التحديثات لبضعة أيام. وكيل raw.githubusercontent.com جودة المشاهدة المفضلة (بيانات الجوال) - + \ No newline at end of file diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-ars/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 622c39ea..43ded674 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -1,6 +1,5 @@ - - + %s Ep %d @@ -473,15 +472,15 @@ Nepodařilo se nainstalovat novou verzi aplikace Původní Aplikace bude po ukončení aktualizována - Vypadá to, že vaše knihovna je prázdná :( -\nPřihlaste se k účtu v knihovně nebo přidejte pořady do místní knihovny + Vaše knihovna je prázdná :( +\nPřihlaste se k účtu v knihovně nebo přidejte pořady do místní knihovny. Vybrat knihovnu Hodnocení (od nejvyššího) Hodnocení (od nejnižšího) Abecedně (od Z do A) Seřadit podle Řazení - Vypadá to, že tento seznam je prázdný, zkuste přepnout na jiný + Tento seznam je prázdný. Zkuste přepnout na jiný. Nalezen soubor bezpečného režimu! \nDo odebrání souboru nebudeme načítat žádná rozšíření. Aktualizováno (od nejnovějšího) @@ -549,9 +548,9 @@ Byla vydána epizoda %d! Odebíráno Proxy raw.githubusercontent.com - Nepodařilo se připojit ke GitHubu, povolování proxy jsdelivr. + Nelze se připojit k serveru GitHub. Zapínání proxy jsDelivr… Upřednostněná kvalita sledování (mobilní data) Vrátit zpět - Obchází blokování GitHubu pomocí jsdelivr, může způsobit zpoždění aktualizací o několik dní. + Obchází blokování GitHubu pomocí jsDelivr. Může způsobit zpoždění aktualizací o několik dní. Obcházení ISP - + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3e71b565..071f30c0 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -166,7 +166,7 @@ Ausgewählte Videoqualität bei Suchergebnissen ausblenden Automatische Plugin-Updates App-Updates anzeigen - Automatisches Suchen nach neuen Updates nach dem Start + Automatisches Suchen nach neuen Updates nach dem Start. Auf Vorabversionen updaten Suche nach Vorabversionen statt nur nach Vollversionen Github @@ -286,7 +286,7 @@ Haftungsausschluss Allgemein Zufalls-Button - Zeigt einen Zufallsbutton auf der Startseite an, mit welchem eine Serie oder ein Film von der Website zufällig ausgewählt wird + Zeige Zufallsgenerator Schaltfläche auf der Startseite Anbieter-Sprachen App-Layout Bevorzugte Medien @@ -501,9 +501,9 @@ Alphabetisch (Z bis A) Bibliothek auswählen Öffnen mit - Sieht aus, als wäre deine Bibliothek leer :( -\nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu - Diese Liste scheint leer zu sein. Versuche, zu einer anderen Liste zu wechseln. + Deine Bibliothek ist leer :( +\nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu. + Diese Liste ist leer. Versuche zu einer anderen Liste zu wechseln. Datei für abgesicherten Modus gefunden! \nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird. Player ausgeblendet - Betrag zum vor- und zurückspulen @@ -524,9 +524,9 @@ %s deabonniert Episode %d erschienen! raw.githubusercontent.com Proxy - GitHub kann nicht erreicht werden, der jsdelivr-Proxy wird aktiviert. + GitHub konnte nicht erreicht werden. Der jsDelivr-Proxy wird aktiviert … Abonnierte Serien werden aktualisiert Rückgängig Abonniert ISP-Umgehungen - + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 8a32b77d..b863479e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -498,12 +498,12 @@ Alfabéticamente (A a Z) Navegador Biblioteca - Parece que esta lista está vacía, intenta cambiar a otra + Esta lista está vacía. Intenta cambiar a otra. Alfabéticamente (Z a A) Seleccionar biblioteca Abrir con - Parece que tu biblioteca está vacía :( -\nInicia sesión en una cuenta de biblioteca o añade series desde tu biblioteca local + Tu biblioteca está vacía :( +\nRegístrate con una cuenta en la biblioteca o agrega los títulos a tu biblioteca local. ¡Se encontró un archivo en modo seguro! \nNo cargar ninguna extensión al inicio hasta que se elimine el archivo. Reproductor visible - buscar cantidad @@ -524,9 +524,9 @@ Actualizando los programas suscritos ¡Episodio %d publicado! Proxy raw.githubusercontent.com - No se ha podido acceder a GitHub, activando el proxy jsdelivr. - Omite el bloqueo de GitHub mediante jsdelivr, lo que puede provocar que las actualizaciones se retrasen unos días. + No se ha podido acceder a GitHub. Activando el proxy jsDelivr… + Omite el bloqueo de GitHub mediante jsDelivr. Lo que puede provocar que las actualizaciones se retrasen unos días. Revertir ISP Bypasses Calidad de visualización preferida (Datos móviles) - + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7c26e6b7..b7c9900b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,6 +1,5 @@ - - + CloudStream Accueil Rechercher @@ -260,16 +259,16 @@ Mode Eigengravy Ajout d\'une option de vitesse dans le lecteur Balayez pour chercher - Balayez vers la gauche ou la droite pour contrôler le temps dans le lecteur vidéo + Swipez d\'un côté à l\'autre pour contrôler votre position dans une vidéo Balayez pour modifier les paramètres Glissez sur le côté gauche ou droit pour modifier la luminosité ou le volume Lecture automatique du prochain épisode Démarrer l\'épisode suivant lorsque l\'épisode en cours se termine Double tape pour chercher Double tape pour mettre en pause - Player seek amount + Montant recherché par le joueur (Seconde) Tapez deux fois sur le côté droit ou gauche pour aller en avant ou en arrière - Tapez au milieu pour mettre en pause + Tapez deux fois au milieu pour mettre en pause Utiliser la luminosité du système Utiliser la luminosité du système dans le lecteur d\'applications au lieu du sombre Mise à jour de la progression de la veille @@ -386,8 +385,8 @@ 4K Web -30 - @string/anime - @string/ova + \@string/anime + OAV NSFW %s %s Filtrez par langue préférée @@ -497,9 +496,9 @@ Note (basse à haute) Note (haut à bas) Alphabétique (A à Z) - On dirait que votre bibliothèque est vide :( -\nConnectez-vous à un compte ou ajoutez des séries à votre bibliothèque locale - Il semble que cette liste soit vide, essayez d\'en choisir une autre + Votre bibliothèque est vide :( +\nConnectez-vous sur un compte de bibliothèque ou ajoutez des spectacles à votre bibliothèque locale. + Cette liste est vide. Essayez d\'en changer. Android TV Trié par Alphabétique (Z à A) @@ -524,4 +523,11 @@ Contournements de FAI L\'épisode %d est sorti ! Échouer - + Le montant de la recherche utilisé lorsque le joueur est caché + Updating subscribed shows + Contourne le blocage de GitHub en utilisant jsDelivr. Les mises à jour peuvent être retardées de quelques jours. + La quantité de recherche utilisée lorsque le joueur est visible + Joueur représenté - Montant de la recherche + Joueur caché - Montant de la recherche + Impossible d\'accéder à GitHub. Activation du proxy jsDelivr… + \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 4dce3c6f..2491c3ce 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -1,6 +1,5 @@ - - + %d %s | %s %s • %s @@ -174,7 +173,7 @@ Sakrij odabranu kvalitetu videozapisa u rezultatima pretraživanja Automatsko ažuriranje dodataka Prikaži ažuriranja aplikacije - Automatski traži nova ažuriranja nakon pokretanja aplikacije + Automatski traži nova ažuriranja nakon pokretanja aplikacije. Ažuriranje na predizdanja Tražite ažuriranja prije izdanja umjesto samo potpunih izdanja Github @@ -527,9 +526,9 @@ 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 + Vaša je biblioteka prazna :( +\nPrijavite se na račun biblioteke ili dodajte emisije u svoju lokalnu biblioteku. + Ova je lista prazna. Pokušajte se prebaciti na jednu drugu listu. Pronađena datoteka sigurnog načina rada! \nNe učitavaju se ekstenzije pri pokretanju dok se datoteka ne ukloni. Prikazan player- iznos preskakanja @@ -544,7 +543,7 @@ Neuspješno Stop Test pružatelja usluga - Ažuriram pretplaćene serije + Ažuriranje pretplaćenih emisija Epizoda %d izbačena! Pretplaćeno Pretplaćen na %s @@ -552,7 +551,7 @@ Vraćanje ISP zaobilaznice raw.githubusercontent.com Proxy - Neuspješno dohvaćanje GitHuba, omogućavanje jsdelivr proxyja. - Zaobilazi GitHub blokiranje koristeći jsdelivr. Može odgoditi ažuriranja za nekoliko dana. + Neuspješno dohvaćanje GitHuba. Uključuje se jsdelivr proxy … + Zaobilazi blokiranje GitHuba koristeći jsdelivr. Može odgoditi ažuriranja za nekoliko dana. Preferirana kvaliteta gledanja (podatkovna mobilna mreža) - + \ 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 0940c8e2..af02b9d0 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -1,6 +1,5 @@ - - + %s Ep %d Pemeran: %s @@ -516,7 +515,7 @@ Browser Pilih pustaka Yahh daftar pustaka kamu kosong :( -\nMasuk ke akun pustaka atau tambah perlihatkan ke lokal pustaka kamu +\nMasuk ke akun pustaka atau tambah perlihatkan ke lokal pustaka kamu. Pustaka Urutkan berdasar Urutkan @@ -527,7 +526,7 @@ Abjad (A ke Z) Abjad (Z ke A) Buka dengan - Yahh daftar ini kosong, coba ganti ke yang lain + Yahh daftar ini kosong. Coba ganti ke yang lain. Mode aman file ditemukan! \nTidak memuat ekstensi pada startup sampai berkas dihapus. Sembunyikan Pemutaran - Geser @@ -548,9 +547,9 @@ Berhenti berlangganan di %s Episode %d telah rilis! raw.githubusercontent.com Proksi - Gagal mencapai GitHub, mengaktifkan proksi jsdelivr. - Bisa melewati pemblokiran GitHub mengunakan jsdelivers. Mungkin dapat menyebabkan tertunda dalam beberapa hari. + Tidak dapat menjangkau GitHub. Mengaktifkan proksi jsDelivr… + Melewati pemblokiran GitHub menggunakan jsDelivr. Dapat menyebabkan pembaruan tertunda beberapa hari. Bypass ISP Pulihkan Nonton dengan kualitas yang di inginkan (Data Seluler) - + \ 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 6b63dd89..248cb230 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,6 +1,5 @@ - - + %s Ep %d Cast: %s @@ -130,7 +129,7 @@ Scorri per mandare avanti/indietro Scorri da un lato all\'altro per controllare la tua posizione in un video Scorri per cambiare le impostazioni - Scorri verso l\'alto o verso il basso sul lato sinistro o destro per modificare la luminosità o il volume + Scorri il dito sul lato sinistro o destro per cambiare la luminosità o il volume Riproduci automaticamente l\'episodio successivo Avvia l\'episodio successivo al termine di quello in corso Doppio tocco per andare avanti/indietro @@ -520,13 +519,13 @@ 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 + La tua libreria è 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 + Questo elenco è vuoto. Prova a passare a un altro. File \"safe mode\" trovato! \nAll\'avvio non sarà caricata alcuna estensione finchè il file non verrà rimosso. Quantità di ricerca usata quando il player è nascosto @@ -545,11 +544,11 @@ Disiscritto da %s Iscritto Iscritto a %s - Impossibile contattare GitHub, abilitazione proxy jsdelivr avviata. - Ignora il blocco di GitHub utilizzando jsdelivr, può causare il ritardo degli aggiornamenti di alcuni giorni. + Impossibile raggiungere GitHub. Attivazione proxy jsDelivr… + Aggira il blocco di GitHub usando jsDelivr. Potrebbe causare un ritardo degli aggiornamenti di alcuni giorni. Baypass ISP Ripristina Aggiornando shows a cui sei iscritto L\'episodio %d è stato rilasciato! Qualità di visualizzazione preferita (Dati mobili) - + \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 2a36c8b4..127f60b7 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -182,4 +182,20 @@ アップデートを確認 作品名 アプリのアップデートをインストール中… - + リポジトリURL + 字幕ディレイ + 登録済み + ストリームへのリンク + 字幕遅延なし + リポジトリ削除 + このリポジトリからすべてのプラグインをダウンロードしますか? + 視聴状態を設定 + 字幕を同期 + リポジトリを追加 + リポジトリ名 + CloudStreamはデフォルトでサイトがインストールされていません。リポジトリからサイトをインストールする必要があります。 +\n +\nSky UK Limitedによる無脳なDMCAテイクダウンのため🤮、アプリ内でリポジトリサイトをリンクすることができません。 +\n +\n私たちのDiscordに参加するか、オンラインで検索してください。 + \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml new file mode 100644 index 00000000..fdadb4ae --- /dev/null +++ b/app/src/main/res/values-ko/strings.xml @@ -0,0 +1,532 @@ + + + 출연: %s + 에피소드 %d이(가) 공개됩니다 + 포스터 + 에피소드 포스터 + 메인 포스터 + 다음 랜덤 + 뒤로가기 + 소스 변경 + 미리보기 배경 + 속도 (%.2fx) + 평점: %.1f + 새로운 업데이트! +\n%s -> %s + %d분 + CloudStream + CloudStream에서 재생 + + 다운로드 + 설정 + 검색… + 검색 %s… + 데이터 없음 + 기타 옵션 + 다음 에피소드 + 장르 + 공유 + 브라우저에서 열기 + 브라우저 + 로딩 건너뛰기 + 로딩중… + 시청 + 보류 + 시청 완료 + 포기 + 시청 예정 + 없음 + 다시보기 + 영화 재생 + 예고편 재생 + 토렌트 재생 + 소스 + 자막 + 연결 재시도… + 뒤로가기 + 에피소드 재생 + 다운로드 + 파일 재생 + 계속 다운로드 + 다운로드 일시정지 + 자동 오류 보고 비활성화 + 상세 정보 + 닫기 + 재생 + 정보 + 시청 상태 설정 + 저장 + 재생 속도 + 글자 색깔 + 외곽선 색깔 + 배경 색깔 + 창 색깔 + 가장자리 타입 + 자막 높이 + 폰트 + 폰트 크기 + 다운로드됨 + 다운로드중 + 다운로드 일시정지 + 다운로드 시작 + 다운로드 실패 + 다운로드 취소 + 다운로드 완료 + 업데이트 시작 + 링크 로딩 오류 + 내부 저장공간 + 자동 선택 언어 + 다운로드 언어 + 자막 언어 + 길게 눌러 기본값으로 재설정 + 다음에서 폰트 가져오기 %s + 계속 시청 + 제거 + -30 + %s 에피소드 %d + 포스터 + %d일 %d시간 %d분 + %d시간 %d분 + %d분 + 검색 + 파일 삭제 + 제거 + 업데이트 및 백업 + 백업 + 더빙 + 자막 + 취소 + 북마크 필터 + 북마크 + 제거 + 적용 + 복사 + 닫기 + 자막 설정 + 소스로 검색 + 로그 + 이 소스가 제대로 작동하려면 VPN이 필요할 수 있습니다 + 이 소스는 토렌트이므로 VPN을 사용하는 것이 좋습니다 + 메타데이터는 사이트별로 제공하지 않으며, 메타데이터가 사이트에 없으면 동영상 로딩이 실패합니다. + 설명 + 플레이어 자막 설정 + Chromecast 자막 + Chromecast 자막 설정 + 배속 모드 + 플레이어에 속도 옵션을 추가합니다 + 스와이프하여 탐색 + 좌우로 스와이프하여 동영상 위치 제어하기 + 스와이프하여 설정 변경 + 왼쪽 또는 오른쪽으로 밀어서 밝기 또는 볼륨을 변경합니다 + 다음 에피소드 자동 재생 + 현재 에피소드가 끝나면 다음 에피소드를 시작합니다 + 두 번 탭하여 탐색 + 두 번 탭하여 일시정지 + 플레이어 탐색 시간 (초) + 가운데를 두 번 탭하여 일시중지 + 시스템 밝기 사용 + 어두운 오버레이 대신 앱 플레이어의 시스템 밝기를 사용합니다 + 시청 진행 상황 업데이트 + 현재 에피소드 진행 상황을 자동으로 동기화합니다 + 백업에서 데이터 복원 + 데이터 백업 + 파일에서 데이터를 복원하지 못했습니다 %s + 저장된 데이터 + 저장소 권한이 없습니다. 다시 시도해 주세요. + 백업 중 오류 %s + 검색 + 라이브러리 + 계정 + 소스별로 구분된 검색 결과를 제공합니다 + 예고편 보기 + Kitsu에서 포스터 보기 + 검색 결과에서 선택한 동영상 품질 숨기기 + 플러그인 자동 다운로드 + 플러그인 자동 업데이트 + 추가된 저장소에서 아직 설치되지 않은 모든 플러그인을 자동으로 설치합니다. + 앱 업데이트 표시 + 앱을 시작한 후 새 업데이트를 자동으로 검색합니다. + 미리보기로 업데이트 + 정식 릴리즈 대신 미리보기 업데이트만 검색합니다 + 일부 휴대폰은 새 패키지 설치 프로그램을 지원하지 않습니다. 업데이트가 설치되지 않으면 레거시 옵션을 사용해 보세요. + 같은 개발자가 만든 라이트 노벨 앱 + 같은 개발자가 만든 애니메이션 앱 + Discord에 참여하기 + 개발자에게 바나나 주기 + 바나나 줌 + 앱 언어 + 링크를 찾을 수 없음 + 클립보드에 링크 복사됨 + 에피소드 재생 + 기본값으로 재설정 + 죄송합니다, 애플리케이션이 충돌했습니다. 버그 보고서가 익명으로 개발자에게 전송됩니다 + 에피소드 + %d-%d + 진행중 + 시청 완료 + 상태 + + 평점 + +30 + %s가 영구 삭제됩니다 +\n정말 삭제하시겠습니까\? + %d분 +\n남음 + 사이트 + 시간 + 개요 + 대기중 + 자막 없음 + 기본 + 남음 + 사용됨 + + 영화 + TV 시리즈 + 카툰 + 애니 + 토렌트 + Chromecast 미러링 + 앱에서 재생 + %s에서 재생 + 브라우저에서 재생 + 링크 복사 + 자동 다운로드 + 다운로드 미러 + 링크 새로고침 + 자막 다운로드 + 화질 탭 + 더빙 탭 + 자막 탭 + 제목 + 업데이트 확인 + 잠금 + 크기 조정 + 소스 + 오프닝 건너뛰기 + 이 업데이트 건너뛰기 + 선호하는 화질 (WiFi) + 선호하는 화질 (모바일 데이터) + 동영상 플레이어 해상도 + 동영상 버퍼 크기 + 동영상 및 이미지 캐시 지우기 + DNS over HTTPS + GitHub에 연결할 수 없습니다. jsDelivr 프록시를 켜는 중… + jsDelivr을 사용하여 GitHub 차단을 우회합니다. 업데이트가 며칠 지연될 수 있습니다. + 복제 사이트 + 사이트 삭제 + 다른 URL을 사용하여 기존 사이트의 복제본을 추가합니다 + 다운로드 경로 + 캐시 + Android TV + 제스처 + 자막 + 레이아웃 + 기본 + 일반 + 플레이어 기능 + 기능 + 소스 언어 + 앱 레이아웃 + 선호하는 미디어 + 지원되는 공급업체에서 19금 사용 설정 + 자막 인코딩 + 소스 + 소스 테스트 + 레이아웃 + 자동 + TV 레이아웃 + 휴대폰 레이아웃 + 에뮬레이터 레이아웃 + 기본 색상 + 앱 테마 + 포스터 제목 위치 + 이메일 + IP + 로그인 + 계정 전환 + 계정 추가 + 계정 생성 + 트래커 추가 + 추가 %s + 동기화 + %d / 10 + /\?\? + /%d + %s 인증됨 + 다음에 로그인할 수 없음 %s + 없음 + 보통 + 전부 + 최대 + 최소 + 윤곽선 + 그림자 + 자막 동기화 + 1000 ms + 자막 딜레이 + 자막이 %d ms 너무 일찍 표시되는 경우, 이 옵션을 사용하세요 + 주연 + 조연 + 출연 + 소스 + 랜덤 + 포스터 이미지 + 제목 + Cam + Cam + Blu-ray + WP + DVD + 4K + SD + 해상도 및 제목 + 해상도 + 잘못된 ID + 잘못된 데이터 + 잘못된 URL + 오류 + 자막에서 선택 캡션 제거 + 선호하는 미디어 언어로 필터링 + 예고편 + 다음 + 이전 + 설정 건너뛰기 + 기기에 맞게 앱 모양 변경하기 + 확장 기능 + 버전 + 18+ + 다운로드 시작 %d %s… + 다운로드 %d %s + 모든 %s가 이미 다운로드되었습니다 + 일괄 다운로드 + 플러그인 + 플러그인 + 이렇게 하면 모든 저장소의 플러그인도 삭제됩니다 + 저장소 삭제 + 사용하려는 사이트 목록 다운로드 + 다운로드됨: %d + CloudStream에는 기본적으로 설치된 사이트가 없습니다. 저장소에서 사이트를 설치해야 합니다. +\n +\nSky UK Limited의 무분별한 DMCA 조치로 인해 🤮 앱에서 저장소 사이트를 연결할 수 없습니다. +\n +\nDiscord에 가입하거나 온라인에서 검색하세요. + 커뮤니티 저장소 보기 + 공개 목록 + 모든 자막 대문자화 + 이 저장소에서 모든 플러그인을 다운로드하시겠습니까\? + %s (사용불가) + 저장소 추가 + 저장소 이름 + 저장소 URL + 플러그인이 로드됨 + 플러그인 다운로드 + 플러그인 삭제됨 + 로드할 수 없음 %s + 오디오 트랙 + 동영상 트랙 + 재시작 시 적용 + 안전 모드 켜기 + 충돌로 인해 문제를 일으키는 확장 프로그램을 찾는 데 도움이 되도록 모든 확장 프로그램이 사용 중지되었습니다. + 충돌 정보 보기 + 언어 + 에피소드 %d 공개! + Picture-in-picture + 플레이어 크기 조정 버튼 + 다른 앱 위에 있는 미니어처 플레이어에서 재생을 계속합니다 + 충돌에 관한 데이터만 전송 + 검은색 테두리 제거 + 오른쪽 또는 왼쪽을 두 번 탭하여 앞뒤로 탐색하기 + 자막 + 로드된 백업 파일 + 정보 + 고급 검색 + 데이터를 보내지 않음 + 설정 프로세스 다시 실행 + APK 인스톨러 + Github + 소스 오류 + 이 소스는 크롬캐스트를 지원하지 않습니다 + 시즌 없음 + 시즌 + 아시아 드라마 + 시즌 + 삭제 + %s %d%s + 파일 삭제 + 일시정지 + 에피소드 + 에피소드 + %d %s + 에피소드를 찾을 수 없음 + 시작 + 실패 + 평점 + 평점: %s + 평점 (높음에서 낮음으로) + 평점 (낮음에서 높음으로) + 19금 + 다큐멘터리 + 라이브 방송 + 19금 + 기타 + OVA + 아시아 드라마 + 라이브 방송 + 동영상 + 포스터의 UI 요소 전환 + 영화 + 카툰 + 토렌트 + 다큐멘터리 + 렌더러 오류 + 시리즈 + 애니 + OVA + 원격 오류 + 다운로드 오류, 저장 권한 확인 + Chromecast 에피소드 + 예기치 않은 플레이어 오류 + 다시 표시하지 않음 + 업데이트를 찾을 수 없음 + 업데이트 + raw.githubusercontent.com 프록시 + 동영상 버퍼 길이 + 저장소에 동영상 캐시 + Android TV와 같이 메모리가 부족한 디바이스에서 너무 높게 설정하면 충돌이 발생할 수 있습니다. + 화면 크기에 맞춤 + Android TV와 같이 저장 공간이 부족한 기기에서 너무 높게 설정하면 문제가 발생할 수 있습니다. + NGINX 서버 URL + 확대 + 링크 + ISP 차단을 우회하는 데 유용합니다 + 화면 맞춤 + 더빙/자막 애니메이션 표시 + 거부 + ISP 우회 + 앱 업데이트 + 확장 기능 + 로그아웃 + 사이트 URL + 비밀번호 + 계정 + 사용자 이름 + 언어 코드 (ko) + 사이트 이름 + %s %s + 자막이 %d ms 너무 늦게 표시되는 경우, 사용하세요 + 자막 지연 없음 + 다운로드한 파일 + 파일에서 불러오기 + 추천 + 인터넷에서 불러오기 + 로드됨 %s + 공개 예정… + Cam + HD + TS + 플레이어 + HDR + TC + 충돌 보고 + 완료 + 다운로드되지 않음: %d + HQ + UHD + 이 언어로 된 동영상 보기 + SDR + Web + 비활성화됨: %d + 사이즈 + 제작자 + 무엇을 보고 싶으신가요 + 상태 + %d 플러그인 업데이트 + 재시작 + 정지 + 소개 + 유형 + 먼저 확장 프로그램을 설치하세요 + 웹 브라우저 + 앱을 찾을 수 없음 + 모든 언어 + 건너뛰기 %s + 오프닝 + 엔딩 + 혼합 엔딩 + 혼합 오프닝 + 크레딧 + 소개 + 기록 삭제 + 기록 + 오프닝/엔딩 시 건너뛰기 팝업 표시 + 텍스트가 너무 많습니다. 클립보드에 저장할 수 없습니다. + 시청에서 삭제 + 정말 종료하시겠습니까\? + + 아니요 + 앱 업데이트 다운로드 중… + 앱 업데이트 설치 중… + 새 버전의 앱을 설치할 수 없습니다 + 레거시 + 패키지 인스톨러 + 앱 종료시 업데이트됩니다 + 정렬 기준 + 정렬 + 업데이트됨 (새로움에서 오래된 순) + 업데이트 (오래됨에서 새로운 순) + 알파벳순 (A에서 Z) + 알파벳순 (Z에서 A) + 다음으로 열기 + 라이브러리가 비어 있습니다 :( +\n라이브러리 계정으로 로그인하거나 로컬 라이브러리에 프로그램을 추가하세요. + 안전 모드 파일을 찾았습니다! +\n파일이 제거될 때까지 시작 시 확장 프로그램을 로드하지 않습니다. + HLS 재생목록 + 내부 플레이어 + MPV + 선호하는 동영상 플레이어 + VLC + 라이브러리 선택 + 웹 동영상 캐스트 + 이 목록이 비어 있습니다. 다른 목록으로 전환해 보세요. + 필러 + 라이브 스트리밍 재생 + 스트림 + 유형을 사용하여 검색 + 개발자에게 %d 바나나 줌 + 바나나를 주지 않음 + 상세 정보 + \@string/home_play + 플롯을 찾을 수 없음 + 설명을 찾을 수 없음 + Logcat 🐈 표시 + 애니메이션용 필러 에피소드 표시 + 통과 + 계속 + 동영상 플레이어 제목 최대 글자 수 + 표시된 플레이어 - 빨리 감기 및 되감기 초 + 플레이어가 보일 때 사용되는 탐색량 + 플레이어 숨김 - 빨리 감기 및 되감기 초 + 플레이어가 숨겨져 있을 때 사용되는 탐색량 + 동작 + 외형 + 랜덤 버튼 + 홈페이지에 랜덤 버튼 표시 + 포스터 아래에 제목을 이동 + 내려감 + 올라감 + 다람쥐 헌 쳇바퀴에 타고파 + 자막에서 부풀림 제거 + 엑스트라 + 스트림 링크 + 트랙 + 레퍼러 + 요약 + 시청함으로 표시 + 되돌리기 + 구독한 프로그램 업데이트 + 구독중 + 구독 %s + 구독 취소 %s + \ No newline at end of file diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml new file mode 100644 index 00000000..8659a139 --- /dev/null +++ b/app/src/main/res/values-lv/strings.xml @@ -0,0 +1,528 @@ + + + Plakāts + %s Ep %d + Cast: %s + Plakāts + Epizodes plakāts + Galvenais plakāts + Nākamais random + Iet atpakaļ + Nomainīt dvēju + Apskatīt background + Ātrums (%.2fx) + Lidzīgi: %.1f + Jauns atjauninājums atrasts! +\n%s -> %s + %d galvenais + Claudstream + Atskaņo ar cloudstream + Mājas + Meklēt + Meklēt %s… + Nav datu + Vairāk opcijas + Nākamā epizode + Internets + Izlaist ladešanos + Lādējas… + Skaties + Aizturēts + Pabeigts + Atmests + Plāno skatīties + Neviena + Atkārtoti skatities + Palaist Filmu + Palaist Trelleri + Palaist Livestreamu + Skatities Torrentu + Devēji + Subtitri + Atkārtot connection + Iet atpakaļ + Palaist epizodi + Ieladēt + Lādēšana pauzēta + Lādēšana sakās + Ielādēt neizdevās + Ielādēšana atcelta + Pabeidza ieladēt + Atjauninājums sakās + Skaties + Kļūda padejot linkus + Iekšējā atmiņa + Dub + Dzēst failu + Palaist failu + Atsākt ielādi + "Pauzēt ielādi" + Atslēgt automātisko kļūdu ziņošanu + Vairāk informācijas + Slēpt + Atskaņot + Informācija + Filtra bookmarks + Bookmarks + Noņemt + Ieliec skatīšanās statusu + Izmantot + Atcelt + Kopēt + Saglabāt + Atskaņošanas ātrums + Subtitru iestādijumi + Apkārt krāsa + Backgrounds krāsa + Līga krāsa + Stūra tips + Fonts + Fonta lielums + Meklēt izmantojot devējus + Meklēt izmantojot tipus + %d Banāni iedoti veidotājiem + Episode %d būs izlaista + Filtrs + Ieladētas + Meklēt… + Settingi + Žanrs + Dalities + Atvērt internetā + Ieladēts + Lādējas + Aizvērt + Sub + Nav banāni iedoti + Subtitru augstums + Iztīrīt + Teksta krāsa + Automātiski-iestādīt valodu + %dd %dh %dm + %dm + %dd %dh %dm + Ielādēt valodas + Subtitru valoda + Tūri lai restartētu uz sākumu + Importēt fontus ieliekot iekšā %s + Turpini skatīties + Noņemt + Vairāk informācijas + \@string/home_play + VPNs varētu būt vajadzīgs lai šis devējs strādātu pareizi + Šis devējs ir Torrents vpn ir rekomendēts + Dati nav doti no saites, video lādēšanas neizdosies ja neiksestē saitē. + Apraksts + Nav apraksts atrasts + Apraksts nav atrasts + Radīt Logcat 🐈 + Log + Bilde bildē + Turpina spēlēt mazā lodziņā virs aplikācijām + Players izmēra poga + Noņemt melnās malas + Subtitri + Players subtitru iestādijumi + Chromecast subtitri + Chromecast subtitru iestāfijumi + Eigengravy Mode + Pievieno atskaņošanas ātrumu playerim + Novelc lai paradītu + Novelc no māla lidz malai lai pozicionētu video + Novēlu lai mainītu iestādījums + Novēlu uz augšu vai apakšu pa labi un pa kreisi lai lai nomainītu gaišumu un skaņu + Automātiski nākamo epizodi + Sākt nākamo epizodi kad šis bridzas + Divreiz uzpied lai paslēptu + Divreiz uzpied lai pauzētu + Players meklēšanas daudzums (sekundes) + Uzpied divreiz pa labi vai kreisi lai palaistu atpakaļ vai uz priekšu + Uzpied divreiz vidū lai pauzētu + Lietot sistēmas gaišums + Lietot sistēmas gaišumu aplikācijas playerī nevis tumšunu + Atjaunināt skatīšanos progresu + Automātiski sync savu pašreizējo epizodes progresu + Atgūt datus no backupa + Saglabāt datus + Ieladētie atgūtie faili + Neizdevās restaurēt datus no faila %s + Dati saglabāti + Krātuves atļaujas nav. Lūdzu mēģiniet vēlreiz. + Kļūda meiģinot saglabāt %s + Meklēt + Library + Konts + Atjaunināt un saglabāt + Informācija + Advancēta meklēšana + Dod tev meklēšanas rezultātus citus no devēja. + Tikai sūtīt datus no kļudām + Nesutīt datus + Radīt fillera epizodi priekš animē + Radīt feel + Radīt plakātu no kitsu + Slēpt izvēlētos video kvalitāti meklēšanas rezultātus + Automātiski papildinājumu atjauninājumi + Automātiski ielādēt papildinājumus + Automātiski instalēt visus neinstalētos papildinājumus no glabātavas + Radīt aplikācijas atjauninājumus + Automātiski meklēt jaunus atjauninājumus kad palaiž aplikāciju + Atsākt uzstādīšanas procesu + Atjaunināt uz priekšizlaišanu + Dažu telefoni nepieņem jauno aplikāciju instaletāju. Meiģiniet veco opciju ja nevar stjaunināt. + Noveles aplikācija no šiem izstrādātājiem + Anime aplikāciju no tiem pašiem izstradatājiem + Ienāc discordā + Iedot banānu izstrādātājiem + Iedotie banāni + Aplikācijas valoda + Šim devējam nav Chromecast pieņemšana + Nav linku strastu + Links kopēts cliobordā + Restartēt uz parasto value + Sezona + %s %d%s + Nav sezonas + Epizode + Epizodes + %d-%d + %d %s + S + E + Epizodes netika atrastas + Dzēsti faili + Dzēst + Pauzēt + Sākt + Neizdevās + Nokārtojāt + Atsākt + -30 + +30 + Šis pilnibā dzesīs %s +\nEsat parliecināts\? + %dm +\natlikušas + Pabeigts + Statuss + gads + Reitings + Ilgums + Saite + Synopsis + Gaida + Lietotie + Aplikācija + Filmas + Seriāli + Animācija + Anime + Torrenti + Dokumentārija + OVA + Āzijas Drāma + Livestreami + NSFW + Citi + Filmas + Sērijas + Animācija + Anime + OVA + Torrenti + Documentarijas + Āzijas drāma + Livestreami + NSFW + Video + Devēja kļūda + Remote kļūda + Negaidīta atskaņotāja kļūda + Ielādēšanas kļūda, pārbaudi atmiņas atļauju + Chromecast epizode + Chromecast morror + Palaist aplikācijā + Atskaņot uekšā %s + Atskaņot internetā + Kopēt linku + Automātiski ielādēt + Ielādēt spoguli + Pārlādēt saites + Ielādēt subtitrus + Kvalitāte + Dub lable + Subtirti + Nosaukums + Render kļūda + Pārbaudīt atjauninājumus + Slēgt + Mainīt lielumu + Devējs + Izlaist OP + Nerādīt atkal + Izlaist šo atjauninājumu + Atjauninājums + Izvēlētā skatīšanās kvalitāte (WiFi) + Video players nosaukuma maksimālie burti + Video atskaņotāja kvalitāte + Video buffer izmērs + Video buffering garums + Video atkritne diskā + Atskaņotājs rāda - seek smount + Meklēšanas summa, kas tiek izmantota, kad spēlētājs ir redzams + Atskaņotājs paslēpts — meklēšanas summa + Izraisa avārijas, ja ierīcēs ar mazu atmiņu ir iestatīta pārāk augsta vērtība, piemēram, Android TV. + Izdevīgs lai izlaistu ISO aizturi + raw.githubusercontent.com Proxy + Apiet GitHub bloķēšanu, izmantojot jsdelivr, tādēļ atjauninājumi var aizkavēties par dažām dienām. + Klonēt saiti + Noņemt saiti + Pievienojiet esošas vietnes klonu ar citu URL + Ielādēšanas ceļš + NGINX servera URL + Radīt Dubbed/Subbed Anime + Ietilpt ekranā + Atruna + ISP Izlaists + Links + Aplikācijas atjauninājumus + Dublējums + Papildinājumi + Akcijas + Atkritne + Android TV + Žesti + Atskaņošanas funkcijas + Subtitri + Iskats + Parasts + Izskats + Funkcijas + Ģenerāls + Randomā poga + Rādīt izlases pogu mājaslapā + Devēja valodas + Aplikācijas izskats + Izvēlētā media + Iespējojiet NSFW atbalstītajiem pakalpojumu sniedzējiem + Subtitru kodējums + Devēji + Devēju tests + Izskats + Automātiski + Televizora izskats + Telefona izskats + Emulators izskats + Plakāta virsraksta lokācija + Parole123 + MansStilīgaisNosaukums + Sveiki@pasaule.com + 127.0.0.1 + ManaForšāSaite + Piemērs.com + Valodas kods (lv) + Konts + Iziet + Ieiet + Mainīt kontu + Pievienot kontu + Veidot kontu + Pievienot izsekošanu + Pievienot %s + Sync + Lidzīgi + %d / 10 + /\?\? + /%d + %s autentificēts + Nevarēja ieiet %s + Nekas + Normāls + Viss + Maksimālais + Minimālais + Apkartlinija + Depresija + Ēna + Paugstināts + Sync subs + 1000 ms + Subtitru pslēninājums + Izmantojiet šo, ja subtitri tiek rādīti %d ms pārāk agri + Nav subtitru kavēšanās + Ātrā brūnā lapsa lec pāri slinkajam sunim + Ielādeja %s + Lādēt no faila + Aplikācijas theme + Ielādēt no interneta + Ieladētie faili + Galvenais + Atbalsta + Aizmugure + Devējs + Randoms + Camera + Kamera + HQ + HD + TS + TC + Blu-ray + WP + DVD + 4K + SD + UHD + HDR + Plakāta bilde + Atskaņotājs + Rezolūcija un tituls + Nederīgs ID + Nederīgi dati + Nederīgs URL + Kļūda + Noņemiet slēgtos parakstus no subtitriem + Noņemiet uzpūšanos no subtitriem + Filtrējiet pēc vēlamās multivides valodas + Ekstras + Treileris + Links uz tstresmu + Referents + Nākamais + Skatieties videoklipus šajās valodās + Iepriekšējais + Izlaist uzstādīšanu + Mainiet lietotnes izskatu, lai tā atbilstu savai ierīcei + Avārijas ziņošana + Ko tu vēlies redzēt + Pabeigts + Papildinājumi + Pievienot repozitoriju + Repository URL + Plugin ieladēti + Plugins dzēsts + Nevarēja ielādēt %s + 18+ + Sākta %d %s lejupielāde… + Lejuplādēts %d %s + Pakešu lejupielāde + Plugins + Plugins + Tādējādi tiks izdzēsti arī visi repozitorija pluginus + Dzēst repozitoriju + Lejupielādēt: %d + Atspējots: %d + Nav lejupielādēts: %d + Atjaunināti %d spraudņi + Skatīt kopienas krātuves + Publisks saraksts + Visi subtitri ar lielajiem burtiem + Vai lejupielādēt visus spraudņus no šīs krātuves\? + %s (atspējots) + Tracks + Audio dziesmas + Video tracks + Likt uz restartēšanu + Restartēt + Pārtraukt + Drošais režīms ieslēgts + Visi paplašinājumi tika izslēgti avārijas dēļ, lai palīdzētu jums atrast to, kas rada problēmas. + Skatīt avārijas informāciju + Rating: %s + Apraksts + Versija + Status + Izmērs + Autors + Atbalstīts + Valodas + HLS atskaņošanas saraksts + Vēlamais video atskaņotājs + Iekšējais atskaņotājs + MPV + Web video apraide + Aplikācijs nav atrasta + Visas valodas + Beigas + Kopsavilkums + Jauktas beigas + Jauktais sākums + Kredīts + Notīrīt vēsturi + Vēsture + Rādīt izlaižamos uznirstošos logus atvēršanai/beigšanai + "Pārāk daudz teksta. Nevar saglabāt starpliktuvē." + + + Notiek lietotnes atjauninājuma lejupielāde… + Notiek lietotnes atjauninājuma instalēšana… + Nevarēja instalēt jauno lietotnes versiju + Mantojums + Insteletājs + Lietotne tiks atjaunināta pēc iziešanas + Kārtot pēc + Kārtot + Vērtējums (no augsta līdz zemam) + Atjaunināts (no jauna uz veco) + Atjaunināts (no vecā uz jauno) + Alfabētiskā secībā (A līdz Z) + Alfabētiskā secībā (Z līdz A) + Atlasiet Bibliotēka + Atvērt ar + Šķiet, ka jūsu bibliotēka ir tukša :( +\n Piesakieties bibliotēkas kontā vai pievienojiet pārraides savai vietējai bibliotēkai + Atgriest + Anulēts %s abonements + %d sērija izlaista! + Meklēt pirmsizlaišanas atjauninājumus nevis tikai pilnos atjauninājumus + Apk insteletājs + Github + Nav subtitru + Atskaņot epizodi + Piedodiet, bet aplikācijā bija kļūda, anonīms kļūdas ziņojums tika aizsūtīts izstrādātājiem. + Iet + Bezmaksas + Ieslēgt elementus uz plakātiem + Parastais + Nav atjauninājumi atrasti + Izdzēst video un bildes atkritne + Izvēlētā skatīšanās kvalitāte (Mobilie Dati) + Rada problēmas, ja ierīcēs ar maz vietas krātuvē ir iestatīts pārāk augsts, piemēram, Android TV. + Meklēšanas summa, kas tiek izmantota, kad spēlētājs ir paslēpts + DNS virs HTTPS + Tuvināt + Neizdevās sasniegt GitHub, iespējot jsdelivr starpniekserveri. + Iztiept + Galvenā krāsa + %s %s + Likt nosaukumu zem plakāta + Izmantojiet šo, ja subtitri tiek rādīti %d ms pārāk vēlu + Rekomendācijas + Drīzumā… + Kamera + Virsraksts + Web + SDR + Rezulūcija + CloudStream pēc noklusējuma nav instalēta neviena vietne. Vietnes jāinstalē no krātuvēm. Sky UK Limited bezsmadzeņu DMCA noņemšanas dēļ mēs nevaram saistīt ar repozitorija vietni lietotnē. Pievienojieties mūsu Discord vai meklējiet tiešsaistē. + Viss %s jau ir lejupielādēts + Repozitorija nosaukums + Plugin ielādēti + Lejupielādējiet to vietņu sarakstu, kuras vēlaties izmantot + Vispirms instalējiet paplašinājumu + Atvēršana + VLC + Interneta mekletājs + Sākums + Izlaist %s + Noņemt no skatītajiem + Atzīmēt kā skatītu + Vai tiešām vēlaties iziet\? + Šķiet, ka šis saraksts ir tukšs, mēģiniet pārslēgties uz citu + Atrasts drošā režīma fails! +\n Paplašinājumi netiek ielādēti startēšanas laikā, kamēr fails nav noņemts. + Vērtējums (no zema līdz augstam) + Abonēto šovu atjaunināšana + Abonēts + Abonēts %s + \ 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 7964dce8..0e4a7aea 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -1,6 +1,5 @@ - - + Брзина (%.2fx) Оценето: %.1f @@ -188,7 +187,7 @@ Истегни Зумирај Disclaimer - Генерално + Општи поставки Јазици на провајдерите Распоред на апликацијата Претпочитани медиуми @@ -303,8 +302,8 @@ Инсталатор на пакети ОВА Ажурирања и резервни копии - Изгледа дека вашата библиотека е празна :( -\nНајавете се на сметка на библиотеката или додајте серии во вашата локална библиотека + Вашата библиотека е празна :( +\nНајавете се на корисничка сметка или додадете серии. Не се пронајдени епизоди Брзата кафеава лисица го прескокнува мрзливото куче Слика на постер @@ -353,14 +352,14 @@ Мешано отворање Екстензии Овозможете NSFW на поддржани провајдери - Не успеа да стигне до GitHub, овозможувајќи jsdelivr прокси. + Не успеа да стигне до GitHub. Вклучувам jsDelivr прокси… Филтрирајте по претпочитан медиумски јазик @string/home_play Филм Додаден %s приклучоци Подреди по - Изгледа дека оваа листа е празна, обидете се да се префрлите на друга + Оваа листа е празна, обидете се да се префрлите на друга. Аниме Износот на барањето што се користи кога плеерот е видлив Dub @@ -384,7 +383,7 @@ Прикажи постери од Kitsu Дали сте сигурни дека сакате да излезете\? Предизвикува проблеми ако е превисоко поставено на уреди со мал простор за складирање, како што е Android TV. - Користејќи jsdelivr, блокирањето на GitHub може да се заобиколи. Може да ги одложи ажурирањата за неколку дена. + Користејќи jsDelivr, блокирањето на GitHub може да се заобиколи. Може да ги одложи ажурирањата за неколку дена. Да Азбучно (Ш до А) WP @@ -519,4 +518,18 @@ Поставете статус на пратење Пушти Livestream %s е автентициран - + %s Епизода %d + %dч %dм + %dм + Следен рандом + Постер + Постер + %dд %dч %dм + Главен постер + Епизодата %d ќе биде објавена на + Постер за епизода + Прегледај позадина + Смени провајдер + Оди назад + Актери: %s + \ No newline at end of file diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index a246cf9c..1f653286 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -1,6 +1,5 @@ - - + വേഗം (%.2fx) റേറ്റിംഗ്: %.1f @@ -170,4 +169,13 @@ ഔചിത്യ വീഡിയോ ക്വാളിറ്റി ചരിത്രം കണ്ടതാണെന്ന് അടയാളപ്പെടുത്തുക - + %dd% + yg5t4r%dujyhtg + qeWERT + %fghj%gf + rtf:% + അക്കൗണ്ട് ഉണ്ടാക്കുക + പുറത്ത്പോകുന്നതോടുകൂടി ആപ് അപ്ഡേറ്റ് ആവുന്നതാണ് + ലൈബ്രറി തിരഞ്ഞെടുക്കുക + ഇത് ഉപയോഗിച്ച് തുറക്കുക + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 5cac7dfd..d7e3ede6 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1,6 +1,5 @@ - - + %s Ep %d Cast: %s @@ -142,7 +141,7 @@ Kijkvoortgang bijwerken Automatisch synchroniseren van je huidige episode vooruitgang Gegevens herstellen vanaf back-up - Back up gegevens + Back-up gegevens Geladen back-up bestand Kan gegevens uit bestand niet herstellen %s De gegevens zijn opgeslagen @@ -441,7 +440,7 @@ Installeer eerst de uitbreiding Alle Talen Installeert automatisch alle nog niet geïnstalleerde plugins uit toegevoegde repositories. - Kan GitHub niet bereiken, schakel jsdelivr proxy in. + Kan GitHub niet bereiken, schakel jsDelivr proxy in… APK Installatie Automatisch plugins downloaden Uitbreidingen @@ -453,7 +452,7 @@ Repository naam Plugin Gedownload Mislukt - Omzeilt de blokkering van GitHub met behulp van jsdelivr, waardoor updates enkele dagen vertraging kunnen oplopen. + Omzeilt de blokkering van GitHub met behulp van jsDelivr, waardoor updates enkele dagen vertraging kunnen oplopen. Repository URL Download %d %s voltooid HLS Afspeellijst @@ -494,7 +493,7 @@ Beoordeling: %s Alle extensies zijn uitgeschakeld door een crash om u te helpen degene te vinden die problemen veroorzaakt. Bekijk de crash info - Deze lijst is blijkbaar leeg, probeer een andere lijst te kiezen + Deze lijst is leeg. Probeer een andere. Alfabetisch (A tot Z) Weet je zeker dat je wilt afsluiten\? Bijgewerkt (Oud naar Nieuw) @@ -509,8 +508,8 @@ Verwijderen uit bekeken App wordt bijgewerkt bij afsluiten Gesorteerd - Het lijkt erop dat je bibliotheek leeg is :( -\nLog in op een bibliotheekaccount of voeg voorstellingen toe aan uw lokale bibliotheek + Je bibliotheek is leeg :( +\nLog in op een bibliotheekaccount of voeg voorstellingen toe aan uw lokale bibliotheek. Uitgeschakeld: %d Stop Niet gedownload: %d @@ -552,4 +551,4 @@ \nWord lid van onze Discord of zoek online. Audiosporen Gesorteerd op - + \ 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 4e7f6abd..6c9e3a67 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -492,4 +492,45 @@ Oppdatering startet Programtillegg nedlastet Programmet vil oppgraderes når du avslutter det - + Blafringsmengde med synlig avspiller + Start på ny + raw.githubusercontent.com-mellomtjener + Sorter etter + Åpne med … + Vurdering (høy til lav) + Start + Alfabetisk (A-Å) + Kunne ikke nå GitHub. Skrur på jsDelivr-mellomtjener … + Tilbyder-test + Bibliotek + Nettleser + Logg + Oppdatert (ny til gammel) + Skjult avspiller — blafringsmengde + Abonnert + Vist avspiller — blafringsmengde + Oppdatert (gammel til ny) + Vellykket + Episode %d sluppet. + Foretrukket visningskvalitet (mobildata) + Stopp + Fjern fra sette + Abonnement på %s opphevet + Android TV + Angre + Oppdatert abonnementer + Mislykket + Alfabetisk (Å-A) + Vurdering (lav til høy) + Abonnerer på %s + Blafringsmengde med skjult avspiller + Velg bibliotek + Omgår blokkering av GitHub ved bruk av jsDelivr. Kan utsette oppdateringer et par dager. + ISP-omgåelser + Denne listen er tom. Prøv å bytte til en annen. + Sorter + Fant fil for trygt modus. +\nLaster ikke inn noen utvidelser ved oppstart til filen er fjernet. + Biblioteket ditt er tomt :( +\nLogg inn på en bibliotekkonto eller legg til programmer i ditt lokale bibliotek. + \ 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 863b2c2f..9801a557 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1,6 +1,5 @@ - - + Prędkość (%.2fx) Ocena: %.1f Znaleziono nową aktualizację! @@ -505,9 +504,9 @@ 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ą + Twoja biblioteka jest pusta :( +\nZaloguj się na swoje konto lub dodaj programy do swojej lokalnej biblioteki. + 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. Używana ilość przewijania, gdy widoczny jest odtwarzacz @@ -530,7 +529,7 @@ Zasubskrybowano %s Anulowano subskrypcję %s Został wydany odcinek %d! - Obchodzi blokadę GitHuba za pomocą jsdelivr, może spowodować opóźnienie aktualizacji o kilka dni. - Nie udało się połączyć z GitHub, włączono serwer pośredniczący jsdelivr. + Obchodzi blokadę GitHuba za pomocą jsDelivr. może spowodować opóźnienie aktualizacji o kilka dni. + Nie udało się połączyć z GitHub, włączono serwer pośredniczący jsDelivr… Domyślna jakość (dane mobilne) - + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index f34dec8f..261051ed 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -444,7 +444,7 @@ Cam Abertura Selecionar Biblioteca - Ignora o bloqueio do GitHub usando jsdelivr, pode fazer com que as atualizações sejam atrasadas em alguns dias. + Ignora o bloqueio do GitHub usando jsDelivr. Pode fazer com que as actualizações sejam atrasadas por alguns dias. VLC Todas as linguagens Atualizado (Novo para Antigo) @@ -483,7 +483,7 @@ Pular %s Abertura mista Alfabético (Z a A) - Parece que esta lista está vazia, tente trocar para outra + Esta lista está vazia. Tente trocar para outra. Inscrito em %s 4K Faixas de vídeo @@ -491,8 +491,8 @@ Atualizando shows inscritos Alfabético (A a Z) Avaliações (Crescente) - Parece que a sua biblioteca está vazia :( -\nFaça login em uma conta de biblioteca ou adicione shows à sua biblioteca local + A sua biblioteca está vazia :( +\nEntre numa conta da biblioteca ou adicione espectáculos à sua biblioteca local. Arquivo de modo de segurança encontrado! \nNenhuma extensão será carregada na inicialização do app até que o arquivo seja removido. Contorno do provedor de serviço de internet (ISP) @@ -503,7 +503,7 @@ Qualidade Preferida (Dados Móveis) Quantidade de busca (em segundos) usada quando o player de video está visível Quantidade de busca (em segundos) usada quando o player de video está oculto - Falha ao conectar com GitHub, ativando proxy jsdelivr. + Não foi possível chegar ao GitHub. Ativando o proxy jsDelivr… Cache Android TV Legendas @@ -529,4 +529,4 @@ Configurações padrão SD Faixas de áudio - + \ 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 99e112ce..ba8c1af7 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1,6 +1,5 @@ - - + %s Ep %d Distribuție: %s @@ -126,14 +125,14 @@ Modul Eigengravy Adăugați opțiunea de viteză în player Derulați spre înainte/înapoi - Derulați spre stânga sau spre dreapta pentru a controla timpul de difuzare a videoclipului + Glisați dintr-o parte în alta pentru a vă controla poziția într-un videoclip Derulați pentru a modifica setările Glisați spre stânga sau spre dreapta pentru a schimba luminozitatea sau volumul Atingeți de două ori pentru a merge înainte/înapoi Atingeți de două ori pentru a pune pauză Atingeți de două ori partea stângă sau dreaptă a ecranului pentru a derula rapid înainte sau înapoi videoclipul - Atingeți de două ori centrul ecranului pentru a întrerupe înregistrarea video - Atingeți dublu pentru a căuta + Atingeți de două ori în mijloc pentru a pune pauză + Cantitatea de căutare al player-ului (secunde) Utilizați luminozitatea sistemului Utilizați luminozitatea sistemului în playerul aplicației în loc de o suprapunere întunecată @@ -157,7 +156,7 @@ Arată trailerul Arată afișele de la Kitsu Afișați actualizările aplicației - Căutați automat noi actualizări la pornire + Căutați automat noi actualizări după pornirea aplicației. Actualizați la prerelease Căutați actualizări ale versiunilor preliminare în loc să căutați doar versiunile complete GitHub @@ -251,22 +250,22 @@ Nu se mai arată din nou Treci peste această actualizare Actualizare - Rezoluția preferată + Calitatea preferată (WiFi) Limitarea caracterelor de titlu în player Rezoluția playerului video Dimensiunea cache-ului video Lungimea buffer-ului video Dimensiunea cache-ului video pe disc Ștergeți memoria cache de imagine și video - Dacă este setat la un nivel prea ridicat, poate cauza probleme pe sistemele cu prea puțină memorie RAM. Cum ar fi dispozitivele Android TV sau telefoanele mai vechi - Dacă este setat la un nivel prea ridicat, poate cauza probleme pe sistemele cu spațiu de stocare intern redus. Ca și dispozitivele Android TV + Provoacă blocaje dacă este setată la un nivel prea ridicat pe dispozitive cu memorie redusă, cum ar fi Android TV. + Cauzează probleme dacă este setat la un nivel prea ridicat pe dispozitive cu spațiu de stocare redus, cum ar fi Android TV. DNS peste HTTPS Folositor pentru evitarea blocajelor ISP Adaugați site-ul Eliminați site-ul Adăugați o copie a unui site existent, cu o adresă URL diferită Locul descărcării - Adresa URL a serverului Nginx + Adresa URL a serverului NGNIX Afișarea anime-urilor dublate/subtitrate Adaptare la ecran Întindere @@ -286,7 +285,7 @@ Dispunere telefonică Dispunerea emulatorului Culoare primară - Tema + Tema aplicației Locația titlului posterului Locația titlului de pe poster @@ -312,7 +311,7 @@ /\?\? /%d %s autentificat - Imposibil de autentificat la %s + Nu s-a putut autentifica la %s Nu există Normal @@ -324,10 +323,10 @@ Umbră Relief Sincronizare subtitrări - 1000ms + 1000 ms Delay subtitrare - Se utilizează dacă subtitrările sunt afișate %dms prea devremeo - Se utilizează dacă subtitrările sunt afișate %dms prea târziu + Utilizează acesta dacă subtitrările sunt afișate %d ms prea devreme + Utilizează acesta dacă subtitrările sunt afișate %d ms prea târziu Fără întârziere la subtitrare Trailer Istoric - Marcare ca vizionat + Marcați ca vizionat Redă automat următorul episod CloudStream Vizionează trailerul @@ -391,4 +390,162 @@ Joacă cu CloudStream Actualizare plugin automată Descarcă plugin-uri automat - + Altele + Trecut + Start + %d %s + NSFW + %d-%d + Player Afișat - Căutați Suma + Player Ascuns - Căutați Suma + Livestream-uri + NSFW + Eșuat + Cantitatea de căutare utilizată atunci când playerul este vizibil + Livestream + Cantitatea de căutare utilizată atunci când playerul este ascuns + Calitatea preferată (Date Mobile) + Video + Instalator APK + Instalează automat toate plugin-urile neinstalate din depozite adăugate. + %s %d%s + Unele telefoane nu acceptă noul program de instalare a pachetului. Încercați opțiunea veche dacă nu se instalează actualizările. + Refaceți procesul de configurare + Treci peste partea de configurare + Descărcare pe loturi + 18+ + Evaluare: %s + Treci peste %s + Aplicația nu a fost găsită + Încheiere mixat + Ștergeți istoricul + Introducere + Da + Ce vrei să vezi + Recapitulare + Alfabetic (A la Z) + Încheiere + Dezabonat de la %s + Nu s-a descărcat: %d + Vezi depozite din comunitate + PackageInstaller (Instalare a pachetelor) + Stare + Nu se poate încărca %s + Piste audio + Referent + Deschidere + Extensii + Layout + Prea mult text. Nu s-a putut salva în clipboard. + Linkuri + Funcții + Autori + Raportarea accidentelor + Adaugă depozit + Se pare că biblioteca ta este goală :( +\nConectează-te la un cont de bibliotecă sau adaugă emisiuni în biblioteca ta locală + Eliminați subtitrările închise din subtitrări + Descărcați lista de site-uri pe care doriți să le utilizați + Evaluare (Ridicat la Scăzut) + Extensii + Ștergeți depozitul + Dimensiune + Cache + Funcțiile player-ului + Plugin încărcat + Vezi informații despre accident + Deschideți cu + Eliminați bloat din subtitrări + Actualizat %d plugin-uri + Evaluare (Scăzut la Ridicat) + Terminat + Versiune + Backup + Suplimente + Actualizat (Nou la Vechi) + Schimbați aspectul aplicației pentru a se potrivi dispozitivului dvs + Nume de depozit + %s (Dezactivat) + Nu + Abonat la %s + Aplicația va fi actualizată la ieșire + Web Video Cast + Ocoliri ISP + Anterior + Sortează + Selectați Biblioteca + Filtrați în funcție de limba media preferată + Episodul %d lansat! + Android TV + VLC + Urmăriți videoclipuri în aceste limbi + Reveniți + Acțiuni + Alfabetic (Z la A) + URL invalid + Toate extensiile au fost dezactivate din cauza unei defecțiuni pentru a vă ajuta să o găsiți pe cea care cauzează probleme. + Se descarcă actualizarea aplicației… + Browser web + CloudStream nu are niciun site instalat în mod implicit. Trebuie să instalați site-urile din depozite. +\n +\nDin cauza unui DMCA takedown fără creier de către Sky UK Limited 🤮 nu putem lega site-ul de depozit în aplicație. +\n +\nAlăturați-vă Discordului nostru sau căutați online. + A început să descarce %d %s… + Mod sigur pornit + Fișier Mod Sigur găsit! +\nNu încarcă nicio extensie la pornire până când fișierul nu este eliminat. + Scoateți de la urmărit + Actualizat (Vechi la Nou) + Aplică la repornire + Descriere + Plugin Descărcat + Sunteți sigur că vreți să ieșiți\? + Se pare că această listă este goală, încercați să treceți la o alta + Sortați după + Player intern + Prestabile + URL-ul depozitului + Oprește + Aspecturi + Plugin Șters + Gesturi + Nu s-a putut instala noua versiune a aplicației + Piste + Repornește + Activează NSFW la furnizori suportate + Nu s-a putut ajunge la GitHub. Se activează proxy-ul jsDelivr… + Proxy raw.githubusercontent.com + Depășește blocarea GitHub folosind jsdelivr, poate cauza întârzieri de câteva zile la actualizări. + Următorul + Toate %s deja descărcate + S-a descărcat: %d + Dezactivat: %d + Toate subtitrările cu majuscule + Descărcați toate plugin-urile din acest depozit\? + Se actualizează emisiunile abonate + Abonat + Lista publică + MPV + Moştenit + Test de furnizor + Furnizori + Link către stream + Acest lucru va șterge, de asemenea, toate plugin-urile din depozit + Se instalează actualizarea aplicației… + S-a descărcat %d %s + Suportat + Playlist HLS + Piste video + Arată Afișați pop-up-uri de săritură pentru deschidere/încheiere + Toate limbile + Deschidere mixat + Credite + Limbă + plugin + plugin-uri + Instalați mai întâi extensia + Player video preferat + Actualizări al aplicației + Subtitrări + \ 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 b5601da3..465773b6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -439,8 +439,8 @@ Алфавитный (Я до А) Выбрать библиотеку Открыть с - Похоже, ваша библиотека пуста :( -\nВойдите в аккаунт с библиотекой или добавьте сериалы в локальную библиотеку + Ваша библиотека пуста :( +\nВойдите в аккаунт с библиотекой или добавьте сериалы в локальную библиотеку. Сортировка Открытый список Рейтинг (высокий - низкий) @@ -487,13 +487,13 @@ Файл безопасного режима найден! \nНе загружаются никакие расширения при запуске, пока файл не будет удален. Приложение будет обновлено после выхода - Похоже, этот список пуст, попробуйте переключиться на другой + Этот список пуст, попробуйте переключиться на другой. Все субтитры заглавными Показывать всплывающие окна для пропуска вступления/заключения Фильтровать по предпочитаемому языку медиа Неверный ID Ссылка на стрим - Показывает кнопку на главной странице, с помощью которой можно выбрать случайный фильм или сериал с главной страницы + Отображать рандомную кнопку на Главной странице Рандомная кнопка Legacy (старый) Веб видеокаст @@ -522,11 +522,11 @@ Подписался на %s Предпочтительное качество видео (Мобильный интернет) raw.githubusercontent.com Прокси-сервер - Не удалось подключиться к GitHub. Будет выполнен прокси jsdelivr. + Не удалось подключиться к GitHub. Включаем проксирование через jsdelivr… Эпизод %d выпущен! Обходы провайдера Обновление подписки на фильмы и сериалы - Обход ограничения доступа к GitHub с помощью jsdelivr может задержать обновления на несколько дней. + Обход ограничения доступа к GitHub с помощью jsDelivr может задержать обновления на несколько дней. Подписные Отказались от подписки на %s - + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 5b543915..c84de758 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,6 +1,5 @@ - - + %d %s | %s %s • %s @@ -311,7 +310,7 @@ 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 İçerik - Ana sayfada rastgele bir film veya dizi seçen bir tuş gösterir + Ana sayfada rastgele bir film veya dizi seçen bir tuş göster Sağlayıcı dilleri Uygulama düzeni Tercih edilen medya @@ -565,7 +564,7 @@ Tercih edilen görüntü kalitesi (Mobil veri) Oynatıcı görünürken atlanacak süre Oynatıcı gizli durumdayken atlanacak süre miktarı - jsdelivr kullanarak GitHub kısıtlamasını aşar. Güncellemeler birkaç gün gecikebilir. + jsdelivr kullanarak GitHub kısıtlamasını kaldırır, güncellemelerin birkaç gün gecikmesine neden olabilir. Android TV Yeni bölüm %d yayınlandı! Sağlayıcıyı kontrol et @@ -578,4 +577,4 @@ %s kanalı aboneliğinden çıkıldı Günlük Oynatıcı görünür durumdayken atlanacak süre miktarı - + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 2e7f4789..084d1ad3 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -427,9 +427,9 @@ Вимкнено: %d Не завантажено: %d Оновлено %d плагіни - За замовчуванням в CloudStream не встановлені сайти. Вам потрібно встановити сайти з репозиторіїв. + CloudStream не має жодного сайту, встановленого за замовчуванням. Вам потрібно встановити сайти з репозиторіїв. \n -\nЧерез безмозкий DMCA від Sky UK Limited 🤮 ми не можемо прив\'язати сайт репозиторію в застосунку. +\nЧерез безглузду заявку DMCA від Sky UK Limited 🤮 ми не можемо надати посилання на репозиторій в застосунку. \n \nПриєднуйтесь до нашого Discord або шукайте в інтернеті. Переглянути репозиторії спільноти @@ -485,7 +485,7 @@ Увімкнено безпечний режим Автори Завантаження оновлення програми… - Усі розширення вимкнено через збій, щоб допомогти вам знайти те, що спричиняє проблеми. + Усі розширення були вимкнені через збій, щоб допомогти вам знайти те, що стало причиною проблеми. Програму не знайдено Змішаний опенінг Видалити з переглянутого @@ -497,13 +497,13 @@ Сортувати за За алфавітом (від А до Я) За рейтингом (від низького до високого) - Схоже, ваша бібліотека порожня :( -\nУвійдіть в обліковий запис бібліотеки або додайте серіали до вашої локальної бібліотеки + Ваша бібліотека порожня :( +\nУвійдіть в обліковий запис бібліотеки або додайте фільми до вашої локальної бібліотеки. За алфавітом (від Я до А) Виберіть бібліотеку Відкрити з Браузер - Схоже, цей список порожній, спробуйте перейти до іншого + Цей список порожній. Спробуйте перейти до іншого. Файл безпечного режиму знайдено! \nРозширеня не завантажуються під час запуску, доки файл не буде видалено. Android TV @@ -525,8 +525,8 @@ Епізод %d випущено! Повернути raw.githubusercontent.com Proxy - Не вдалося зв\'язатися з GitHub, увімкнувши проксі-сервер jsdelivr. + Не вдалося отримати доступ до GitHub. Увімкнення проксі-сервера jsDelivr… Обходи ISP - Обхід блокування GitHub за допомогою jsdelivr, може призвести до затримки оновлень на кілька днів. + Обхід блокування GitHub за допомогою jsDelivr. Можлива затримка оновлень на кілька днів. Бажана якість перегляду (Мобільні дані) - + \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 71d97abc..1d3fedae 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1,6 +1,5 @@ - - + %d %s | %s %s • %s @@ -312,7 +311,7 @@ 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. 通用 随机按钮 - 在主页上显示按钮,可以从主页上随机选择电影或电视剧 + 在主页中显示随机按钮 片源语言 应用布局 首选类型 @@ -552,9 +551,9 @@ 字母排序(从 Z 到 A) 选择库 打开方式 - 看来您的库是空的 :( -\n登录库账户或添加节目到您的本地库 - 看来此列表是空的,请尝试切换到另一个 + 您的库是空的 :( +\n登录库账户或添加节目到您的本地库。 + 此列表是空的,请尝试切换到另一个。 播放器显示 - 快进快退秒数 播放器可见时使用的快进快退秒数 播放器隐藏 - 快进快退秒数 @@ -573,9 +572,9 @@ 成功 日志 raw.githubusercontent.com 代理 - 连接 Github 失败,正在启用 jsdelivr 代理。 - 使用 jsdelivr,可以绕过 GitHub 的封锁。可能会延迟几天的更新。 + 无法访问 GitHub。正在开启 jsDelivr 代理… + 使用 jsDelivr 绕过 GitHub 的封锁。可能会延迟几天的更新。 ISP 绕过 还原 首选播放画质(移动数据) - + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ac76e243..8f67739d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -382,8 +382,8 @@ DNS over HTTPS Useful for bypassing ISP blocks raw.githubusercontent.com Proxy - Failed to reach GitHub, enabling jsdelivr proxy. - Bypasses blocking of GitHub using jsdelivr, may cause updates to be delayed by few days. + Could not reach GitHub. Turning on jsDelivr proxy… + Bypasses blocking of GitHub using jsDelivr. May cause updates to be delayed by few days. Clone site Remove site Add a clone of an existing site, with a different URL @@ -648,8 +648,9 @@ 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 + Your library is empty :( +\nLog in on a library account or add shows to your local library. + This list is empty. Try switching to another one. Safe mode file found!\nNot loading any extensions on startup until file is removed. Revert Updating subscribed shows From 56a680fa9cbfd83110e836cae96a20ed4c797f25 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 11:56:05 +0000 Subject: [PATCH 069/570] chore(locales): fix locale issues --- .../lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 3 +++ app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-ars/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 5 +++-- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 7 ++++--- app/src/main/res/values-hr/strings.xml | 5 +++-- app/src/main/res/values-in/strings.xml | 5 +++-- app/src/main/res/values-it/strings.xml | 5 +++-- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-ko/strings.xml | 4 ++-- app/src/main/res/values-lv/strings.xml | 4 ++-- app/src/main/res/values-mk/strings.xml | 5 +++-- app/src/main/res/values-ml/strings.xml | 5 +++-- app/src/main/res/values-nl/strings.xml | 5 +++-- app/src/main/res/values-no/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 5 +++-- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 5 +++-- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 5 +++-- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 5 +++-- 24 files changed, 53 insertions(+), 38 deletions(-) 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 4aa859aa..5a000eb4 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 @@ -57,6 +57,7 @@ fun getCurrentLocale(context: Context): String { val appLanguages = arrayListOf( /* begin language list */ Triple("", "العربية", "ar"), + Triple("", "ars", "ars"), Triple("", "български", "bg"), Triple("", "বাংলা", "bn"), Triple("\uD83C\uDDE7\uD83C\uDDF7", "português brasileiro", "bp"), @@ -76,6 +77,8 @@ val appLanguages = arrayListOf( Triple("\uD83C\uDDEE\uD83C\uDDF1", "עברית", "iw"), Triple("", "日本語 (にほんご)", "ja"), Triple("", "ಕನ್ನಡ", "kn"), + Triple("", "한국어", "ko"), + Triple("", "latviešu valoda", "lv"), Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), Triple("", "bahasa Melayu", "ms"), diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 637e8c15..c1f07d6c 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -561,4 +561,4 @@ تجاوز حظر GitHub باستخدام jsdelivr ، قد يتسبب في تأخير التحديثات لبضعة أيام. وكيل raw.githubusercontent.com جودة المشاهدة المفضلة (بيانات الجوال) - \ No newline at end of file + diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index a6b3daec..42eba3cc 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 43ded674..4424529a 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -1,5 +1,6 @@ - + + %s Ep %d @@ -553,4 +554,4 @@ Vrátit zpět Obchází blokování GitHubu pomocí jsDelivr. Může způsobit zpoždění aktualizací o několik dní. Obcházení ISP - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 071f30c0..e0a9594c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -529,4 +529,4 @@ Rückgängig Abonniert ISP-Umgehungen - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index b863479e..d248044d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -529,4 +529,4 @@ Revertir ISP Bypasses Calidad de visualización preferida (Datos móviles) - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b7c9900b..62e41fdb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,5 +1,6 @@ - + + CloudStream Accueil Rechercher @@ -385,7 +386,7 @@ 4K Web -30 - \@string/anime + @string/anime OAV NSFW %s %s @@ -530,4 +531,4 @@ Joueur représenté - Montant de la recherche Joueur caché - Montant de la recherche Impossible d\'accéder à GitHub. Activation du proxy jsDelivr… - \ No newline at end of file + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 2491c3ce..41b95aad 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -1,5 +1,6 @@ - + + %d %s | %s %s • %s @@ -554,4 +555,4 @@ Neuspješno dohvaćanje GitHuba. Uključuje se jsdelivr proxy … Zaobilazi blokiranje GitHuba koristeći jsdelivr. Može odgoditi ažuriranja za nekoliko dana. Preferirana kvaliteta gledanja (podatkovna mobilna mreža) - \ 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 af02b9d0..15c09228 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -1,5 +1,6 @@ - + + %s Ep %d Pemeran: %s @@ -552,4 +553,4 @@ Bypass ISP Pulihkan Nonton dengan kualitas yang di inginkan (Data Seluler) - \ 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 248cb230..25b8ca5a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,5 +1,6 @@ - + + %s Ep %d Cast: %s @@ -551,4 +552,4 @@ Aggiornando shows a cui sei iscritto L\'episodio %d è stato rilasciato! Qualità di visualizzazione preferita (Dati mobili) - \ No newline at end of file + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 127f60b7..347712d8 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -198,4 +198,4 @@ \nSky UK Limitedによる無脳なDMCAテイクダウンのため🤮、アプリ内でリポジトリサイトをリンクすることができません。 \n \n私たちのDiscordに参加するか、オンラインで検索してください。 - \ No newline at end of file + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index fdadb4ae..74c05d07 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -497,7 +497,7 @@ 개발자에게 %d 바나나 줌 바나나를 주지 않음 상세 정보 - \@string/home_play + @string/home_play 플롯을 찾을 수 없음 설명을 찾을 수 없음 Logcat 🐈 표시 @@ -529,4 +529,4 @@ 구독중 구독 %s 구독 취소 %s - \ No newline at end of file + diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 8659a139..56724a28 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -108,7 +108,7 @@ Turpini skatīties Noņemt Vairāk informācijas - \@string/home_play + @string/home_play VPNs varētu būt vajadzīgs lai šis devējs strādātu pareizi Šis devējs ir Torrents vpn ir rekomendēts Dati nav doti no saites, video lādēšanas neizdosies ja neiksestē saitē. @@ -525,4 +525,4 @@ Abonēto šovu atjaunināšana Abonēts Abonēts %s - \ 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 0e4a7aea..d217f97f 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -1,5 +1,6 @@ - + + Брзина (%.2fx) Оценето: %.1f @@ -532,4 +533,4 @@ Смени провајдер Оди назад Актери: %s - \ No newline at end of file + diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 1f653286..feff0673 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -1,5 +1,6 @@ - + + വേഗം (%.2fx) റേറ്റിംഗ്: %.1f @@ -178,4 +179,4 @@ പുറത്ത്പോകുന്നതോടുകൂടി ആപ് അപ്ഡേറ്റ് ആവുന്നതാണ് ലൈബ്രറി തിരഞ്ഞെടുക്കുക ഇത് ഉപയോഗിച്ച് തുറക്കുക - \ No newline at end of file + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index d7e3ede6..e640a28a 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1,5 +1,6 @@ - + + %s Ep %d Cast: %s @@ -551,4 +552,4 @@ \nWord lid van onze Discord of zoek online. Audiosporen Gesorteerd op - \ 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 6c9e3a67..92882faf 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -533,4 +533,4 @@ \nLaster ikke inn noen utvidelser ved oppstart til filen er fjernet. Biblioteket ditt er tomt :( \nLogg inn på en bibliotekkonto eller legg til programmer i ditt lokale bibliotek. - \ 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 9801a557..1071a9b3 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1,5 +1,6 @@ - + + Prędkość (%.2fx) Ocena: %.1f Znaleziono nową aktualizację! @@ -532,4 +533,4 @@ Obchodzi blokadę GitHuba za pomocą jsDelivr. może spowodować opóźnienie aktualizacji o kilka dni. Nie udało się połączyć z GitHub, włączono serwer pośredniczący jsDelivr… Domyślna jakość (dane mobilne) - \ No newline at end of file + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 261051ed..705285eb 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -529,4 +529,4 @@ Configurações padrão SD Faixas de áudio - \ 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 ba8c1af7..bd22fb33 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1,5 +1,6 @@ - + + %s Ep %d Distribuție: %s @@ -548,4 +549,4 @@ Player video preferat Actualizări al aplicației Subtitrări - \ 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 465773b6..bcd3fc0f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -529,4 +529,4 @@ Обход ограничения доступа к GitHub с помощью jsDelivr может задержать обновления на несколько дней. Подписные Отказались от подписки на %s - \ No newline at end of file + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index c84de758..2ee7b65f 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,5 +1,6 @@ - + + %d %s | %s %s • %s @@ -577,4 +578,4 @@ %s kanalı aboneliğinden çıkıldı Günlük Oynatıcı görünür durumdayken atlanacak süre miktarı - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 084d1ad3..c58dd334 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -529,4 +529,4 @@ Обходи ISP Обхід блокування GitHub за допомогою jsDelivr. Можлива затримка оновлень на кілька днів. Бажана якість перегляду (Мобільні дані) - \ No newline at end of file + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 1d3fedae..f63e1d75 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1,5 +1,6 @@ - + + %d %s | %s %s • %s @@ -577,4 +578,4 @@ ISP 绕过 还原 首选播放画质(移动数据) - \ No newline at end of file + From fb3576ea52e12f5c1a71f0bbcc1ad72f70dabea5 Mon Sep 17 00:00:00 2001 From: Horis <821938089@qq.com> Date: Fri, 21 Apr 2023 19:56:17 +0800 Subject: [PATCH 070/570] fix video download (#453) --- .../com/lagradost/cloudstream3/utils/VideoDownloadManager.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 2902b76b..c18ff48f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -1388,7 +1388,7 @@ object VideoDownloadManager { } } - if (link.isM3u8 || URI(link.url).path.endsWith(".m3u8")) { + if (link.isM3u8 || URL(link.url).path.endsWith(".m3u8")) { val startIndex = if (tryResume) { context.getKey( KEY_DOWNLOAD_INFO, @@ -1474,6 +1474,8 @@ object VideoDownloadManager { if (connectionResult != null && connectionResult > 0) { // SUCCESS removeKey(KEY_RESUME_PACKAGES, id.toString()) break + } else if (index == item.links.lastIndex) { + downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) } } } catch (e: Exception) { From 42bf8ed08e934e3b5c68fe2966ebf07b43e30532 Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Wed, 3 May 2023 22:16:35 +0200 Subject: [PATCH 071/570] Update newpipe (#462) --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fa1b277a..7110c43d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -216,8 +216,8 @@ dependencies { // slow af yt //implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT") - // newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L190 - implementation("com.github.TeamNewPipe:NewPipeExtractor:9ffdd0948b2ecd82655f5ff2a3e127b2b7695d5b") + // newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204 + implementation("com.github.TeamNewPipe:NewPipeExtractor:v0.22.6") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6") // Library/extensions searching with Levenshtein distance From 8a5ddcd1267244d27c99eba361e78a7083b59023 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 3 May 2023 21:45:40 +0200 Subject: [PATCH 072/570] Added translation using Weblate (Odia) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translated using Weblate (Vietnamese) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Latvian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Vietnamese) Currently translated at 99.3% (606 of 610 strings) Translated using Weblate (Malayalam) Currently translated at 41.4% (253 of 610 strings) Translated using Weblate (Vietnamese) Currently translated at 96.3% (588 of 610 strings) Translated using Weblate (Hungarian) Currently translated at 83.6% (510 of 610 strings) Translated using Weblate (Slovak) Currently translated at 72.4% (442 of 610 strings) Translated using Weblate (Slovak) Currently translated at 44.9% (274 of 610 strings) Translated using Weblate (Czech) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Slovak) Currently translated at 34.2% (209 of 610 strings) Co-authored-by: Bojtár Zsömle Co-authored-by: Dinh Nguyen Co-authored-by: Hosted Weblate Co-authored-by: Juraj Liso Co-authored-by: Khoi Co-authored-by: Kiên Tài Co-authored-by: Subham Jena Co-authored-by: akku vijay Co-authored-by: liva Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ml/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/ Translation: Cloudstream/App --- app/src/main/res/values-cs/strings.xml | 7 +- app/src/main/res/values-hu/strings.xml | 5 +- app/src/main/res/values-lv/strings.xml | 34 ++-- app/src/main/res/values-ml/strings.xml | 27 ++- app/src/main/res/values-or/strings.xml | 2 + app/src/main/res/values-sk/strings.xml | 254 ++++++++++++++++++++++++- app/src/main/res/values-vi/strings.xml | 41 ++-- 7 files changed, 334 insertions(+), 36 deletions(-) create mode 100644 app/src/main/res/values-or/strings.xml diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 4424529a..36f5d3c7 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -1,6 +1,5 @@ - - + %s Ep %d @@ -132,7 +131,7 @@ Klepněte dvakrát vpravo nebo vlevo pro posun vpřed nebo vzad Klepněte dvakrát 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ý jas namísto tmavého překrytí Aktualizovat postup sledování Automaticky synchronizovat postup sledování současné epizody Obnovit data ze zálohy @@ -554,4 +553,4 @@ Vrátit zpět Obchází blokování GitHubu pomocí jsDelivr. Může způsobit zpoždění aktualizací o několik dní. Obcházení ISP - + \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 7c2bbc18..8ed09726 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -57,7 +57,7 @@ Megnyitás böngészőben Betöltés kihagyása Poster - @string/result_poster_img_des + \@string/result_poster_img_desPoszter Nézés Befejezve Később megnézés @@ -495,4 +495,5 @@ Beállítás kihagyása HQ %d letöltve - + Start + \ No newline at end of file diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 56724a28..1ddd4cd2 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -39,7 +39,7 @@ Skatities Torrentu Devēji Subtitri - Atkārtot connection + Atkārtot pieslēgumu… Iet atpakaļ Palaist epizodi Ieladēt @@ -56,7 +56,7 @@ Dzēst failu Palaist failu Atsākt ielādi - "Pauzēt ielādi" + Pauzēt ielādi Atslēgt automātisko kļūdu ziņošanu Vairāk informācijas Slēpt @@ -98,7 +98,7 @@ Iztīrīt Teksta krāsa Automātiski-iestādīt valodu - %dd %dh %dm + %dh %dm %dm %dd %dh %dm Ielādēt valodas @@ -130,7 +130,7 @@ Novelc lai paradītu Novelc no māla lidz malai lai pozicionētu video Novēlu lai mainītu iestādījums - Novēlu uz augšu vai apakšu pa labi un pa kreisi lai lai nomainītu gaišumu un skaņu + Novēlu uz augšu vai apakšu pa labi un pa kreisi lai nomainītu gaišumu un skaņu Automātiski nākamo epizodi Sākt nākamo epizodi kad šis bridzas Divreiz uzpied lai paslēptu @@ -155,7 +155,7 @@ Atjaunināt un saglabāt Informācija Advancēta meklēšana - Dod tev meklēšanas rezultātus citus no devēja. + Dod tev meklēšanas rezultātus citus no devēja Tikai sūtīt datus no kļudām Nesutīt datus Radīt fillera epizodi priekš animē @@ -164,9 +164,9 @@ Slēpt izvēlētos video kvalitāti meklēšanas rezultātus Automātiski papildinājumu atjauninājumi Automātiski ielādēt papildinājumus - Automātiski instalēt visus neinstalētos papildinājumus no glabātavas + Automātiski instalēt visus neinstalētos papildinājumus no glabātavas. Radīt aplikācijas atjauninājumus - Automātiski meklēt jaunus atjauninājumus kad palaiž aplikāciju + Automātiski meklēt jaunus atjauninājumus kad palaiž aplikāciju. Atsākt uzstādīšanas procesu Atjaunināt uz priekšizlaišanu Dažu telefoni nepieņem jauno aplikāciju instaletāju. Meiģiniet veco opciju ja nevar stjaunināt. @@ -274,7 +274,7 @@ Izraisa avārijas, ja ierīcēs ar mazu atmiņu ir iestatīta pārāk augsta vērtība, piemēram, Android TV. Izdevīgs lai izlaistu ISO aizturi raw.githubusercontent.com Proxy - Apiet GitHub bloķēšanu, izmantojot jsdelivr, tādēļ atjauninājumi var aizkavēties par dažām dienām. + Apiet GitHub bloķēšanu, izmantojot jsdelivr. Tādēļ atjauninājumi var aizkavēties par dažām dienām. Klonēt saiti Noņemt saiti Pievienojiet esošas vietnes klonu ar citu URL @@ -451,7 +451,7 @@ Notīrīt vēsturi Vēsture Rādīt izlaižamos uznirstošos logus atvēršanai/beigšanai - "Pārāk daudz teksta. Nevar saglabāt starpliktuvē." + Pārāk daudz teksta. Nevar saglabāt starpliktuvē. Notiek lietotnes atjauninājuma lejupielāde… @@ -470,7 +470,7 @@ Atlasiet Bibliotēka Atvērt ar Šķiet, ka jūsu bibliotēka ir tukša :( -\n Piesakieties bibliotēkas kontā vai pievienojiet pārraides savai vietējai bibliotēkai +\nPiesakieties bibliotēkas kontā vai pievienojiet pārraides savai vietējai bibliotēkai. Atgriest Anulēts %s abonements %d sērija izlaista! @@ -479,7 +479,7 @@ Github Nav subtitru Atskaņot epizodi - Piedodiet, bet aplikācijā bija kļūda, anonīms kļūdas ziņojums tika aizsūtīts izstrādātājiem. + Piedodiet, bet aplikācijā bija kļūda. Anonīms kļūdas ziņojums tika aizsūtīts izstrādātājiem Iet Bezmaksas Ieslēgt elementus uz plakātiem @@ -491,7 +491,7 @@ Meklēšanas summa, kas tiek izmantota, kad spēlētājs ir paslēpts DNS virs HTTPS Tuvināt - Neizdevās sasniegt GitHub, iespējot jsdelivr starpniekserveri. + Neizdevās sasniegt GitHub. Pieslēdzas starpniekserverim caur jsDelivr… Iztiept Galvenā krāsa %s %s @@ -504,7 +504,11 @@ Web SDR Rezulūcija - CloudStream pēc noklusējuma nav instalēta neviena vietne. Vietnes jāinstalē no krātuvēm. Sky UK Limited bezsmadzeņu DMCA noņemšanas dēļ mēs nevaram saistīt ar repozitorija vietni lietotnē. Pievienojieties mūsu Discord vai meklējiet tiešsaistē. + CloudStream pēc noklusējuma nav instalēta neviena vietne. Vietnes jāinstalē no krātuvēm. +\n +\nSky UK Limited bezsmadzeņu DMCA noņemšanas dēļ mēs nevaram saistīt ar repozitorija vietni lietotnē. +\n +\nPievienojieties mūsu Discord vai meklējiet tiešsaistē. Viss %s jau ir lejupielādēts Repozitorija nosaukums Plugin ielādēti @@ -518,11 +522,11 @@ Noņemt no skatītajiem Atzīmēt kā skatītu Vai tiešām vēlaties iziet\? - Šķiet, ka šis saraksts ir tukšs, mēģiniet pārslēgties uz citu + Šķiet, ka šis saraksts ir tukšs, mēģiniet pārslēgties uz citu. Atrasts drošā režīma fails! \n Paplašinājumi netiek ielādēti startēšanas laikā, kamēr fails nav noņemts. Vērtējums (no zema līdz augstam) Abonēto šovu atjaunināšana Abonēts Abonēts %s - + \ No newline at end of file diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index feff0673..a9d4b894 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -1,6 +1,5 @@ - - + വേഗം (%.2fx) റേറ്റിംഗ്: %.1f @@ -179,4 +178,26 @@ പുറത്ത്പോകുന്നതോടുകൂടി ആപ് അപ്ഡേറ്റ് ആവുന്നതാണ് ലൈബ്രറി തിരഞ്ഞെടുക്കുക ഇത് ഉപയോഗിച്ച് തുറക്കുക - + ട്രെയിലർ പ്ലേ ചെയ്യുക + ലൈവ് സ്ട്രീം പ്ലേ ചെയ്യുക + ഫില്ലർ + %d min + ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് കളിക്കുക + അടുത്ത ക്രമരഹിതമായ + എപ്പിസോഡ് പോസ്റ്റർ + അപ്ഡേറ്റ് ആരംഭിച്ചു + പ്രധാന പോസ്റ്റർ + പോസ്റ്റർ + ലോഡിംഗ് ഒഴിവാക്കുക + തിരയുക %s… + %dm + മടങ്ങിപ്പോവുക + പശ്ചാത്തല പ്രിവ്യൂ + പോസ്റ്റർ + ദാതാവിനെ മാറ്റുക + ലോഡിംഗ്… + ബ്രൗസർ + ഒന്നുമില്ല + വീണ്ടും കാണുക + സ്ട്രീം + \ No newline at end of file diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-or/strings.xml @@ -0,0 +1,2 @@ + + \ 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 index 12e580a2..1653e83f 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -104,4 +104,256 @@ 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 - + Pokračuje v prehrávaní v miniatúrnom prehrávači nad ostatnými aplikáciami + Zopakujte pripojenie… + Upustené + Obraz v obraze + Streamovať torrent + Náhľad pozadia + Tlačidlo na zmenu veľkosti prehrávača + Sledujem + Popis sa nenašiel + Ďalší náhodný + Stream + Podržané + Zobraziť Logcat 🐈 + Protokol + Prehliadač + Zhrnutie sa nenašlo + Dvojitým ťuknutím pozastaviť + Aktualizácie a zálohovanie + Informácie + Rozšírené vyhľadávanie + Zobraziť plagáty z Kitsu + Automatická aktualizácia doplnkov + Skryť vybranú kvalitu videa vo výsledkoch vyhľadávania + Zobraziť výplňovú epizódu pre anime + APK inštalátor + Niektoré telefóny nepodporujú nový inštalátor balíčkov. Ak sa aktualizácie nenainštalujú, skúste použiť staršiu možnosť. + Nenáročná aplikácia pre romány od rovnakých vývojárov + Jazyk aplikácie + Nenašli sa žiadne odkazy + Darujte benén vývojárom + Benén darovaný + Tento poskytovateľ nepodporuje Chromecast + Súbor zálohy načítaný + Automaticky synchronizovať priebeh sledovania súčasnej epizódy + Ťuknite dvakrát vpravo alebo vľavo pre pretočenie vpred alebo vzad + Potiahnutím zmeniť nastavenia + Posunutím nahor alebo nadol na ľavej alebo pravej strane zmeníte jas alebo hlasitosť + Dĺžka pretočenia (sekundy) + Zopakovať proces nastavenia + Titulky + Dáta uložené + Anime aplikácia od rovnakých vývojárov + Zálohovať dáta + Odkaz skopírovaný do schránky + Použiť systémový jas + Obnoviť dáta zo zálohy + Dvojitým ťuknutím pretočiť + Pridá možnosť rýchlosti do prehrávača + Automaticky sťahovať doplnky + Pripojte sa na Discord + Neodosiela žiadne dáta + Odstrániť čierne okraje + Automaticky vyhľadať nové aktualizácie po spustení aplikácie. + Prehrať epizódu + Chýbajú povolenia k úložisku. Skúste to prosím znova. + Nastavenia titulkov prehrávača + Spustiť ďalšiu epizódu po skončení aktuálnej + Chromecast titulky + Eigengravy režim + Potiahnutím pretočiť + Automaticky prehrať ďalšiu epizódu + Aktualizovať priebeh sledovania + Ťuknite dvakrát do stredu pre pozastavenie + V prehrávači použiť systémový jas namiesto tmavého prekrytia + Zobraziť upútavky + Automaticky nainštalovať všetky ešte nenainštalované doplnky z pridaných repozitárov. + Odosiela dáta len pri pádoch + Knižnica + GitHub + Hľadať + Účty + Nastavenia Chromecast titulkov + Potiahnutím zo strany na stranu môžete ovládať svoju pozíciu vo videu + Nepodarilo sa obnoviť dáta zo súboru %s + Chyba pri zálohovaní %s + Poskytne vám výsledky vyhľadávania rozdelené podľa poskytovateľa + Zobraziť aktualizácie aplikácie + Aktualizácia na predbežné vydania + Vyhľadať aktualizácie predbežných vydaní namiesto plných vydaní + Ospravedlňujeme sa, aplikácia spadla. Vývojárom bude odoslané anonymné hlásenie o páde + Obnoviť predvolenú hodnotu + Sezóna + Synopsa + vo fronte + Žiadne titulky + Anime + Kreslené + Skopírovať odkaz + Automaticky stiahnuť + Zrkadlo sťahovania + Zamknúť + Nenašla sa žiadna aktualizácia + Skontrolovať aktualizáciu + Zmeniť veľkosť + DNS cez HTTPS + Odstrániť stránku + Pridá klon existujúcej stránky s inou URL adresou + Cesta sťahovania + Android TV + Gestá + Funkcie prehrávača + Všeobecné + Náhodné tlačidlo + Prihlásiť sa + Prepnúť účet + Pridať účet + Rozloženie aplikácie + ahoj@svet.sk + Úspešné + MojeSuperMeno + Seriály + Seriál + E + Roztiahnuť + Záloha + Zdroj + Voľné + Veľkosť vyrovnávacej pamäte videa + Aktualizácie aplikácie + Umiestnenie názvu plagátu + Živý prenos + heslo123 + S + Pokračovať + NSFW + Akcie + Pozastaviť + Zobraziť anime dabované/s titulkami + Titulky + Synchronizovať + Prehrať v aplikácii + %d-%d + Spôsobuje zlyhania, ak je nastavená príliš vysoko na zariadeniach s nízkou pamäťou, ako je Android TV. + raw.githubusercontent.com Proxy + Trvanie + Aplikácia + /%d + Pridané %s + Dokument + Chromecast zrkadlo + Predvolené + Ostatné + Chyba pri sťahovaní, skontrolujte povolenia k úložisku + Primárna farba + Vytvoriť účet + %d / 10 + Priblížiť + Torrenty + Rozlíšenie prehrávača + Umiestniť názov pod plagát + Preferovaná kvalita sledovania (WiFi) + Rozšírenia + Hodnotené + -30 + Chyba vykresľovania + Neočakávaná chyba prehrávača + Téma aplikácie + Dokumenty + Preferované médiá + URL servera NGINX + %d %s + Predvolené + Pridať sledovanie + Žiadna sezóna + Epizóda + Znova načítať odkazy + Jazyky poskytovateľa + Spustiť + Živé prenosy + Stiahnuť titulky + Povoliť NSFW u podporovaných poskytovateľov + Obchádzanie ISP + Prepnúť UI prvky na plagáte + Rozloženie + Neúspešné + Stav + Preskočiť OP + Vyrovnávacia pamäť videa na disku + Hodnotenie + Torrent + OVA + Preskočiť túto aktualizáciu + /\?\? + Film + 127.0.0.1 + účet + Rok + Prispôsobiť obrazovke + Zmazať + Využité + Štítok kvality + Prehrávač skrytý - dĺžka pretočenia + Vzhľad + %s %d%s + Obchádza blokovanie GitHubu pomocou jsDelivr. Môže spôsobiť oneskorenie aktualizácií o niekoľko dní. + Zobraziť náhodné tlačidlo na domovskej stránke + Odhlásiť sa + Aktualizovať + Stránka + Dĺžka vyrovnávacej pamäte videa + Zmazať súbor + Prehrať v %s + Funkcie + Nezobrazovať znova + Vzdialená chyba + Užitočné na obchádzanie blokácií ISP + Odkazy + Klonovať stránku + OVA + Filmy + príklad.sk + Vyrovnávacia pamäť + Nepodarilo sa pripojiť na GitHub. Zapína sa proxy jsDelivr… + Nenašli sa žiadne epizódy + +30 + Ázijské drámy + Anime + Chyba zdroja + Prehrať v prehliadači + Štítok dabingu + Štítok titulkov + Názov + Vymazať vyrovnávaciu pamäť videí a obrázkov + Prehrávač zobrazený - dĺžka pretáčania + Dĺžka pretočenia, keď je prehrávač viditeľný + Dĺžka pretočenia, keď je prehrávač skrytý + Kódovanie titulkov + Test poskytovateľa + Rozloženie + Automaticky + Mobilné rozloženie + Poskytovatelia + TV rozloženie + Kód jazyka (sk) + MôjSuperWeb + %s %s + Vylúčenie zodpovednosti + NSFW + Týmto sa natrvalo vymaže %s +\nSte si istý\? + %dm +\nzostáva + Prebieha + Dokončené + Rozloženie emulátora + Epizódy + Video + Ázijská dráma + Chromecastovať epizódu + Preferovaná kvalita sledovania (mobilné dáta) + Maximálny počet znakov v názve prehrávača + Spôsobuje problémy, ak je nastavená príliš vysoko v zariadeniach s malým ukladacím priestorom, ako je napríklad Android TV. + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 8cad60ad..926460d3 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,6 +1,5 @@ - - + %s Tập %d @@ -129,16 +128,16 @@ Chỉnh tốc độ phim Có thể điều chỉnh tốc độ phát phim Vuốt để tua nhanh - Bạn có thể vuốt trái hoặc phải để tua nhanh khi xem phim + Vuốt sang trái hoặc phải để tua video Vuốt để chỉnh độ sáng và âm lượng - Vuốt từ dưới lên trên ở bên trái hoặc phải đều điều chỉnh độ sáng và âm lượng + Vuốt lên hoặc vuốt xuống ở hai bên để điều chỉnh độ sáng và âm lượng Tự động phát tập tiếp theo Phát tập tiếp theo sau khi hết tập hiện tại Nhấn 2 lần để tua Nhấn 2 lần để tạm dừng - Thời lượng tua + Thời lượng tua (Giây) Nhấn 2 lần vào bên trái hoặc bên phải màn hình để tua trước hoặc sau - Nhấn vào giữa để tạm dừng + Nhấn vào giữa hai lần để tạm dừng Sử dụng độ sáng hệ thống Sử dụng độ sáng hệ thống trong trình phát ứng dụng Cập nhật tiến trình xem @@ -164,7 +163,7 @@ Ẩ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 + Tự động tìm kiếm bản cập nhật mới sau khi khởi động app. Cập nhật phiên bản Beta Tìm kiếm các phiên bản Beta thay vì đợi bản cập nhật chính thức Github @@ -230,7 +229,7 @@ Phim Bộ Hoạt Hình Anime - @string/ova + OVA Torrent Phim Tài Liệu Truyền Hình Châu Á @@ -508,7 +507,7 @@ 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ộ +\nĐăng nhập vào tài khoản 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 @@ -517,7 +516,7 @@ 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 + 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 Nhật ký @@ -525,4 +524,24 @@ Thất bại Thành công Bắt đầu - + Kiểm tra nguồn phim + raw.githubusercontent.com Proxy + Không thể kết nối được tới GitHub. Đang bật jsDelivr proxy… + Android TV + Khởi động lại + Đã đăng kí %s + Tập %d đã ra mắt! + Đã đăng kí + Dừng + Bỏ qua ISP + Đã bỏ đăng ký %s + Tìm thấy tệp Safe mode! +\nKhông tải bất cứ tiện ích nào khi khởi dộng cho đến khi tệp bị xoá + Trở lại + Đang cập nhật các phim đã đăng kí + Bỏ qua chặn GitHub bằng cách dùng jsDelivr. Có thể gây ra việc cập nhật bị chậm vài ngày + Lượng tua thêm được sử dụng khi trình phát ẩn + Lượng tua thêm + Lượng tua thêm được sử dụng khi trình phát hiện lên + Lượng tua thêm + \ No newline at end of file From 3121b5b123894287de25bc45c0023ef072d1b41f Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Wed, 3 May 2023 20:17:55 +0000 Subject: [PATCH 073/570] chore(locales): fix locale issues --- .../lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 1 + app/src/main/res/values-cs/strings.xml | 5 +++-- app/src/main/res/values-hu/strings.xml | 4 ++-- app/src/main/res/values-lv/strings.xml | 2 +- app/src/main/res/values-ml/strings.xml | 5 +++-- app/src/main/res/values-or/strings.xml | 2 +- app/src/main/res/values-sk/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 5 +++-- 8 files changed, 15 insertions(+), 11 deletions(-) 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 5a000eb4..b733ae9f 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 @@ -85,6 +85,7 @@ val appLanguages = arrayListOf( Triple("", "Nederlands", "nl"), Triple("", "norsk nynorsk", "nn"), Triple("", "norsk bokmål", "no"), + Triple("", "ଓଡ଼ିଆ", "or"), Triple("", "polski", "pl"), Triple("\uD83C\uDDF5\uD83C\uDDF9", "português", "pt"), Triple("\uD83E\uDD8D", "mmmm... monke", "qt"), diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 36f5d3c7..7ad80259 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -1,5 +1,6 @@ - + + %s Ep %d @@ -553,4 +554,4 @@ Vrátit zpět Obchází blokování GitHubu pomocí jsDelivr. Může způsobit zpoždění aktualizací o několik dní. Obcházení ISP - \ No newline at end of file + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 8ed09726..e4be49e5 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -57,7 +57,7 @@ Megnyitás böngészőben Betöltés kihagyása Poster - \@string/result_poster_img_desPoszter + @string/result_poster_img_desPoszter Nézés Befejezve Később megnézés @@ -496,4 +496,4 @@ HQ %d letöltve Start - \ No newline at end of file + diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 1ddd4cd2..ddd39942 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -529,4 +529,4 @@ Abonēto šovu atjaunināšana Abonēts Abonēts %s - \ No newline at end of file + diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index a9d4b894..1f117af6 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -1,5 +1,6 @@ - + + വേഗം (%.2fx) റേറ്റിംഗ്: %.1f @@ -200,4 +201,4 @@ ഒന്നുമില്ല വീണ്ടും കാണുക സ്ട്രീം - \ No newline at end of file + diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index a6b3daec..42eba3cc 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -1,2 +1,2 @@ - \ 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 index 1653e83f..e0cc27d0 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -356,4 +356,4 @@ Preferovaná kvalita sledovania (mobilné dáta) Maximálny počet znakov v názve prehrávača Spôsobuje problémy, ak je nastavená príliš vysoko v zariadeniach s malým ukladacím priestorom, ako je napríklad Android TV. - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 926460d3..d7795713 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,5 +1,6 @@ - + + %s Tập %d @@ -544,4 +545,4 @@ Lượng tua thêm Lượng tua thêm được sử dụng khi trình phát hiện lên Lượng tua thêm - \ No newline at end of file + From 27155e0f7e8c6342a2f75da18ac9698965e2c4b8 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 3 May 2023 22:29:28 +0200 Subject: [PATCH 074/570] Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Update translation files Updated by "Remove blank strings" hook in Weblate. Translated using Weblate (Hungarian) Currently translated at 83.6% (510 of 610 strings) Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/ Translation: Cloudstream/App --- app/src/main/res/values-hu/strings.xml | 4 ++-- app/src/main/res/values-qt/strings.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index e4be49e5..8e9c3dfc 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -57,7 +57,7 @@ Megnyitás böngészőben Betöltés kihagyása Poster - @string/result_poster_img_desPoszter + Poszter Nézés Befejezve Később megnézés @@ -496,4 +496,4 @@ HQ %d letöltve Start - + \ No newline at end of file diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index f763d795..76852ca4 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -247,5 +247,5 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOOOGGAGHAGHAAA aoaaaaaoooghhh oooooh uuaagh - @string/home_play - + \@string/home_play + \ No newline at end of file From 386ce75df165a06c161f7058b03d052e2fca7db0 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Wed, 3 May 2023 20:30:03 +0000 Subject: [PATCH 075/570] chore(locales): fix locale issues --- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-qt/strings.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 8e9c3dfc..46407f76 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -496,4 +496,4 @@ HQ %d letöltve Start - \ No newline at end of file + diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index 76852ca4..f763d795 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -247,5 +247,5 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOOOGGAGHAGHAAA aoaaaaaoooghhh oooooh uuaagh - \@string/home_play - \ No newline at end of file + @string/home_play + From 3b21ec3794b2f37a297a5f04ff8194a8c7d74b2e Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Wed, 10 May 2023 09:13:48 +0200 Subject: [PATCH 076/570] NewPipeExtractor:v0.22.6 -> NewPipeExtractor:master-SNAPSHOT (#468) --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7110c43d..ebde6187 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -217,7 +217,7 @@ dependencies { //implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT") // newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204 - implementation("com.github.TeamNewPipe:NewPipeExtractor:v0.22.6") + implementation("com.github.TeamNewPipe:NewPipeExtractor:master-SNAPSHOT") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6") // Library/extensions searching with Levenshtein distance From 77d4ecd7c6d8f783eca4921505c9267cf3d02447 Mon Sep 17 00:00:00 2001 From: jhih_yu Date: Wed, 10 May 2023 15:14:01 +0800 Subject: [PATCH 077/570] Fix Traditional Chinese (zh-rTW) display name (#467) --- .../com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b733ae9f..ee262eec 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 @@ -101,7 +101,7 @@ val appLanguages = arrayListOf( Triple("", "اردو", "ur"), Triple("", "Tiếng Việt", "vi"), Triple("", "中文", "zh"), - Triple("\uD83C\uDDF9\uD83C\uDDFC", "文言", "zh-rTW"), + Triple("\uD83C\uDDF9\uD83C\uDDFC", "正體中文(臺灣)", "zh-rTW"), /* end language list */ ).sortedBy { it.second.lowercase() } //ye, we go alphabetical, so ppl don't put their lang on top From b37aa5534308e7c6b702c493823639fa2598648f Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Wed, 10 May 2023 09:16:24 +0200 Subject: [PATCH 078/570] remove strings.xml comment --- app/src/main/res/values-bg/strings.xml | 1 - app/src/main/res/values-bp/strings.xml | 1 - app/src/main/res/values-cs/strings.xml | 1 - app/src/main/res/values-el/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-hi/strings.xml | 1 - app/src/main/res/values-hr/strings.xml | 1 - app/src/main/res/values-in/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-mk/strings.xml | 1 - app/src/main/res/values-ml/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 1 - app/src/main/res/values-pl/strings.xml | 1 - app/src/main/res/values-ro/strings.xml | 1 - app/src/main/res/values-sv/strings.xml | 1 - app/src/main/res/values-tl/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-vi/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - app/src/main/res/values-zh/strings.xml | 1 - 20 files changed, 20 deletions(-) diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index d3bb648e..0543a94e 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -1,5 +1,4 @@ - %s еп. %d diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 16df53a6..38424e56 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -1,5 +1,4 @@ - diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 7ad80259..16ceff2d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -1,5 +1,4 @@ - diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 67e81957..5e02924f 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -1,5 +1,4 @@ - CloudStream Αρχική diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 62e41fdb..36c1cf1f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,5 +1,4 @@ - CloudStream Accueil diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index e4b9fe46..e0179646 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -1,5 +1,4 @@ - रफ्तार (%.2fx) diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 41b95aad..754b7a3a 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -1,5 +1,4 @@ - %d %s | %s diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 15c09228..a8c6a197 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -1,5 +1,4 @@ - %s Ep %d diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 25b8ca5a..6dca2e3a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,5 +1,4 @@ - %s Ep %d diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index d217f97f..66a6b9ba 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -1,5 +1,4 @@ - Брзина (%.2fx) diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 1f117af6..3d6240f9 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -1,5 +1,4 @@ - വേഗം (%.2fx) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index e640a28a..f56b0bfb 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1,5 +1,4 @@ - %s Ep %d diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 1071a9b3..2961cb47 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1,5 +1,4 @@ - Prędkość (%.2fx) Ocena: %.1f diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index bd22fb33..294abcfd 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1,5 +1,4 @@ - %s Ep %d diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 736f27ce..168e23fa 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -1,5 +1,4 @@ - Betygsatt: %.1f Hastighet (%.2fx) diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index a1faf3e1..95d38478 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -1,5 +1,4 @@ - diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 2ee7b65f..170c3679 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,5 +1,4 @@ - %d %s | %s diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index d7795713..f896e5c1 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,5 +1,4 @@ - diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 01b3b682..3488d8e0 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1,5 +1,4 @@ - %d %s | %s diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index f63e1d75..dbd96827 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1,5 +1,4 @@ - %d %s | %s From ae1aaa3d7dc8d8ddbf8510e6ab22ead6ecb4b1ea Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Wed, 10 May 2023 07:26:34 +0000 Subject: [PATCH 079/570] Translated using Weblate (Ukrainian) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Hindi) Currently translated at 38.3% (234 of 610 strings) Translated using Weblate (Japanese) Currently translated at 46.5% (284 of 610 strings) Translated using Weblate (Odia) Currently translated at 21.4% (131 of 610 strings) Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Persian) Currently translated at 22.1% (135 of 610 strings) Translated using Weblate (Odia) Currently translated at 21.3% (130 of 610 strings) Translated using Weblate (Odia) Currently translated at 18.1% (111 of 610 strings) Co-authored-by: 1 Co-authored-by: Adarsh0-s Co-authored-by: Hosted Weblate Co-authored-by: KING APPS Co-authored-by: Skrripy Co-authored-by: Subham Jena Co-authored-by: jhihyu lin Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fa/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hi/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/or/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hant/ Translation: Cloudstream/App --- app/src/main/res/values-fa/strings.xml | 12 +++++ app/src/main/res/values-hi/strings.xml | 9 ++++ app/src/main/res/values-ja/strings.xml | 3 ++ app/src/main/res/values-or/strings.xml | 46 +++++++++++++++++- app/src/main/res/values-uk/strings.xml | 12 ++--- app/src/main/res/values-zh-rTW/strings.xml | 55 +++++++++++++++++++--- 6 files changed, 124 insertions(+), 13 deletions(-) diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 6a6b5243..2e4b89b3 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -33,4 +33,16 @@ %dساعت %dدقیقه %dدقیقه پوستر اصلی + تورنت + آزاد + مستند ها + انیمیشن ویدیویی اصلی + حداکثر + فیلم‌ها + سریال های تلویزیونی + درام های آسیایی + انیمه + کارتونها + استفاده شده + برنامه diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index e0179646..1401b3d8 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -146,4 +146,13 @@ %dh %dm %dm विज्ञापन + अगला रैंडम + वापस जाओ + पोस्टर + पृष्ठभूमि का पूर्वावलोकन करें + प्रदाता बदलें + Cast: %s + मुख्य पोस्टर + एपिसोड का पोस्टर + %s Ep %d diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 347712d8..5fcc14da 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -198,4 +198,7 @@ \nSky UK Limitedによる無脳なDMCAテイクダウンのため🤮、アプリ内でリポジトリサイトをリンクすることができません。 \n \n私たちのDiscordに参加するか、オンラインで検索してください。 + バックグラウンドをプレビュー + ライブストリームの再生 + プロバイダーの変更 diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 42eba3cc..91481fd2 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -1,2 +1,46 @@ - + + ଅଧିକ ଵିକଳ୍ପ + ଦେଖୁଛନ୍ତି + %dଦି %dଘ %dମି + %dଘ %dମି + %dମି + ପୁନଃଦେଖୁଛନ୍ତି + ଲୁଚାଅ + ଚଲାଅ + ସୂଚନା + ଗୃହ + ସନ୍ଧାନ + ଧରଣ + ସ୍ଥଗିତ + ସାରିଛନ୍ତି + ସେଟିଂ + %d ମିନିଟ୍ + ଵେଗ (%.2fଗୁଣ) + ତ୍ୟାଗିଛନ୍ତି + ଦେଖିବା ପାଇଁ ଇଚ୍ଛୁକ + କିଛି ନାହିଁ + ଅଧିକ ସୂଚନା + ପାତ୍ର: %s + ପୋଷ୍ଟର୍ + ପୋଷ୍ଟର୍ + ଅଧ୍ୟାୟ ଚଲାଅ + କୌଣସି ଅଧ୍ୟାୟ ମିଳିଲା ନାହିଁ + ଅଧ୍ୟାୟ + ଅଧ୍ୟାୟ + %s‌ରେ ଚଲାଅ + ବ୍ରାଉଜର୍‌ରେ ଚଲାଅ + ଉପଶୀର୍ଷକ ଡାଉନଲୋଡ୍ କରିବା + /%d + /\?\? + ଅଧ୍ୟାୟ %d ମୁକ୍ତିଲାଭ କଲା! + ସ୍ୱତଃ ଡାଉନଲୋଡ୍ + ଲିଙ୍କ୍‌ଗୁଡ଼ିକୁ ପୁନଃଲୋଡ୍ କରିବା + ଲିଙ୍କ୍ କପି କରିନେବା + ଆପ୍‌ରେ ଚଲାଅ + Chromecast ଅଧ୍ୟାୟ + + ଅଧ୍ୟାୟର ପୋଷ୍ଟର୍ + ମୁଖ୍ୟ ପୋଷ୍ଟର୍ + ଡିଫଲ୍ଟ + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index c58dd334..82527c95 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -453,22 +453,22 @@ MPV Відтворення веб-відео Веб-браузер - Кінець + Ендінґ Коротке повторення Пропустити %s - Змішаний кінець + Змішаний ендінґ Подяки - Опенінг + Опенінґ Вступ Очистити історію Історія - Показувати спливаючі вікна для опенінгу/кінця + Показувати спливаючі вікна для опенінґу/кінця Забагато тексту. Не вдалося зберегти в буфер обміну. Позначити як переглянуте Ви впевнені що хочете вийти\? Так Ні - Установлення оновлення програми… + Встановлення оновлення програми… Не вдалося встановити нову версію програми Старий Інсталятор пакетів @@ -487,7 +487,7 @@ Завантаження оновлення програми… Усі розширення були вимкнені через збій, щоб допомогти вам знайти те, що стало причиною проблеми. Програму не знайдено - Змішаний опенінг + Змішаний опенінґ Видалити з переглянутого За оновленням (від старого до нового) За оновленням (від нового до старого) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 3488d8e0..1fd01d8a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -142,14 +142,14 @@ 播放速度 在播放器中添加播放速度選項 活動控制進度 - 左右滑動控制播放進度 + 從一側滑動到另一側以控制影片中的位置 滑動更改設定 上下滑動更改亮度或音量 自動播放下一集 播放完畢後播放下一集 輕按兩下以控制進度 輕按兩下以暫停 - 輕按兩下以控制進度時間 + 輕按兩下以控制進度時間(秒) 在右側或左側輕按兩次以向前或向後快轉 輕按兩下中間以暫停 使用系統亮度 @@ -177,7 +177,7 @@ 在搜尋結果中隱藏選中的影片畫質 自動更新外掛程式 顯示應用更新 - 啟動時自動搜尋更新 + 啟動應用程式後自動搜尋更新。 更新至預覽版 搜尋預覽版更新而不是僅搜尋正式版 Github @@ -244,8 +244,8 @@ 電影 電視劇 卡通 - @string/anime - @string/ova + 動畫 + OVA 種子 紀錄片 亞洲劇 @@ -285,7 +285,7 @@ 不再顯示 跳過此更新 更新 - 偏好播放畫質 + 偏好播放畫質 (WiFi) 影片播放器標題最大字數 影片播放器標題 影片緩衝大小 @@ -534,4 +534,47 @@ 外觀 功能 瀏覽器 + 第 %d 集已發行! + 媒體庫 + 開始 + 播放器顯示 - 快轉快退秒數 + 開啟方式 + 應用程式將在關閉時更新 + 評分(從低到高) + 更新開始 + 外掛程式已下載 + 從觀看中刪除 + 排序方式 + 排序 + 評分(從高到低) + 播放器可見時使用的快轉快退秒數 + 播放器隱藏 - 快轉快退秒數 + 更新(從新到舊) + 更新(從舊到新) + 按字母順序(A 到 Z) + 按字母順序(Z 到 A) + 選擇媒體庫 + 找到安全模式檔案! +\n在刪除檔案之前不在啟動時載入任何擴充功能。 + 日誌 + 失敗 + 通過 + 播放器隱藏時使用的快轉快退秒數 + Android TV + 片源測試 + 重新啟動 + 停止 + 訂閱 + 已訂閱 %s + 已取消訂閱 %s + 偏好播放畫質 (行動數據) + raw.githubusercontent.com Proxy + 繞過 ISP + 還原 + 無法訪問 GitHub。 正在開啟 jsDelivr proxy… + 使用 jsDelivr 繞過 GitHub 的阻擋。 可能導致更新延遲幾天。 + 您的媒體庫是空的:( +\n登入媒體庫帳戶或將節目添加到您本機的媒體庫。 + 此列表是空的。 嘗試切換到另一個。 + 正在更新訂閱節目 From 0f00b1baf05d94778a384f276c0b8906025a993b Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Wed, 10 May 2023 07:27:01 +0000 Subject: [PATCH 080/570] chore(locales): fix locale issues --- app/src/main/res/values-or/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 91481fd2..f500d5a6 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -43,4 +43,4 @@ ଅଧ୍ୟାୟର ପୋଷ୍ଟର୍ ମୁଖ୍ୟ ପୋଷ୍ଟର୍ ଡିଫଲ୍ଟ - \ No newline at end of file + From 8c9d52bc0e35b548339b7021a73f7f140cd5e0f2 Mon Sep 17 00:00:00 2001 From: Shif-Jess <117321707+Shif-Jess@users.noreply.github.com> Date: Sun, 14 May 2023 23:19:04 +0700 Subject: [PATCH 081/570] Added new Extractors (#461) --- .../cloudstream3/extractors/Chillx.kt | 135 +++++++++++++++ .../cloudstream3/extractors/Filesim.kt | 21 ++- .../cloudstream3/extractors/Gofile.kt | 59 +++++++ .../cloudstream3/extractors/Krakenfiles.kt | 37 ++++ .../cloudstream3/extractors/StreamSB.kt | 15 ++ .../cloudstream3/extractors/Uservideo.kt | 51 ++++++ .../cloudstream3/extractors/Vicloud.kt | 51 ++++++ .../cloudstream3/extractors/XStreamCdn.kt | 10 ++ .../extractors/helper/GogoHelper.kt | 158 ++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 18 ++ 10 files changed, 554 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt new file mode 100644 index 00000000..1c548e74 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -0,0 +1,135 @@ +package com.lagradost.cloudstream3.extractors + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import javax.crypto.Cipher +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.SecretKeySpec + +class Bestx : Chillx() { + override val name = "Bestx" + override val mainUrl = "https://bestx.stream" +} + +class Watchx : Chillx() { + override val name = "Watchx" + override val mainUrl = "https://watchx.top" +} +open class Chillx : ExtractorApi() { + override val name = "Chillx" + override val mainUrl = "https://chillx.top" + override val requiresReferer = true + + companion object { + private const val KEY = "4VqE3#N7zt&HEP^a" + } + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val master = Regex("MasterJS\\s*=\\s*'([^']+)").find( + app.get( + url, + referer = referer + ).text + )?.groupValues?.get(1) + val encData = AppUtils.tryParseJson(base64Decode(master ?: return)) + val decrypt = cryptoAESHandler(encData ?: return, KEY, false) + + val source = Regex("""sources:\s*\[\{"file":"([^"]+)""").find(decrypt)?.groupValues?.get(1) + val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1) + + // required + val headers = mapOf( + "Accept" to "*/*", + "Connection" to "keep-alive", + "Sec-Fetch-Dest" to "empty", + "Sec-Fetch-Mode" to "cors", + "Sec-Fetch-Site" to "cross-site", + "Origin" to mainUrl, + ) + + callback.invoke( + ExtractorLink( + name, + name, + source ?: return, + "$mainUrl/", + Qualities.P1080.value, + headers = headers, + isM3u8 = true + ) + ) + + AppUtils.tryParseJson>("[$tracks]") + ?.filter { it.kind == "captions" }?.map { track -> + subtitleCallback.invoke( + SubtitleFile( + track.label ?: "", + track.file ?: return@map null + ) + ) + } + } + + private fun cryptoAESHandler( + data: AESData, + pass: String, + encrypt: Boolean = true + ): String { + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512") + val spec = PBEKeySpec( + pass.toCharArray(), + data.salt?.hexToByteArray(), + data.iterations?.toIntOrNull() ?: 1, + 256 + ) + val key = factory.generateSecret(spec) + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + return if (!encrypt) { + cipher.init( + Cipher.DECRYPT_MODE, + SecretKeySpec(key.encoded, "AES"), + IvParameterSpec(data.iv?.hexToByteArray()) + ) + String(cipher.doFinal(base64DecodeArray(data.ciphertext.toString()))) + } else { + cipher.init( + Cipher.ENCRYPT_MODE, + SecretKeySpec(key.encoded, "AES"), + IvParameterSpec(data.iv?.hexToByteArray()) + ) + base64Encode(cipher.doFinal(data.ciphertext?.toByteArray())) + } + } + + private fun String.hexToByteArray(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + return chunked(2) + .map { it.toInt(16).toByte() } + + .toByteArray() + } + + data class AESData( + @JsonProperty("ciphertext") val ciphertext: String? = null, + @JsonProperty("iv") val iv: String? = null, + @JsonProperty("salt") val salt: String? = null, + @JsonProperty("iterations") val iterations: String? = null, + ) + + data class Tracks( + @JsonProperty("file") val file: String? = null, + @JsonProperty("label") val label: String? = null, + @JsonProperty("kind") val kind: String? = null, + ) +} 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 84fd0552..4c1791a8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt @@ -5,6 +5,25 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 +class Moviesm4u : Filesim() { + override val mainUrl = "https://moviesm4u.com" + override val name = "Moviesm4u" +} + +class FileMoonIn : Filesim() { + override val mainUrl = "https://filemoon.in" + override val name = "FileMoon" +} + +class StreamhideCom : Filesim() { + override var name: String = "Streamhide" + override var mainUrl: String = "https://streamhide.com" +} + +class Movhide : Filesim() { + override var name: String = "Movhide" + override var mainUrl: String = "https://movhide.pro" +} class Ztreamhub : Filesim() { override val mainUrl: String = "https://ztreamhub.com" //Here 'cause works @@ -35,7 +54,7 @@ open class Filesim : ExtractorApi() { response.select("script[type=text/javascript]").map { script -> if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) { val unpackedscript = getAndUnpack(script.data()) - val m3u8Regex = Regex("file.\\\"(.*?m3u8.*?)\\\"") + val m3u8Regex = Regex("file.\"(.*?m3u8.*?)\"") val m3u8 = m3u8Regex.find(unpackedscript)?.destructured?.component1() ?: "" if (m3u8.isNotEmpty()) { generateM3u8( diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt new file mode 100644 index 00000000..2ec185e0 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt @@ -0,0 +1,59 @@ +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.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities + +open class Gofile : ExtractorApi() { + override val name = "Gofile" + override val mainUrl = "https://gofile.io" + override val requiresReferer = false + private val mainApi = "https://api.gofile.io" + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z]+)").find(url)?.groupValues?.get(1) + val token = app.get("$mainApi/createAccount").parsedSafe()?.data?.get("token") + app.get("$mainApi/getContent?contentId=$id&token=$token&websiteToken=12345") + .parsedSafe()?.data?.contents?.forEach { + callback.invoke( + ExtractorLink( + this.name, + this.name, + it.value["link"] ?: return, + "", + getQuality(it.value["name"]), + headers = mapOf( + "Cookie" to "accountToken=$token" + ) + ) + ) + } + + } + + private fun getQuality(str: String?): Int { + return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull() + ?: Qualities.Unknown.value + } + + data class Account( + @JsonProperty("data") val data: HashMap? = null, + ) + + data class Data( + @JsonProperty("contents") val contents: HashMap>? = null, + ) + + data class Source( + @JsonProperty("data") val data: Data? = null, + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt new file mode 100644 index 00000000..b6887259 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt @@ -0,0 +1,37 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.httpsify + +open class Krakenfiles : ExtractorApi() { + override val name = "Krakenfiles" + override val mainUrl = "https://krakenfiles.com" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val id = Regex("/(?:view|embed-video)/([\\da-zA-Z]+)").find(url)?.groupValues?.get(1) + val doc = app.get("$mainUrl/embed-video/$id").document + val link = doc.selectFirst("source")?.attr("src") + + callback.invoke( + ExtractorLink( + this.name, + this.name, + httpsify(link ?: return), + "", + Qualities.Unknown.value + ) + ) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt index 1c6c7b94..a9fa20ba 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt @@ -7,6 +7,21 @@ import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +class Sbasian : StreamSB() { + override var mainUrl = "https://sbasian.pro" + override var name = "Sbasian" +} + +class Sbnet : StreamSB() { + override var name = "Sbnet" + override var mainUrl = "https://sbnet.one" +} + +class Keephealth : StreamSB() { + override var name = "Keephealth" + override var mainUrl = "https://keephealth.info" +} + class Sbspeed : StreamSB() { override var name = "Sbspeed" override var mainUrl = "https://sbspeed.com" diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt new file mode 100644 index 00000000..37a7edb5 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt @@ -0,0 +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.AppUtils +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities + +open class Uservideo : ExtractorApi() { + override val name: String = "Uservideo" + override val mainUrl: String = "https://uservideo.xyz" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val script = app.get(url).document.selectFirst("script:containsData(hosts =)")?.data() + val host = script?.substringAfter("hosts = [\"")?.substringBefore("\"];") + val servers = script?.substringAfter("servers = \"")?.substringBefore("\";") + + val sources = app.get("$host/s/$servers").text.substringAfter("\"sources\":[").substringBefore("],").let { + AppUtils.tryParseJson>("[$it]") + } + val quality = Regex("(\\d{3,4})[Pp]").find(url)?.groupValues?.getOrNull(1)?.toIntOrNull() + + sources?.map { source -> + callback.invoke( + ExtractorLink( + name, + name, + source.src ?: return@map null, + url, + quality ?: Qualities.Unknown.value, + ) + ) + } + + } + + data class Sources( + @JsonProperty("src") val src: String? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("label") val label: String? = null, + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt new file mode 100644 index 00000000..c8b2ae07 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt @@ -0,0 +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.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName + +open class Vicloud : ExtractorApi() { + override val name: String = "Vicloud" + override val mainUrl: String = "https://vicloud.sbs" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val id = Regex("\"apiQuery\":\"(.*?)\"").find(app.get(url).text)?.groupValues?.getOrNull(1) + app.get( + "$mainUrl/api/?$id=&_=${System.currentTimeMillis()}", + headers = mapOf( + "X-Requested-With" to "XMLHttpRequest" + ), + referer = url + ).parsedSafe()?.sources?.map { source -> + callback.invoke( + ExtractorLink( + name, + name, + source.file ?: return@map null, + url, + getQualityFromName(source.label), + ) + ) + } + + } + + private data class Sources( + @JsonProperty("file") val file: String? = null, + @JsonProperty("label") val label: String? = null, + ) + + private data class Responses( + @JsonProperty("sources") val sources: List? = arrayListOf(), + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt index 15ff0436..ccb2fde7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt @@ -8,6 +8,16 @@ import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.getQualityFromName +class StreamM4u : XStreamCdn() { + override val name: String = "StreamM4u" + override val mainUrl: String = "https://streamm4u.club" +} + +class Fembed9hd : XStreamCdn() { + override var mainUrl = "https://fembed9hd.com" + override var name = "Fembed9hd" +} + class Cdnplayer: XStreamCdn() { override val name: String = "Cdnplayer" override val mainUrl: String = "https://cdnplayer.online" diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt new file mode 100644 index 00000000..1766af6c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt @@ -0,0 +1,158 @@ +package com.lagradost.cloudstream3.extractors.helper + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64Decode +import com.lagradost.cloudstream3.base64DecodeArray +import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.getQualityFromName +import org.jsoup.nodes.Document +import java.net.URI +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +object GogoHelper { + + /** + * @param id base64Decode(show_id) + IV + * @return the encryption key + * */ + private fun getKey(id: String): String? { + return normalSafeApiCall { + id.map { + it.code.toString(16) + }.joinToString("").substring(0, 32) + } + } + + // https://github.com/saikou-app/saikou/blob/45d0a99b8a72665a29a1eadfb38c506b842a29d7/app/src/main/java/ani/saikou/parsers/anime/extractors/GogoCDN.kt#L97 + // No Licence on the function + private fun cryptoHandler( + string: String, + iv: String, + secretKeyString: String, + encrypt: Boolean = true + ): String { + //println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string") + val ivParameterSpec = IvParameterSpec(iv.toByteArray()) + val secretKey = SecretKeySpec(secretKeyString.toByteArray(), "AES") + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + return if (!encrypt) { + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec) + String(cipher.doFinal(base64DecodeArray(string))) + } else { + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec) + base64Encode(cipher.doFinal(string.toByteArray())) + } + } + + /** + * @param iframeUrl something like https://gogoplay4.com/streaming.php?id=XXXXXX + * @param mainApiName used for ExtractorLink names and source + * @param iv secret iv from site, required non-null if isUsingAdaptiveKeys is off + * @param secretKey secret key for decryption from site, required non-null if isUsingAdaptiveKeys is off + * @param secretDecryptKey secret key to decrypt the response json, required non-null if isUsingAdaptiveKeys is off + * @param isUsingAdaptiveKeys generates keys from IV and ID, see getKey() + * @param isUsingAdaptiveData generate encrypt-ajax data based on $("script[data-name='episode']")[0].dataset.value + * */ + suspend fun extractVidstream( + iframeUrl: String, + mainApiName: String, + callback: (ExtractorLink) -> Unit, + iv: String?, + secretKey: String?, + secretDecryptKey: String?, + // This could be removed, but i prefer it verbose + isUsingAdaptiveKeys: Boolean, + isUsingAdaptiveData: Boolean, + // If you don't want to re-fetch the document + iframeDocument: Document? = null + ) = safeApiCall { + if ((iv == null || secretKey == null || secretDecryptKey == null) && !isUsingAdaptiveKeys) + return@safeApiCall + + val id = Regex("id=([^&]+)").find(iframeUrl)!!.value.removePrefix("id=") + + var document: Document? = iframeDocument + val foundIv = + iv ?: (document ?: app.get(iframeUrl).document.also { document = it }) + .select("""div.wrapper[class*=container]""") + .attr("class").split("-").lastOrNull() ?: return@safeApiCall + val foundKey = secretKey ?: getKey(base64Decode(id) + foundIv) ?: return@safeApiCall + val foundDecryptKey = secretDecryptKey ?: foundKey + + val uri = URI(iframeUrl) + val mainUrl = "https://" + uri.host + + val encryptedId = cryptoHandler(id, foundIv, foundKey) + val encryptRequestData = if (isUsingAdaptiveData) { + // Only fetch the document if necessary + val realDocument = document ?: app.get(iframeUrl).document + val dataEncrypted = + realDocument.select("script[data-name='episode']").attr("data-value") + val headers = cryptoHandler(dataEncrypted, foundIv, foundKey, false) + "id=$encryptedId&alias=$id&" + headers.substringAfter("&") + } else { + "id=$encryptedId&alias=$id" + } + + val jsonResponse = + app.get( + "$mainUrl/encrypt-ajax.php?$encryptRequestData", + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ) + val dataencrypted = + jsonResponse.text.substringAfter("{\"data\":\"").substringBefore("\"}") + val datadecrypted = cryptoHandler(dataencrypted, foundIv, foundDecryptKey, false) + val sources = AppUtils.parseJson(datadecrypted) + + suspend fun invokeGogoSource( + source: GogoSource, + sourceCallback: (ExtractorLink) -> Unit + ) { + if (source.file.contains(".m3u8")) { + M3u8Helper.generateM3u8( + mainApiName, + source.file, + mainUrl, + headers = mapOf("Origin" to "https://plyr.link") + ).forEach(sourceCallback) + } else { + sourceCallback.invoke( + ExtractorLink( + mainApiName, + mainApiName, + source.file, + mainUrl, + getQualityFromName(source.label), + ) + ) + } + } + + sources.source?.forEach { + invokeGogoSource(it, callback) + } + sources.sourceBk?.forEach { + invokeGogoSource(it, callback) + } + } + + data class GogoSources( + @JsonProperty("source") val source: List?, + @JsonProperty("sourceBk") val sourceBk: List?, + ) + + data class GogoSource( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String?, + @JsonProperty("type") val type: String?, + @JsonProperty("default") val default: String? = null + ) +} \ No newline at end of file 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 b03c9fb7..4fde7181 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -342,6 +342,24 @@ val extractorApis: MutableList = arrayListOf( DesuOdvip(), DesuDrive(), + Chillx(), + Watchx(), + Bestx(), + Keephealth(), + Sbnet(), + Sbasian(), + Sblongvu(), + Fembed9hd(), + StreamM4u(), + Krakenfiles(), + Gofile(), + Vicloud(), + Uservideo(), + + Movhide(), + StreamhideCom(), + FileMoonIn(), + Moviesm4u(), Filesim(), FileMoon(), FileMoonSx(), From c0a8461b87e866e8709e26262704c0f609d5f205 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sat, 13 May 2023 09:41:46 +0200 Subject: [PATCH 082/570] Translated using Weblate (Kannada) Currently translated at 36.2% (221 of 610 strings) Translated using Weblate (Odia) Currently translated at 38.5% (235 of 610 strings) Translated using Weblate (Odia) Currently translated at 26.3% (161 of 610 strings) Translated using Weblate (Odia) Currently translated at 22.6% (138 of 610 strings) Co-authored-by: Hosted Weblate Co-authored-by: Subham Jena Co-authored-by: deepu2 Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/kn/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/or/ Translation: Cloudstream/App --- app/src/main/res/values-kn/strings.xml | 9 +- app/src/main/res/values-or/strings.xml | 109 ++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 1236dbba..9716a8a6 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -125,4 +125,11 @@ ಡೌನ್‌ಲೋಡ್ ಪ್ರಾರಂಭವಾಗಿದೆ ಡೌನ್‌ಲೋಡ್ ರದ್ದುಗೊಳಿಸಲಾಗಿದೆ ಮುಂದಿನ ರಾಂಡಮ್ - + ಮುಂದಕ್ಕೆ ಹೋಗಲು ಸ್ವೈಪ್ ಮಾಡಿ + ವೀಡಿಯೊದಲ್ಲಿ ನಿಮ್ಮ ಸ್ಥಾನವನ್ನು ನಿಯಂತ್ರಿಸಲು ಅಕ್ಕಪಕ್ಕಕ್ಕೆ ಸ್ವೈಪ್ ಮಾಡಿ + ಮುಂದಿನ ಸಂಚಿಕೆಯನ್ನು ಆಟೋ ಪ್ಲೇ ಮಾಡಿ + ಮುಂದೂಡಲು ಅಥವಾ ಇಂದೂಡಲು ಎರಡು ಬಾರಿ ಟ್ಯಾಪ್ ಮಾಡಿ + Brightness ಅಥವಾ volume ಬದಲಾಯಿಸಲು ಎಡ ಅಥವಾ ಬಲಭಾಗದಲ್ಲಿ ಮೇಲಕ್ಕೆ ಅಥವಾ ಕೆಳಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ + ಈಗಿನ ಎಪಿಸೋಡ್ ಮುಗಿದಾಗ ಮುಂದಿನ ಎಪಿಸೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ + ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸ್ವೈಪ್ ಮಾಡಿ + \ No newline at end of file diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index f500d5a6..e7b897c8 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -26,8 +26,8 @@ ପୋଷ୍ଟର୍ ଅଧ୍ୟାୟ ଚଲାଅ କୌଣସି ଅଧ୍ୟାୟ ମିଳିଲା ନାହିଁ - ଅଧ୍ୟାୟ - ଅଧ୍ୟାୟ + ଟି ଅଧ୍ୟାୟ + ଟିଏ ଅଧ୍ୟାୟ %s‌ରେ ଚଲାଅ ବ୍ରାଉଜର୍‌ରେ ଚଲାଅ ଉପଶୀର୍ଷକ ଡାଉନଲୋଡ୍ କରିବା @@ -43,4 +43,107 @@ ଅଧ୍ୟାୟର ପୋଷ୍ଟର୍ ମୁଖ୍ୟ ପୋଷ୍ଟର୍ ଡିଫଲ୍ଟ - + ଭାଷା + ନାହିଁ + ଵର୍ଣ୍ଣନା + ହଁ + ଲାଇବ୍ରେରୀ + ଇତିଵୃତ୍ତି + ଲେଖକ + %s ବାଦ୍ ଦିଅ + ଉପଶୀର୍ଷକ ଭାଷା + %s (ଅକ୍ଷମ) + ସ୍ଥିତି + ଆକାର + ସମର୍ଥିତ + HLS ଚାଳନାତାଲିକା + ଅନ୍ତଃ-ଚାଳକ + ଆଦ୍ୟ + ପ୍ରାନ୍ତ + ଆପ୍ ମିଳିଲା ନାହିଁ + ସବୁ ଭାଷା + VLC + MPV + ମିଶ୍ରିତ ପ୍ରାନ୍ତ + ମିଶ୍ରିତ ଆଦ୍ୟ + ଶ୍ରେୟ + ଉପକ୍ରମ + ଏହି ଭାଷାଗୁଡ଼ିକରେ ଵିଡ଼ିଓ ଦେଖନ୍ତୁ + ସଂସ୍କରଣ + ଆପ୍ ଭାଷା + ଅଧ୍ୟାୟ ଚଲାଅ + + ଚଳିତ + ଲିଙ୍କ୍ କ୍ଲିପ୍‌ବୋର୍ଡରେ କପି କରିନିଆଗଲା + ଚଳଚ୍ଚିତ୍ର + ସିଧାପ୍ରସାରଣ + ଉତ୍ସ + କୌଣସି ଅଦ୍ୟତନ ମିଳିଲା ନାହିଁ + ସାଧାରଣ + ପୁନଃ ଦେଖାଅନି + ସ୍ୱତଃ + ତ୍ରୁଟି + ବ୍ୟାକଅପ୍‌ରୁ ତଥ୍ୟ ପୁନରୁଦ୍ଧାର କରିବା + ଷ୍ଟୋରେଜ୍ ଅନୁମତି ଦିଆଯାଇ ନାହିଁ। ଦୟାକରି ପୁଣିଥରେ ଚେଷ୍ଟା କରନ୍ତୁ। + ଅଦ୍ୟତନ ଏଵଂ ବ୍ୟାକଅପ୍ + ବ୍ୟାକଅପ୍ + ଆଣ୍ଡ୍ରଏଡ୍ ଟିଵି + ଅଙ୍ଗଭଙ୍ଗୀ + ନୂଆ ଅଦ୍ୟତନ ମିଳିଲା! +\n%s -> %s + ଅଵଧି + ଆପ୍ + ବ୍ୟାକଅପ୍ ଫାଇଲ୍ ଧାରଣ ହେଲା + ତଥ୍ୟ ଗଚ୍ଛିତ ହୋଇଛି + %s ବ୍ୟାକଅପ୍ ନେବାରେ ତ୍ରୁଟି ଘଟିଲା + ଋତୁ + କୌଣସି ଋତୁ ନାହିଁ + ଫାଇଲ୍ ଵିଲୋପ କରିବେ + ପାରିତ ହେଲା + -୩୦ + ସ୍ଥିତି + ଵ୍ୟଵହୃତ + ଟିଵି ଧାରାଵାହିକ + ଏସୀୟ ନାଟକ + ଅନ୍ୟାନ୍ୟ + ଵିଡ଼ିଓ + ଉତ୍ସ ତ୍ରୁଟି + ଅପ୍ରତ୍ୟାଶିତ ଚାଳକ ତ୍ରୁଟି + ଆଖ୍ୟା + ଅଦ୍ୟତନ ପାଇଁ ଯାଞ୍ଚ କରିବା + ତାଲା + ଆକାର ଠିକ୍ କରିବା + ଏହି ଅଦ୍ୟତନଟିକୁ ବାଦ୍ ଦିଅ + କୃତ୍ୟ + ଉପଶୀର୍ଷକ + ଵୈଶିଷ୍ଟ୍ୟସବୁ + ଵେଶ + ଡିଫଲ୍ଟଗୁଡ଼ା + ପ୍ରାଥମିକ ରଙ୍ଗ + %s ଯୋଡ଼ାଗଲା + ଆଖ୍ୟା + ହେଲା + ଆପ୍ ଅଦ୍ୟତନ ଡାଉନଲୋଡ୍ ଚାଲିଛି… + ଆପ୍ ଅଦ୍ୟତନ ଅଧିସ୍ଥାପନ ଚାଲିଛି… + ଆପ୍‌ର ନୂଆ ସଂସ୍କରଣ ଅଧିସ୍ଥାପନ କରିହେଲା ନାହିଁ + ଵିଫଳ ହେଲା + ଚାଳକ + ତଥ୍ୟର ବ୍ୟାକଅପ୍ ନେବା + ଵିଲୋପ କର + ଵୃତ୍ତଚିତ୍ର + ଅନିମେ + ଧାରାଵାହିକ + ଚଳଚ୍ଚିତ୍ର + ଵୃତ୍ତଚିତ୍ର + ଏସୀୟ ନାଟକ + ସିଧାପ୍ରସାରଣ + ଗୁଣଵତ୍ତା ଲେବଲ୍ + ଅଦ୍ୟତନ କରିବା + ଚାଳକ ଵୈଶିଷ୍ଟ୍ୟସବୁ + ଆପ୍ ଥିମ୍ + ଭାଷା ସ୍ୱତଃ-ଚୟନ + ଅନିମେ + ଉପଶୀର୍ଷକ + +୩୦ + ଵର୍ଷ + \ No newline at end of file From b115817357e8e6cdc229e390256a1c4889c7e1da Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Sun, 14 May 2023 16:20:11 +0000 Subject: [PATCH 083/570] chore(locales): fix locale issues --- app/src/main/res/values-kn/strings.xml | 2 +- app/src/main/res/values-or/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 9716a8a6..399aafb1 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -132,4 +132,4 @@ Brightness ಅಥವಾ volume ಬದಲಾಯಿಸಲು ಎಡ ಅಥವಾ ಬಲಭಾಗದಲ್ಲಿ ಮೇಲಕ್ಕೆ ಅಥವಾ ಕೆಳಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ ಈಗಿನ ಎಪಿಸೋಡ್ ಮುಗಿದಾಗ ಮುಂದಿನ ಎಪಿಸೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸ್ವೈಪ್ ಮಾಡಿ - \ No newline at end of file + diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index e7b897c8..eaa76652 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -146,4 +146,4 @@ ଉପଶୀର୍ଷକ +୩୦ ଵର୍ଷ - \ No newline at end of file + From 0d431fd508cb893add78d95e8ebbbddc122f05db Mon Sep 17 00:00:00 2001 From: Hexated <37908684+hexated@users.noreply.github.com> Date: Sun, 21 May 2023 06:37:17 +0700 Subject: [PATCH 084/570] fixed Stramsb & Voe (#470) --- .../cloudstream3/extractors/StreamSB.kt | 103 +++++++++--------- .../lagradost/cloudstream3/extractors/Voe.kt | 8 +- .../cloudstream3/extractors/VoeExtractor.kt | 54 --------- .../cloudstream3/utils/ExtractorApi.kt | 3 +- 4 files changed, 62 insertions(+), 106 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/VoeExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt index a9fa20ba..3d2a81b7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt @@ -6,6 +6,11 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +import kotlin.random.Random + +class Vidgomunimesb : StreamSB() { + override var mainUrl = "https://vidgomunimesb.xyz" +} class Sbasian : StreamSB() { override var mainUrl = "https://sbasian.pro" @@ -100,24 +105,62 @@ class Sblongvu : StreamSB() { override var mainUrl = "https://sblongvu.com" } -// This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt -// The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE open class StreamSB : ExtractorApi() { override var name = "StreamSB" override var mainUrl = "https://watchsb.com" override val requiresReferer = false + private val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - private val hexArray = "0123456789ABCDEF".toCharArray() + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val regexID = + Regex("(embed-[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+|/e/[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+)") + val id = regexID.findAll(url).map { + it.value.replace(Regex("(embed-|/e/)"), "") + }.first() + val master = "$mainUrl/375664356a494546326c4b797c7c6e756577776778623171737/${encodeId(id)}" + val headers = mapOf( + "watchsb" to "sbstream", + ) + val mapped = app.get( + master.lowercase(), + headers = headers, + referer = url, + ).parsedSafe

() + M3u8Helper.generateM3u8( + name, + mapped?.streamData?.file ?: return, + url, + headers = headers + ).forEach(callback) - private fun bytesToHex(bytes: ByteArray): String { - val hexChars = CharArray(bytes.size * 2) - for (j in bytes.indices) { - val v = bytes[j].toInt() and 0xFF - - hexChars[j * 2] = hexArray[v ushr 4] - hexChars[j * 2 + 1] = hexArray[v and 0x0F] + mapped.streamData.subs?.map {sub -> + subtitleCallback.invoke( + SubtitleFile( + sub.label.toString(), + sub.file ?: return@map null, + ) + ) + } + } + + private fun encodeId(id: String): String { + val code = "${createHashTable()}||$id||${createHashTable()}||streamsb" + return code.toCharArray().joinToString("") { char -> + char.code.toString(16) + } + } + + private fun createHashTable(): String { + return buildString { + repeat(12) { + append(alphabet[Random.nextInt(alphabet.length)]) + } } - return String(hexChars) } data class Subs ( @@ -141,42 +184,4 @@ open class StreamSB : ExtractorApi() { @JsonProperty("status_code") val statusCode: Int, ) - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val regexID = - Regex("(embed-[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+|/e/[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)") - val id = regexID.findAll(url).map { - it.value.replace(Regex("(embed-|/e/)"), "") - }.first() -// val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362" - val master = "$mainUrl/sources16/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/" - val headers = mapOf( - "watchsb" to "sbstream", - ) - val mapped = app.get( - master.lowercase(), - headers = headers, - referer = url, - ).parsedSafe
() - // val urlmain = mapped.streamData.file.substringBefore("/hls/") - M3u8Helper.generateM3u8( - name, - mapped?.streamData?.file ?: return, - url, - headers = headers - ).forEach(callback) - - mapped.streamData.subs?.map {sub -> - subtitleCallback.invoke( - SubtitleFile( - sub.label.toString(), - sub.file ?: return@map null, - ) - ) - } - } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt index 12a76a9b..2c6998de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt @@ -6,6 +6,10 @@ import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +class Tubeless : Voe() { + override var mainUrl = "https://tubelessceliolymph.com" +} + open class Voe : ExtractorApi() { override val name = "Voe" override val mainUrl = "https://voe.sx" @@ -18,8 +22,8 @@ open class Voe : ExtractorApi() { callback: (ExtractorLink) -> Unit ) { val res = app.get(url, referer = referer).document - val link = res.select("script").find { it.data().contains("const sources") }?.data() - ?.substringAfter("\"hls\": \"")?.substringBefore("\",") + val script = res.select("script").find { it.data().contains("sources =") }?.data() + val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1) M3u8Helper.generateM3u8( name, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VoeExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VoeExtractor.kt deleted file mode 100644 index ad3f0150..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VoeExtractor.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.lagradost.cloudstream3.extractors - -import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.ExtractorApi -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.getQualityFromName - -open class VoeExtractor : ExtractorApi() { - override val name: String = "Voe" - override val mainUrl: String = "https://voe.sx" - override val requiresReferer = false - - private data class ResponseLinks( - @JsonProperty("hls") val hls: String?, - @JsonProperty("mp4") val mp4: String?, - @JsonProperty("video_height") val label: Int? - //val type: String // Mp4 - ) - - override suspend fun getUrl(url: String, referer: String?): List { - val html = app.get(url).text - if (html.isNotBlank()) { - val src = html.substringAfter("const sources =").substringBefore(";") - // Remove last comma, it is not proper json otherwise - .replace("0,", "0") - // Make json use the proper quotes - .replace("'", "\"") - - //Log.i(this.name, "Result => (src) ${src}") - parseJson(src)?.let { voeLink -> - //Log.i(this.name, "Result => (voeLink) ${voeLink}") - - // Always defaults to the hls link, but returns the mp4 if null - val linkUrl = voeLink.hls ?: voeLink.mp4 - val linkLabel = voeLink.label?.toString() ?: "" - if (!linkUrl.isNullOrEmpty()) { - return listOf( - ExtractorLink( - name = this.name, - source = this.name, - url = linkUrl, - quality = getQualityFromName(linkLabel), - referer = url, - isM3u8 = voeLink.hls != null - ) - ) - } - } - } - return emptyList() - } -} \ No newline at end of file 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 4fde7181..5062ebd9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -236,6 +236,7 @@ val extractorApis: MutableList = arrayListOf( XStreamCdn(), StreamSB(), + Vidgomunimesb(), StreamSB1(), StreamSB2(), StreamSB3(), @@ -275,7 +276,6 @@ val extractorApis: MutableList = arrayListOf( Uqload2(), Evoload(), Evoload1(), - VoeExtractor(), UpstreamExtractor(), Tomatomatela(), @@ -375,6 +375,7 @@ val extractorApis: MutableList = arrayListOf( Vidmoly(), Vidmolyme(), Voe(), + Tubeless(), Moviehab(), MoviehabNet(), Jeniusplay(), From b5566af401113d3882b38d2a7e041e4264e32eba Mon Sep 17 00:00:00 2001 From: LagradOst <46196380+Blatzar@users.noreply.github.com> Date: Wed, 14 Jun 2023 17:30:39 +0000 Subject: [PATCH 085/570] Added quality profiles (#414) * Added quality profiles * Better quality selection * Added profile bg and fixed some sources (#483) Co-authored-by: Blatzar <> --------- Co-authored-by: Lag <> Co-authored-by: Osten <11805592+LagradOst@users.noreply.github.com> --- .../cloudstream3/extractors/DoodExtractor.kt | 2 +- .../cloudstream3/extractors/GuardareStream.kt | 2 +- .../cloudstream3/extractors/Tantifilm.kt | 2 +- .../ui/player/FullScreenPlayer.kt | 40 +++- .../cloudstream3/ui/player/GeneratorPlayer.kt | 121 ++++++++---- .../player/source_priority/PriorityAdapter.kt | 60 ++++++ .../player/source_priority/ProfilesAdapter.kt | 116 ++++++++++++ .../source_priority/QualityDataHelper.kt | 159 ++++++++++++++++ .../source_priority/QualityProfileDialog.kt | 106 +++++++++++ .../source_priority/SourcePriorityDialog.kt | 105 ++++++++++ .../cloudstream3/ui/result/UiText.kt | 4 +- .../cloudstream3/utils/DataStoreHelper.kt | 2 +- .../cloudstream3/utils/ExtractorApi.kt | 28 ++- .../utils/SingleSelectionHelper.kt | 11 -- .../lagradost/cloudstream3/utils/UIHelper.kt | 34 +++- .../res/drawable/baseline_help_outline_24.xml | 5 + .../main/res/drawable/baseline_remove_24.xml | 2 +- app/src/main/res/drawable/profile_bg_blue.jpg | Bin 0 -> 21107 bytes .../res/drawable/profile_bg_dark_blue.jpg | Bin 0 -> 42704 bytes .../main/res/drawable/profile_bg_orange.jpg | Bin 0 -> 70427 bytes app/src/main/res/drawable/profile_bg_pink.jpg | Bin 0 -> 117989 bytes .../main/res/drawable/profile_bg_purple.jpg | Bin 0 -> 8564 bytes app/src/main/res/drawable/profile_bg_red.jpg | Bin 0 -> 47464 bytes app/src/main/res/drawable/profile_bg_teal.jpg | Bin 0 -> 128982 bytes .../res/layout/player_prioritize_item.xml | 48 +++++ .../layout/player_quality_profile_dialog.xml | 105 ++++++++++ .../layout/player_quality_profile_item.xml | 66 +++++++ .../layout/player_select_source_and_subs.xml | 44 ++++- .../layout/player_select_source_priority.xml | 179 ++++++++++++++++++ app/src/main/res/values/strings.xml | 21 +- app/src/main/res/xml/settings_player.xml | 16 +- 31 files changed, 1188 insertions(+), 90 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt create mode 100644 app/src/main/res/drawable/baseline_help_outline_24.xml create mode 100644 app/src/main/res/drawable/profile_bg_blue.jpg create mode 100644 app/src/main/res/drawable/profile_bg_dark_blue.jpg create mode 100644 app/src/main/res/drawable/profile_bg_orange.jpg create mode 100644 app/src/main/res/drawable/profile_bg_pink.jpg create mode 100644 app/src/main/res/drawable/profile_bg_purple.jpg create mode 100644 app/src/main/res/drawable/profile_bg_red.jpg create mode 100644 app/src/main/res/drawable/profile_bg_teal.jpg create mode 100644 app/src/main/res/layout/player_prioritize_item.xml create mode 100644 app/src/main/res/layout/player_quality_profile_dialog.xml create mode 100644 app/src/main/res/layout/player_quality_profile_item.xml create mode 100644 app/src/main/res/layout/player_select_source_priority.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt index 0d94eb08..24495a40 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt @@ -58,7 +58,7 @@ open class DoodLaExtractor : ExtractorApi() { val quality = Regex("\\d{3,4}p").find(response0.substringAfter("").substringBefore(""))?.groupValues?.get(0) return listOf( ExtractorLink( - trueUrl, + this.name, this.name, trueUrl, mainUrl, 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 2adc00d5..3d046267 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt @@ -58,7 +58,7 @@ open class GuardareStream : ExtractorApi() { jsonVideoData.data.forEach { callback.invoke( ExtractorLink( - it.file + ".${it.type}", + this.name, this.name, it.file + ".${it.type}", mainUrl, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt index d721dea8..13aa48c6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt @@ -30,7 +30,7 @@ open class Tantifilm : ExtractorApi() { val jsonvideodata = parseJson(response) return jsonvideodata.data.map { ExtractorLink( - it.file+".${it.type}", + this.name, this.name, it.file+".${it.type}", mainUrl, 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 86e21fd6..9ff1c52d 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 @@ -39,6 +39,7 @@ import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvidersIsActive +import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog @@ -108,8 +109,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() { // get() = episodes.isNotEmpty() // options for player - protected var currentPrefQuality = - Qualities.P2160.value // preferred maximum quality, used for ppl w bad internet or on cell + + /** + * Default profile 1 + * Decides how links should be sorted based on a priority system. + * This will be set in runtime based on settings. + **/ + protected var currentQualityProfile = 1 +// 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; @@ -1221,10 +1229,16 @@ open class FullScreenPlayer : AbstractPlayerFragment() { .toLong() * 1000L androidTVInterfaceOffSeekTime = - settingsManager.getInt(ctx.getString(R.string.android_tv_interface_off_seek_key), 10) + 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) + settingsManager.getInt( + ctx.getString(R.string.android_tv_interface_on_seek_key), + 10 + ) .toLong() * 1000L navigationBarHeight = ctx.getNavigationBarHeight() @@ -1257,10 +1271,20 @@ open class FullScreenPlayer : AbstractPlayerFragment() { ctx.getString(R.string.double_tap_pause_enabled_key), false ) - currentPrefQuality = settingsManager.getInt( - ctx.getString(if (ctx.isUsingMobileData()) R.string.quality_pref_mobile_data_key else R.string.quality_pref_key), - currentPrefQuality - ) + + val profiles = QualityDataHelper.getProfiles() + val type = if (ctx.isUsingMobileData()) + QualityDataHelper.QualityProfileType.Data + else QualityDataHelper.QualityProfileType.WiFi + + currentQualityProfile = + profiles.firstOrNull { it.type == type }?.id ?: profiles.firstOrNull()?.id + ?: currentQualityProfile + +// currentPrefQuality = settingsManager.getInt( +// ctx.getString(if (ctx.isUsingMobileData()) R.string.quality_pref_mobile_data_key else R.string.quality_pref_key), +// currentPrefQuality +// ) // useSystemBrightness = // settingsManager.getBoolean(ctx.getString(R.string.use_system_brightness_key), false) } 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 46f2bca9..e20a07fa 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 @@ -31,6 +31,10 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitl import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType +import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper +import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog +import com.lagradost.cloudstream3.ui.player.source_priority.SourcePriority +import com.lagradost.cloudstream3.ui.player.source_priority.SourcePriorityDialog import com.lagradost.cloudstream3.ui.result.* import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1 @@ -57,6 +61,7 @@ import kotlinx.coroutines.Job import java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap +import kotlin.math.abs class GeneratorPlayer : FullScreenPlayer() { companion object { @@ -188,17 +193,31 @@ class GeneratorPlayer : FullScreenPlayer() { player.addTimeStamps(listOf()) // clear stamps } - private fun sortLinks(useQualitySettings: Boolean = true): List> { - return currentLinks.sortedBy { - val (linkData, _) = it - var quality = linkData?.quality ?: Qualities.Unknown.value + private fun closestQuality(target: Int?): Qualities { + if (target == null) return Qualities.Unknown + return Qualities.values().minBy { abs(it.value - target) } + } - // we set all qualities above current max as reverse - if (useQualitySettings && quality > currentPrefQuality) { - quality = currentPrefQuality - quality - 1 - } - // negative because we want to sort highest quality first - -(quality) + private fun getLinkPriority( + qualityProfile: Int, + link: Pair + ): Int { + val (linkData, _) = link + + val qualityPriority = QualityDataHelper.getQualityPriority( + qualityProfile, + closestQuality(linkData?.quality) + ) + val sourcePriority = + QualityDataHelper.getSourcePriority(qualityProfile, linkData?.name) + + // negative because we want to sort highest quality first + return qualityPriority + sourcePriority + } + + private fun sortLinks(qualityProfile: Int): List> { + return currentLinks.sortedBy { + -getLinkPriority(qualityProfile, it) } } @@ -584,33 +603,39 @@ class GeneratorPlayer : FullScreenPlayer() { var sourceIndex = 0 var startSource = 0 + var sortedUrls = emptyList>() - val sortedUrls = sortLinks(useQualitySettings = false) - if (sortedUrls.isEmpty()) { - sourceDialog.findViewById(R.id.sort_sources_holder)?.isGone = true - } else { - startSource = sortedUrls.indexOf(currentSelectedLink) - sourceIndex = startSource + fun refreshLinks(qualityProfile: Int) { + sortedUrls = sortLinks(qualityProfile) + if (sortedUrls.isEmpty()) { + sourceDialog.findViewById(R.id.sort_sources_holder)?.isGone = + true + } else { + startSource = sortedUrls.indexOf(currentSelectedLink) + sourceIndex = startSource - val sourcesArrayAdapter = - ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) + val sourcesArrayAdapter = + ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) - sourcesArrayAdapter.addAll(sortedUrls.map { (link, uri) -> - val name = link?.name ?: uri?.name ?: "NULL" - "$name ${Qualities.getStringByInt(link?.quality)}" - }) + sourcesArrayAdapter.addAll(sortedUrls.map { (link, uri) -> + val name = link?.name ?: uri?.name ?: "NULL" + "$name ${Qualities.getStringByInt(link?.quality)}" + }) - providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE - providerList.adapter = sourcesArrayAdapter - providerList.setSelection(sourceIndex) - providerList.setItemChecked(sourceIndex, true) + providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE + providerList.adapter = sourcesArrayAdapter + providerList.setSelection(sourceIndex) + providerList.setItemChecked(sourceIndex, true) - providerList.setOnItemClickListener { _, _, which, _ -> - sourceIndex = which - providerList.setItemChecked(which, true) + providerList.setOnItemClickListener { _, _, which, _ -> + sourceIndex = which + providerList.setItemChecked(which, true) + } } } + refreshLinks(currentQualityProfile) + sourceDialog.setOnDismissListener { if (shouldDismiss) dismiss() selectSourceDialog = null @@ -650,6 +675,29 @@ class GeneratorPlayer : FullScreenPlayer() { sourceDialog.dismissSafe(activity) } + fun setProfileName(profile: Int) { + sourceDialog.source_settings_btt.setText( + QualityDataHelper.getProfileName( + profile + ) + ) + } + setProfileName(currentQualityProfile) + + sourceDialog.profiles_click_settings.setOnClickListener { + val activity = activity ?: return@setOnClickListener + QualityProfileDialog( + activity, + R.style.AlertDialogCustomBlack, + currentLinks.mapNotNull { it.first }, + currentQualityProfile + ) { profile -> + currentQualityProfile = profile.id + setProfileName(profile.id) + refreshLinks(profile.id) + }.show() + } + sourceDialog.subtitles_encoding_format?.apply { val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) @@ -847,7 +895,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun startPlayer() { if (isActive) return // we don't want double load when you skip loading - val links = sortLinks() + val links = sortLinks(currentQualityProfile) if (links.isEmpty()) { noLinksFound() return @@ -868,12 +916,12 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun hasNextMirror(): Boolean { - val links = sortLinks() + val links = sortLinks(currentQualityProfile) return links.isNotEmpty() && links.indexOf(currentSelectedLink) + 1 < links.size } override fun nextMirror() { - val links = sortLinks() + val links = sortLinks(currentQualityProfile) if (links.isEmpty()) { noLinksFound() return @@ -1314,6 +1362,15 @@ class GeneratorPlayer : FullScreenPlayer() { val turnVisible = it.isNotEmpty() val wasGone = overlay_loading_skip_button?.isGone == true overlay_loading_skip_button?.isVisible = turnVisible + + normalSafeApiCall { + currentLinks.lastOrNull()?.let { last -> + if (getLinkPriority(currentQualityProfile, last) >= QualityDataHelper.AUTO_SKIP_PRIORITY) { + startPlayer() + } + } + } + if (turnVisible && wasGone) { overlay_loading_skip_button?.requestFocus() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt new file mode 100644 index 00000000..8e0ce67c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt @@ -0,0 +1,60 @@ +package com.lagradost.cloudstream3.ui.player.source_priority + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.utils.AppUtils +import kotlinx.android.synthetic.main.player_prioritize_item.view.* + +data class SourcePriority( + val data: T, + val name: String, + var priority: Int +) + +class PriorityAdapter(override val items: MutableList>) : + AppUtils.DiffAdapter>(items) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return PriorityViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false) + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is PriorityViewHolder -> holder.bind(items[position]) + } + } + + class PriorityViewHolder( + itemView: View, + ) : RecyclerView.ViewHolder(itemView) { + fun bind(item: SourcePriority) { + val plusButton: ImageView = itemView.add_button + val subtractButton: ImageView = itemView.subtract_button + val priorityText: TextView = itemView.priority_text + val priorityNumber: TextView = itemView.priority_number + priorityText.text = item.name + + fun updatePriority() { + priorityNumber.text = item.priority.toString() + } + + updatePriority() + plusButton.setOnClickListener { + // If someone clicks til the integer limit then they deserve to crash. + item.priority++ + updatePriority() + } + + subtractButton.setOnClickListener { + item.priority-- + updatePriority() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt new file mode 100644 index 00000000..ff84c1f5 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt @@ -0,0 +1,116 @@ +package com.lagradost.cloudstream3.ui.player.source_priority + +import android.content.res.ColorStateList +import android.graphics.Typeface +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.ui.result.UiImage +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.UIHelper.setImage +import kotlinx.android.synthetic.main.player_quality_profile_item.view.card_view +import kotlinx.android.synthetic.main.player_quality_profile_item.view.outline +import kotlinx.android.synthetic.main.player_quality_profile_item.view.profile_image_background +import kotlinx.android.synthetic.main.player_quality_profile_item.view.profile_text +import kotlinx.android.synthetic.main.player_quality_profile_item.view.text_is_mobile_data +import kotlinx.android.synthetic.main.player_quality_profile_item.view.text_is_wifi + +class ProfilesAdapter( + override val items: MutableList, + val usedProfile: Int, + val clickCallback: (oldIndex: Int?, newIndex: Int) -> Unit, +) : + AppUtils.DiffAdapter( + items, + comparison = { first: QualityDataHelper.QualityProfile, second: QualityDataHelper.QualityProfile -> + first.id == second.id + }) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return ProfilesViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.player_quality_profile_item, parent, false) + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is ProfilesViewHolder -> holder.bind(items[position], position) + } + } + + private var currentItem: Pair? = null + + fun getCurrentProfile(): QualityDataHelper.QualityProfile? { + return currentItem?.second + } + + inner class ProfilesViewHolder( + itemView: View, + ) : RecyclerView.ViewHolder(itemView) { + private val art = listOf( + R.drawable.profile_bg_teal, + R.drawable.profile_bg_blue, + R.drawable.profile_bg_dark_blue, + R.drawable.profile_bg_purple, + R.drawable.profile_bg_pink, + R.drawable.profile_bg_red, + R.drawable.profile_bg_orange, + ) + + fun bind(item: QualityDataHelper.QualityProfile, index: Int) { + val priorityText: TextView = itemView.profile_text + val profileBg: ImageView = itemView.profile_image_background + val wifiText: TextView = itemView.text_is_wifi + val dataText: TextView = itemView.text_is_mobile_data + val outline: View = itemView.outline + val cardView: View = itemView.card_view + + priorityText.text = item.name.asString(itemView.context) + dataText.isVisible = item.type == QualityDataHelper.QualityProfileType.Data + wifiText.isVisible = item.type == QualityDataHelper.QualityProfileType.WiFi + + fun setCurrentItem() { + val prevIndex = currentItem?.first + // Prevent UI bug when re-selecting the item quickly + if (prevIndex == index) { + return + } + currentItem = index to item + clickCallback.invoke(prevIndex, index) + } + + outline.isVisible = currentItem?.second?.id == item.id + + profileBg.setImage(UiImage.Drawable(art[index % art.size]), null, false) { palette -> + val color = palette.getDarkVibrantColor( + ContextCompat.getColor( + itemView.context, + R.color.dubColorBg + ) + ) + wifiText.backgroundTintList = ColorStateList.valueOf(color) + dataText.backgroundTintList = ColorStateList.valueOf(color) + } + + val textStyle = + if (item.id == usedProfile) { + Typeface.BOLD + } else { + Typeface.NORMAL + } + + priorityText.setTypeface(null, textStyle) + + cardView.setOnClickListener { + setCurrentItem() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt new file mode 100644 index 00000000..96249db4 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt @@ -0,0 +1,159 @@ +package com.lagradost.cloudstream3.ui.player.source_priority + +import android.content.Context +import androidx.annotation.StringRes +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.debugAssert +import com.lagradost.cloudstream3.ui.result.UiText +import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount +import com.lagradost.cloudstream3.utils.Qualities + +object QualityDataHelper { + private const val VIDEO_SOURCE_PRIORITY = "video_source_priority" + private const val VIDEO_PROFILE_NAME = "video_profile_name" + private const val VIDEO_QUALITY_PRIORITY = "video_quality_priority" + private const val VIDEO_PROFILE_TYPE = "video_profile_type" + private const val DEFAULT_SOURCE_PRIORITY = 1 + /** + * Automatically skip loading links once this priority is reached + **/ + const val AUTO_SKIP_PRIORITY = 10 + + /** + * Must be higher than amount of QualityProfileTypes + **/ + private const val PROFILE_COUNT = 7 + + /** + * Unique guarantees that there will always be one of this type in the profile list. + **/ + enum class QualityProfileType(@StringRes val stringRes: Int, val unique: Boolean) { + None(R.string.none, false), + WiFi(R.string.wifi, true), + Data(R.string.mobile_data, true) + } + + data class QualityProfile( + val name: UiText, + val id: Int, + val type: QualityProfileType + ) + + fun getSourcePriority(profile: Int, name: String?): Int { + if (name == null) return DEFAULT_SOURCE_PRIORITY + return getKey( + "$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile", + name, + DEFAULT_SOURCE_PRIORITY + ) ?: DEFAULT_SOURCE_PRIORITY + } + + fun setSourcePriority(profile: Int, name: String, priority: Int) { + setKey("$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile", name, priority) + } + + fun setProfileName(profile: Int, name: String?) { + val path = "$currentAccount/$VIDEO_PROFILE_NAME/$profile" + if (name == null) { + removeKey(path) + } else { + setKey(path, name.trim()) + } + } + + fun getProfileName(profile: Int): UiText { + return getKey("$currentAccount/$VIDEO_PROFILE_NAME/$profile")?.let { txt(it) } + ?: txt(R.string.profile_number, profile) + } + + fun getQualityPriority(profile: Int, quality: Qualities): Int { + return getKey( + "$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile", + quality.value.toString(), + quality.defaultPriority + ) ?: quality.defaultPriority + } + + fun setQualityPriority(profile: Int, quality: Qualities, priority: Int) { + setKey( + "$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile", + quality.value.toString(), + priority + ) + } + + fun getQualityProfileType(profile: Int): QualityProfileType { + return getKey("$currentAccount/$VIDEO_PROFILE_TYPE/$profile") ?: QualityProfileType.None + } + + fun setQualityProfileType(profile: Int, type: QualityProfileType?) { + val path = "$currentAccount/$VIDEO_PROFILE_TYPE/$profile" + if (type == QualityProfileType.None) { + removeKey(path) + } else { + setKey(path, type) + } + } + + /** + * Gets all quality profiles, always includes one profile with WiFi and Data + * Must under all circumstances at least return one profile + **/ + fun getProfiles(): List { + val availableTypes = QualityProfileType.values().toMutableList() + val profiles = (1..PROFILE_COUNT).map { profileNumber -> + // Get the real type + val type = getQualityProfileType(profileNumber) + + // This makes it impossible to get more than one of each type + // Duplicates will be turned to None + val uniqueType = if (type.unique && !availableTypes.remove(type)) { + QualityProfileType.None + } else { + type + } + + QualityProfile( + getProfileName(profileNumber), + profileNumber, + uniqueType + ) + }.toMutableList() + + /** + * If no profile of this type exists: insert it on the earliest profile with None type + **/ + fun insertType( + list: MutableList, + type: QualityProfileType + ) { + if (list.any { it.type == type }) return + val index = + list.indexOfFirst { it.type == QualityProfileType.None } + list.getOrNull(index)?.copy(type = type) + ?.let { fixed -> + list.set(index, fixed) + } + } + + QualityProfileType.values().forEach { + if (it.unique) insertType(profiles, it) + } + + debugAssert({ + !QualityProfileType.values().all { type -> + !type.unique || profiles.any { it.type == type } + } + }, { "All unique quality types do not exist" }) + + debugAssert({ + profiles.isEmpty() + }, { "No profiles!" }) + + return profiles + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt new file mode 100644 index 00000000..28a6365f --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt @@ -0,0 +1,106 @@ +package com.lagradost.cloudstream3.ui.player.source_priority + +import android.app.Dialog +import android.view.View +import android.widget.TextView +import androidx.annotation.StyleRes +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentActivity +import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfileName +import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfiles +import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog +import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe +import kotlinx.android.synthetic.main.player_quality_profile_dialog.* + +class QualityProfileDialog( + val activity: FragmentActivity, + @StyleRes val themeRes: Int, + private val links: List, + private val usedProfile: Int, + private val profileSelectionCallback: (QualityDataHelper.QualityProfile) -> Unit +) : Dialog(activity, themeRes) { + override fun show() { + setContentView(R.layout.player_quality_profile_dialog) + val profilesRecyclerView: RecyclerView = profiles_recyclerview + val useBtt: View = use_btt + val editBtt: View = edit_btt + val cancelBtt: View = cancel_btt + val defaultBtt: View = set_default_btt + val currentProfileText: TextView = currently_selected_profile_text + val selectedItemActionsHolder: View = selected_item_holder + + fun getCurrentProfile(): QualityDataHelper.QualityProfile? { + return (profilesRecyclerView.adapter as? ProfilesAdapter)?.getCurrentProfile() + } + + fun refreshProfiles() { + currentProfileText.text = getProfileName(usedProfile).asString(context) + (profilesRecyclerView.adapter as? ProfilesAdapter)?.updateList(getProfiles()) + } + + profilesRecyclerView.adapter = ProfilesAdapter( + mutableListOf(), + usedProfile, + ) { oldIndex: Int?, newIndex: Int -> + profilesRecyclerView.adapter?.notifyItemChanged(newIndex) + selectedItemActionsHolder.alpha = 1f + if (oldIndex != null) { + profilesRecyclerView.adapter?.notifyItemChanged(oldIndex) + } + } + + refreshProfiles() + + editBtt.setOnClickListener { + getCurrentProfile()?.let { profile -> + SourcePriorityDialog(context, themeRes, links, profile) { + refreshProfiles() + }.show() + } + } + + + defaultBtt.setOnClickListener { + val currentProfile = getCurrentProfile() ?: return@setOnClickListener + val choices = QualityDataHelper.QualityProfileType.values() + .filter { it != QualityDataHelper.QualityProfileType.None } + val choiceNames = choices.map { txt(it.stringRes).asString(context) } + + activity.showBottomDialog( + choiceNames, + choices.indexOf(currentProfile.type), + txt(R.string.set_default).asString(context), + false, + {}, + { index -> + val pickedChoice = choices.getOrNull(index) ?: return@showBottomDialog + // Remove previous picks + if (pickedChoice.unique) { + getProfiles().filter { it.type == pickedChoice }.forEach { + QualityDataHelper.setQualityProfileType(it.id, null) + } + } + + QualityDataHelper.setQualityProfileType(currentProfile.id, pickedChoice) + refreshProfiles() + }) + } + + cancelBtt.setOnClickListener { + this.dismissSafe() + } + + useBtt.setOnClickListener { + getCurrentProfile()?.let { + profileSelectionCallback.invoke(it) + this.dismissSafe() + } + } + + super.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt new file mode 100644 index 00000000..efc1f1b8 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt @@ -0,0 +1,105 @@ +package com.lagradost.cloudstream3.ui.player.source_priority + +import android.app.Dialog +import android.content.Context +import android.view.View +import android.widget.EditText +import android.widget.TextView +import androidx.annotation.StyleRes +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.FragmentActivity +import androidx.recyclerview.widget.RecyclerView +import androidx.work.impl.constraints.controllers.ConstraintController +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe +import kotlinx.android.synthetic.main.player_select_source_priority.* + +class SourcePriorityDialog( + ctx: Context, + @StyleRes themeRes: Int, + val links: List, + private val profile: QualityDataHelper.QualityProfile, + /** + * Notify that the profile overview should be updated, for example if the name has been updated + * Should not be called excessively. + **/ + private val updatedCallback: () -> Unit +) : Dialog(ctx, themeRes) { + override fun show() { + setContentView(R.layout.player_select_source_priority) + val sourcesRecyclerView: RecyclerView = sort_sources + val qualitiesRecyclerView: RecyclerView = sort_qualities + val profileText: EditText = profile_text_editable + val saveBtt: View = save_btt + val exitBtt: View = close_btt + val helpBtt: View = help_btt + + profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context)) + profileText.hint = txt(R.string.profile_number, profile.id).asString(context) + + sourcesRecyclerView.adapter = PriorityAdapter( + links.map { link -> + SourcePriority( + null, + link.source, + QualityDataHelper.getSourcePriority(profile.id, link.source) + ) + }.distinctBy { it.name }.sortedBy { -it.priority }.toMutableList() + ) + + qualitiesRecyclerView.adapter = PriorityAdapter( + Qualities.values().mapNotNull { + SourcePriority( + it, + Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, + QualityDataHelper.getQualityPriority(profile.id, it) + ) + }.sortedBy { -it.priority }.toMutableList() + ) + + @Suppress("UNCHECKED_CAST") // We know the types + saveBtt.setOnClickListener { + val qualityAdapter = qualitiesRecyclerView.adapter as? PriorityAdapter + val sourcesAdapter = sourcesRecyclerView.adapter as? PriorityAdapter + + val qualities = qualityAdapter?.items ?: emptyList() + val sources = sourcesAdapter?.items ?: emptyList() + + qualities.forEach { + val data = it.data as? Qualities ?: return@forEach + QualityDataHelper.setQualityPriority(profile.id, data, it.priority) + } + + sources.forEach { + QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority) + } + + qualityAdapter?.updateList(qualities.sortedBy { -it.priority }) + sourcesAdapter?.updateList(sources.sortedBy { -it.priority }) + + val savedProfileName = profileText.text.toString() + if (savedProfileName.isBlank()) { + QualityDataHelper.setProfileName(profile.id, null) + } else { + QualityDataHelper.setProfileName(profile.id, savedProfileName) + } + updatedCallback.invoke() + } + + exitBtt.setOnClickListener { + this.dismissSafe() + } + + helpBtt.setOnClickListener { + AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { + setMessage(R.string.quality_profile_help) + }.show() + } + + super.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index 81ef8d57..f2eca5b8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -72,7 +72,7 @@ sealed class UiImage { fun ImageView?.setImage(value: UiImage?, fadeIn: Boolean = true) { when (value) { - is UiImage.Image -> setImageImage(value,fadeIn) + is UiImage.Image -> setImageImage(value, fadeIn) is UiImage.Drawable -> setImageDrawable(value) null -> { this?.isVisible = false @@ -88,7 +88,7 @@ fun ImageView?.setImageImage(value: UiImage.Image, fadeIn: Boolean = true) { fun ImageView?.setImageDrawable(value: UiImage.Drawable) { if (this == null) return this.isVisible = true - setImageResource(value.resId) + this.setImage(UiImage.Drawable(value.resId)) } @JvmName("imgNull") 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 516cd990..3bdb64e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -117,7 +117,7 @@ object DataStoreHelper { /** * A datastore wide account for future implementations of a multiple account system **/ - private var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION + var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION fun getAllWatchStateIds(): List? { val folder = "$currentAccount/$RESULT_WATCH_STATE" 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 5062ebd9..f6373dce 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -114,16 +114,16 @@ data class ExtractorSubtitleLink( */ val schemaStripRegex = Regex("""^(https:|)//(www\.|)""") -enum class Qualities(var value: Int) { - Unknown(400), - P144(144), // 144p - P240(240), // 240p - P360(360), // 360p - P480(480), // 480p - P720(720), // 720p - P1080(1080), // 1080p - P1440(1440), // 1440p - P2160(2160); // 4k or 2160p +enum class Qualities(var value: Int, val defaultPriority: Int) { + Unknown(400, 4), + P144(144, 0), // 144p + P240(240, 2), // 240p + P360(360, 3), // 360p + P480(480, 4), // 480p + P720(720, 5), // 720p + P1080(1080, 6), // 1080p + P1440(1440, 7), // 1440p + P2160(2160, 8); // 4k or 2160p companion object { fun getStringByInt(qual: Int?): String { @@ -135,6 +135,14 @@ enum class Qualities(var value: Int) { else -> "${qual}p" } } + fun getStringByIntFull(quality: Int): String { + return when (quality) { + 0 -> "Auto" + Unknown.value -> "Unknown" + P2160.value -> "4K" + else -> "${quality}p" + } + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt index 2dc6846c..1f6d726d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt @@ -250,17 +250,6 @@ object SingleSelectionHelper { ) } - fun showBottomDialog( - items: List, - selectedIndex: Int, - name: String, - showApply: Boolean, - dismissCallback: () -> Unit, - callback: (Int) -> Unit, - ) { - - } - /** Only for a low amount of items */ fun Activity?.showBottomDialog( items: List, 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 c300d615..7d798204 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -44,12 +44,13 @@ 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.RequestOptions.bitmapTransform import com.bumptech.glide.request.target.Target import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings -import com.lagradost.cloudstream3.utils.GlideOptions.bitmapTransform import jp.wasabeef.glide.transformations.BlurTransformation import kotlin.math.roundToInt @@ -188,11 +189,30 @@ object UIHelper { fadeIn: Boolean = true, colorCallback: ((Palette) -> Unit)? = null ): Boolean { - if (this == null || url.isNullOrBlank()) return false + if (url.isNullOrBlank()) return false + this.setImage(UiImage.Image(url, headers, errorImageDrawable), errorImageDrawable, fadeIn, colorCallback) + return true + } + + fun ImageView?.setImage( + uiImage: UiImage?, + @DrawableRes + errorImageDrawable: Int? = null, + fadeIn: Boolean = true, + colorCallback: ((Palette) -> Unit)? = null + ): Boolean { + if (this == null || uiImage == null) return false + + val (glideImage, identifier) = + (uiImage as? UiImage.Drawable)?.resId?.let { + it to it.toString() + } ?: (uiImage as? UiImage.Image)?.let { image -> + GlideUrl(image.url) { image.headers ?: emptyMap() } to image.url + } ?: return false return try { val builder = GlideApp.with(this) - .load(GlideUrl(url) { headers ?: emptyMap() }) + .load(glideImage) .skipMemoryCache(true) .diskCacheStrategy(DiskCacheStrategy.ALL).let { req -> if (fadeIn) @@ -211,7 +231,13 @@ object UIHelper { isFirstResource: Boolean ): Boolean { resource?.toBitmapOrNull() - ?.let { bitmap -> createPaletteAsync(url, bitmap, colorCallback) } + ?.let { bitmap -> + createPaletteAsync( + identifier, + bitmap, + colorCallback + ) + } return false } diff --git a/app/src/main/res/drawable/baseline_help_outline_24.xml b/app/src/main/res/drawable/baseline_help_outline_24.xml new file mode 100644 index 00000000..3a72cda0 --- /dev/null +++ b/app/src/main/res/drawable/baseline_help_outline_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_remove_24.xml b/app/src/main/res/drawable/baseline_remove_24.xml index 791a2f81..f4455598 100644 --- a/app/src/main/res/drawable/baseline_remove_24.xml +++ b/app/src/main/res/drawable/baseline_remove_24.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> + android:tint="?attr/white"> diff --git a/app/src/main/res/drawable/profile_bg_blue.jpg b/app/src/main/res/drawable/profile_bg_blue.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e573439b04e57d86042066539a6b521621662eb8 GIT binary patch literal 21107 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

16UgK~XVKnIr>}rG^j`L`1 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

T4~l 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 IyM1T>*`W+&=l}o! literal 117989 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 diff --git a/app/src/main/res/drawable/profile_bg_teal.jpg b/app/src/main/res/drawable/profile_bg_teal.jpg index 75ef777bfebf000ff741ad7dab8e0aacc69a6937..9323665088fa221aa7d7c777bb8a901848f88e43 100644 GIT binary patch literal 121206 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 From a95fcfc9dba211187470f4250db4ca1e96eb7564 Mon Sep 17 00:00:00 2001 From: Saksham Shekher <95137948+OshekharO@users.noreply.github.com> Date: Tue, 27 Jun 2023 18:24:05 +0530 Subject: [PATCH 088/570] SpeedoStream update (#493) --- .../com/lagradost/cloudstream3/extractors/SpeedoStream.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt index 8ef6c463..90104ace 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt @@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper class SpeedoStream1 : SpeedoStream() { - override val mainUrl = "https://speedostream.nl" + override val mainUrl = "https://speedostream.pm" } open class SpeedoStream : ExtractorApi() { @@ -39,4 +39,4 @@ open class SpeedoStream : ExtractorApi() { ) -} \ No newline at end of file +} From da0be63b7ce5241e69d1221f60d1a24f49d778e3 Mon Sep 17 00:00:00 2001 From: Nexus <79303560+Nexus-Gits@users.noreply.github.com> Date: Fri, 30 Jun 2023 21:49:52 +0530 Subject: [PATCH 089/570] Update ByteShare.kt (#494) * Update ByteShare.kt --- .../java/com/lagradost/cloudstream3/extractors/ByteShare.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt index 3e0a03c0..2d56fe1f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt @@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.utils.* open class ByteShare : ExtractorApi() { override val name = "ByteShare" - override val mainUrl = "https://byteshare.net" + override val mainUrl = "https://byteshare.to" override val requiresReferer = false override suspend fun getUrl(url: String, referer: String?): List { @@ -20,4 +20,4 @@ open class ByteShare : ExtractorApi() { ) return sources } -} \ No newline at end of file +} From 51c10891626fee6e3a1638ad4c1e020f1fc6bf05 Mon Sep 17 00:00:00 2001 From: Nexus <79303560+Nexus-Gits@users.noreply.github.com> Date: Sun, 2 Jul 2023 23:11:19 +0530 Subject: [PATCH 090/570] Updated some of the extractor Domains (#495) * Update AsianLoad.kt Asianload domain changed * Update Hxfile.kt Some of the old url fix * Update Moviehab.kt * Update MultiQuality.kt * Update AsianLoad.kt * Update Hxfile.kt * Update MultiQuality.kt --- .../java/com/lagradost/cloudstream3/extractors/AsianLoad.kt | 4 ++-- .../java/com/lagradost/cloudstream3/extractors/Hxfile.kt | 6 +++--- .../java/com/lagradost/cloudstream3/extractors/Moviehab.kt | 4 ++-- .../com/lagradost/cloudstream3/extractors/MultiQuality.kt | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt index 7a62fb52..4bed3169 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt @@ -9,7 +9,7 @@ import java.net.URI open class AsianLoad : ExtractorApi() { override var name = "AsianLoad" - override var mainUrl = "https://asianembed.io" + override var mainUrl = "https://asianhdplay.pro" override val requiresReferer = true private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""") @@ -43,4 +43,4 @@ open class AsianLoad : ExtractorApi() { return extractedLinksList } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt index f5dde774..bfd7cae5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt @@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson class Neonime7n : Hxfile() { override val name = "Neonime7n" - override val mainUrl = "https://7njctn.neonime.watch" + override val mainUrl = "https://neonime.fun" override val redirect = false } @@ -19,7 +19,7 @@ class Neonime8n : Hxfile() { class KotakAnimeid : Hxfile() { override val name = "KotakAnimeid" - override val mainUrl = "https://kotakanimeid.com" + override val mainUrl = "https://nontonanimeid.bio" override val requiresReferer = true } @@ -97,4 +97,4 @@ open class Hxfile : ExtractorApi() { @JsonProperty("label") val label: String? ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt index aaa33ca1..51939cc2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt @@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper class MoviehabNet : Moviehab() { - override var mainUrl = "https://play.moviehab.net" + override var mainUrl = "https://play.moviehab.asia" } open class Moviehab : ExtractorApi() { @@ -41,4 +41,4 @@ open class Moviehab : ExtractorApi() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt index 44657196..c7f4ac76 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt @@ -9,7 +9,7 @@ import java.net.URI open class MultiQuality : ExtractorApi() { override var name = "MultiQuality" - override var mainUrl = "https://gogo-play.net" + override var mainUrl = "https://anihdplay.com" private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""") private val m3u8Regex = Regex(""".*?(\d*).m3u8""") private val urlRegex = Regex("""(.*?)([^/]+$)""") @@ -56,4 +56,4 @@ open class MultiQuality : ExtractorApi() { return extractedLinksList } } -} \ No newline at end of file +} From 847957362f498c7801ff3d92c4e2a914d3202a11 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Mon, 10 Jul 2023 06:52:03 +0700 Subject: [PATCH 091/570] Extractor: added Pixeldrain, Wibufile and fix some extractors (#502) Co-authored-by: Sofie99 --- .../cloudstream3/extractors/DoodExtractor.kt | 4 ++ .../cloudstream3/extractors/Filesim.kt | 19 +++++++++- .../cloudstream3/extractors/Pixeldrain.kt | 30 +++++++++++++++ .../cloudstream3/extractors/StreamSB.kt | 25 +++++++++++++ .../cloudstream3/extractors/Wibufile.kt | 37 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 11 ++++++ 6 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Pixeldrain.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt index 24495a40..8dcfb859 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt @@ -7,6 +7,10 @@ import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.getQualityFromName import kotlinx.coroutines.delay +class Dooood : DoodLaExtractor() { + override var mainUrl = "https://dooood.com" +} + class DoodWfExtractor : DoodLaExtractor() { override var mainUrl = "https://dood.wf" } 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 4c1791a8..be0efd0c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt @@ -5,6 +5,16 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 +class Guccihide : Filesim() { + override val name = "Guccihide" + override var mainUrl = "https://guccihide.com" +} + +class Ahvsh : Filesim() { + override val name = "Ahvsh" + override var mainUrl = "https://ahvsh.com" +} + class Moviesm4u : Filesim() { override val mainUrl = "https://moviesm4u.com" override val name = "Moviesm4u" @@ -15,6 +25,11 @@ class FileMoonIn : Filesim() { override val name = "FileMoon" } +class StreamhideTo : Filesim() { + override val mainUrl = "https://streamhide.to" + override val name = "Streamhide" +} + class StreamhideCom : Filesim() { override var name: String = "Streamhide" override var mainUrl: String = "https://streamhide.com" @@ -42,7 +57,7 @@ class FileMoonSx : Filesim() { open class Filesim : ExtractorApi() { override val name = "Filesim" override val mainUrl = "https://files.im" - override val requiresReferer = false + override val requiresReferer = true override suspend fun getUrl( url: String, @@ -50,7 +65,7 @@ open class Filesim : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val response = app.get(url, referer = mainUrl).document + val response = app.get(url, referer = referer).document response.select("script[type=text/javascript]").map { script -> if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) { val unpackedscript = getAndUnpack(script.data()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pixeldrain.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pixeldrain.kt new file mode 100644 index 00000000..9b481240 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pixeldrain.kt @@ -0,0 +1,30 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities + +open class Pixeldrain : ExtractorApi() { + override val name = "Pixeldrain" + override val mainUrl = "https://pixeldrain.com" + override val requiresReferer = false + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val mId = Regex("/([ul]/[\\da-zA-Z\\-]+)").find(url)?.groupValues?.get(1)?.split("/") + callback.invoke( + ExtractorLink( + this.name, + this.name, + "$mainUrl/api/file/${mId?.last() ?: return}?download", + url, + Qualities.Unknown.value, + ) + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt index 3d2a81b7..df050cf3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt @@ -8,6 +8,31 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper import kotlin.random.Random +class Sblona : StreamSB() { + override var name = "Sblona" + override var mainUrl = "https://sblona.com" +} + +class Lvturbo : StreamSB() { + override var name = "Lvturbo" + override var mainUrl = "https://lvturbo.com" +} + +class Sbrapid : StreamSB() { + override var name = "Sbrapid" + override var mainUrl = "https://sbrapid.com" +} + +class Sbface : StreamSB() { + override var name = "Sbface" + override var mainUrl = "https://sbface.com" +} + +class Sbsonic : StreamSB() { + override var name = "Sbsonic" + override var mainUrl = "https://sbsonic.com" +} + class Vidgomunimesb : StreamSB() { override var mainUrl = "https://vidgomunimesb.xyz" } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt new file mode 100644 index 00000000..ae1e872a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt @@ -0,0 +1,37 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import java.net.URI + +open class Wibufile : ExtractorApi() { + override val name: String = "Wibufile" + override val mainUrl: String = "https://wibufile.com" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get(url).text + val video = Regex("src: ['\"](.*?)['\"]").find(res)?.groupValues?.get(1) + + callback.invoke( + ExtractorLink( + name, + name, + video ?: return, + "$mainUrl/", + Qualities.Unknown.value, + URI(url).path.endsWith(".m3u8") + ) + ) + + } + +} \ No newline at end of file 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 f6373dce..f0c1ea3b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -244,6 +244,7 @@ val extractorApis: MutableList = arrayListOf( XStreamCdn(), StreamSB(), + Sblona(), Vidgomunimesb(), StreamSB1(), StreamSB2(), @@ -265,6 +266,10 @@ val extractorApis: MutableList = arrayListOf( Sbflix(), Streamsss(), Sbspeed(), + Sbsonic(), + Sbface(), + Sbrapid(), + Lvturbo(), Fastream(), @@ -300,6 +305,7 @@ val extractorApis: MutableList = arrayListOf( DoodToExtractor(), DoodSoExtractor(), DoodLaExtractor(), + Dooood(), DoodWsExtractor(), DoodShExtractor(), DoodWatchExtractor(), @@ -366,9 +372,14 @@ val extractorApis: MutableList = arrayListOf( Movhide(), StreamhideCom(), + StreamhideTo(), + Pixeldrain(), + Wibufile(), FileMoonIn(), Moviesm4u(), Filesim(), + Ahvsh(), + Guccihide(), FileMoon(), FileMoonSx(), Vido(), From 525bf8d86116036b8a1e370f44aa19db8fc0cbcf Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 9 Jul 2023 01:35:42 +0200 Subject: [PATCH 092/570] Translated using Weblate (Odia) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 38.9% (242 of 621 strings) Translated using Weblate (Dutch) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Turkish) Currently translated at 99.5% (618 of 621 strings) Translated using Weblate (Turkish) Currently translated at 99.5% (618 of 621 strings) Translated using Weblate (German) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Malay) Currently translated at 20.4% (127 of 621 strings) Translated using Weblate (Italian) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (German) Currently translated at 99.8% (620 of 621 strings) Translated using Weblate (Polish) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Czech) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Odia) Currently translated at 39.1% (239 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Hebrew) Currently translated at 94.9% (579 of 610 strings) Translated using Weblate (Malay) Currently translated at 18.1% (111 of 610 strings) Translated using Weblate (Urdu) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Odia) Currently translated at 39.0% (238 of 610 strings) Translated using Weblate (Norwegian Nynorsk) Currently translated at 46.0% (281 of 610 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 96.7% (590 of 610 strings) Translated using Weblate (Odia) Currently translated at 39.0% (238 of 610 strings) Translated using Weblate (Odia) Currently translated at 38.6% (236 of 610 strings) Translated using Weblate (Bengali) Currently translated at 38.1% (233 of 610 strings) Co-authored-by: Aftabuzzaman Co-authored-by: Aitor Salaberria Co-authored-by: Alexthegib Co-authored-by: Andreas Co-authored-by: Bananenbrot Co-authored-by: BluTiger Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com> Co-authored-by: Dan Co-authored-by: Efe Devirgen Co-authored-by: Eryk Michalak Co-authored-by: Ettore Atalan Co-authored-by: Fjuro Co-authored-by: Hosted Weblate Co-authored-by: Kai Co-authored-by: Levi Klippel Co-authored-by: Nathan Khutorskoy Co-authored-by: Rex_sa Co-authored-by: Subham Jena Co-authored-by: Sufyan Zahoor Jutt Co-authored-by: enescan201 Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/he/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ms/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nn/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/or/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ur/ Translation: Cloudstream/App --- app/src/main/res/values-ar/strings.xml | 19 ++- app/src/main/res/values-bn/strings.xml | 31 ++--- app/src/main/res/values-cs/strings.xml | 19 ++- app/src/main/res/values-de/strings.xml | 19 ++- app/src/main/res/values-es/strings.xml | 21 ++- app/src/main/res/values-in/strings.xml | 19 ++- app/src/main/res/values-it/strings.xml | 19 ++- app/src/main/res/values-iw/strings.xml | 4 +- app/src/main/res/values-ms/strings.xml | 40 +++++- app/src/main/res/values-nl/strings.xml | 19 ++- app/src/main/res/values-nn/strings.xml | 15 ++- app/src/main/res/values-no/strings.xml | 34 ++--- app/src/main/res/values-or/strings.xml | 11 +- app/src/main/res/values-pl/strings.xml | 19 ++- app/src/main/res/values-pt/strings.xml | 19 ++- app/src/main/res/values-tr/strings.xml | 23 +++- app/src/main/res/values-uk/strings.xml | 19 ++- app/src/main/res/values-ur/strings.xml | 179 ++++++++++++++++++++++++- 18 files changed, 473 insertions(+), 56 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index c1f07d6c..6b722e43 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -561,4 +561,21 @@ تجاوز حظر GitHub باستخدام jsdelivr ، قد يتسبب في تأخير التحديثات لبضعة أيام. وكيل raw.githubusercontent.com جودة المشاهدة المفضلة (بيانات الجوال) - + الملف الشخصي %d + واي فاي + بيانات الهاتف + تعيين الافتراضي + استخدام + تعديل + الملفات التعريفية + مساعدة + ‎‎هنا يمكنك تغيير طريقة ترتيب المصادر. إذا كان للفيديو أولوية أعلى ، فسيظهر في الأعلى في اختيار المصدر. يمثل مجموع أولوية المصدر وأولوية الجودة أولوية الفيديو. +\n +\nالمصدر أ: 3 +\nالجودة ب: 7 +\nسيكون لها أولوية فيديو مجمعة تبلغ 10. +\n +\nملاحظة: إذا كان المجموع 10 أو أكثر ، فسيتخطى المشغل التحميل تلقائيًا عند تحميل هذا الرابط! + النوعيات + خلفية الملف الشخصي + \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 12752938..cc272a3a 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -7,7 +7,7 @@ %dদিন %dঘন্টা %dমিনিট %dঘন্টা %dমিনিট %d মিনিট - এপিসোড পোস্টার + পর্বের পোস্টার মূল পোস্টার পরবর্তী র‍্যান্ডম ফিরে যান @@ -24,9 +24,9 @@ ডাউনলোডসমূহ সেটিংস %s এপি %d - এপিসোড %d রিলিজ হবে + পর্ব %d মুক্তির তারিখ শেয়ার - ব্রাঊজারে খুলুন + ব্রাউজারে খুলুন জনরা খুঁজুন… খুঁজুন %s… @@ -35,7 +35,7 @@ পিছনে যান কোন তথ্য নেই আরো অপশন - পরবর্তি এপিসোড + পরবর্তী পর্ব লোডিং বাদ দিন লোডিং… দেখা হচ্ছে @@ -46,10 +46,10 @@ কোন কিছুই না পুনরায় দেখা হচ্ছে টরেন্ট স্ট্রিম করুন - সোর্সসমূহ + উৎসসমূহ সাবটাইটেলসমূহ আবার সংযোগ দেওয়ার চেষ্টা করুন… - এপিসোড চালান + পর্ব চালান ডাউনলোড ডাউনলোড হয়েছে ডাউনলোড চলছে @@ -61,22 +61,22 @@ লিংক লোডিং ব্যর্থ ডাব সাব - ফাইল প্লে করুন + ফাইল চালান ডাউনলোড থামান আটো বাগ রিপোর্ট বন্ধ করুন আরো তথ্য বন্ধ করুন - প্লে + চালান তথ্য বুকমার্কসমূহ বাদ দিন বুকমার্ক করুন - এপ্লাই করুন + প্রয়োগ করুন বাদ দিন কপি করুন বন্ধ করুন মুছুন - সেভ করুন + সংরক্ষণ করুন ভিডিও এর গতি ফন্ট আউটলাইন কালার সাবটাইটেল এজের ধরণ @@ -100,11 +100,11 @@ ডাউনলোড ব্যর্থ ইন্টারনাল স্টোরেজ বুকমার্ক ফিল্টার করুন - ফাইল ডিলিট করুন + ফাইল মুছুন ডাউনলোড চালু করুন সাবটাইটেল সেটিংস সাবটাইটেল ব্যাকগ্রাউন্ড কালার - ফন্ট কালার + লেখার রং এটা একটি টরেন্ট প্রভাইডার, ভিপিএন ব্যবহার করা উচিৎ উইন্ডো কালার ফন্টের সাইজ @@ -130,8 +130,8 @@ ভিডিওপ্লেয়ার এ সময় নিয়ন্ত্রণ করতে, ডানে অথবা বামে সোয়াইপ করুন সেটিংস পরিবর্তন করতে সোয়াইপ করুন উজ্জ্বলতা অথবা স্বরমাত্রা পরিবর্তন করতে যথাক্রমে বামে অথবা ডানে সোয়াইপ করুন - স্বয়ংক্রিয়ভাবে পরবর্তী এপিসোড প্লে করুন - পরবর্তী এপিসোডটি চালু করুন যখন চলতি এপিসোডটি শেষ হয় + স্বয়ংক্রিয়ভাবে পরবর্তী পর্ব চালান + বর্তমান পর্বটি শেষ হলে পরের পর্বটি চালান থামতে দুইবার চাপুন ডাটা জমা হয়েছে স্টোরেজ এর অনুমতি অনুপস্থিত। দয়া করে আবার চেষ্টা করুন। @@ -148,4 +148,5 @@ আগাতে ডবল ট্যাপ করুন আইজেনগ্রাভি মোড আপডেট শুরু হয়েছে - + ব্রাউজার + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 16ceff2d..5cbdb7db 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -553,4 +553,21 @@ Vrátit zpět Obchází blokování GitHubu pomocí jsDelivr. Může způsobit zpoždění aktualizací o několik dní. Obcházení ISP - + Profil %d + Wi-Fi + Mobilní data + Nastavit jako výchozí + Použít + Upravit + Profily + Nápověda + Kvality + Pozadí profilu + Zde můžete změnit pořadí zdrojů. Pokud má video vyšší prioritu, objeví se ve výběru zdrojů výše. Součet priority zdroje a priority kvality je priorita videa. +\n +\nZdroj A: 3 +\nKvalita B: 7 +\nBudou mít celkovou prioritu videa 10. +\n +\nPOZNÁMKA: Pokud je součet 10 nebo vyšší, přehrávač automaticky přeskočí načítání při načtení daného odkazu! + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e0a9594c..69a850b3 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -529,4 +529,21 @@ Rückgängig Abonniert ISP-Umgehungen - + Profil %d + Wi-Fi + Mobile Daten + Standard festlegen + Verwenden + Bearbeiten + Profile + Hilfe + Qualitäten + Profil-Hintergrund + Hier kannst du verändern, wie die Quellen geordnet werden. Wenn ein Video eine höhere Priorität hat, wird es höher in der Quellenauswahl erscheinen. Die Summe der Quellenpriorität und der Qualitätspriorität ist die Videopriorität. +\n +\nQuelle A: 3 +\nQualität B: 7 +\nWerden eine kombinierte Videopriorität von 10 haben. +\n +\nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird! + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d248044d..4326bccd 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -503,7 +503,7 @@ Seleccionar biblioteca Abrir con Tu biblioteca está vacía :( -\nRegístrate con una cuenta en la biblioteca o agrega los títulos a tu biblioteca local. +\nInicia sesión en una cuenta de biblioteca o añade series desde tu biblioteca local. ¡Se encontró un archivo en modo seguro! \nNo cargar ninguna extensión al inicio hasta que se elimine el archivo. Reproductor visible - buscar cantidad @@ -529,4 +529,21 @@ Revertir ISP Bypasses Calidad de visualización preferida (Datos móviles) - + Ayuda + Aquí puedes cambiar el orden de las fuentes. Si un vídeo tiene una prioridad más alta, aparecerá más arriba en la selección de las fuentes. La suma de la prioridad de la fuente y la prioridad de la calidad es la prioridad del vídeo. +\n +\nFuente A: 3 +\nCalidad B: 7 +\nTendrá una prioridad en el vídeo combinada de 10. +\n +\nNOTA: ¡Si la suma es 10 o más el reproductor saltará automáticamente la carga cuando se cargue ese enlace! + Perfil %d + Wifi + Editar + Perfiles + Datos móviles + Establecer por defecto + Usar + Calidades + Perfil del fondo + \ 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 a8c6a197..e519d062 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -552,4 +552,21 @@ Bypass ISP Pulihkan Nonton dengan kualitas yang di inginkan (Data Seluler) - + Data seluler + Bantuan + Di sini anda dapat mengubah sumber yang diurutkan. Jika video memiliki prioritas lebih tinggi, video tersebut akan muncul lebih tinggi dalam pemilihan sumber. Jumlah prioritas sumber dan prioritas kualitas adalah prioritas video. +\n +\nSumber A: 3 +\nKualitas B: 7 +\nAkan memiliki prioritas video yang digabung 10. +\n +\nCATATAN: Jika jumlahnya 10 atau lebih, pemain akan melewatkan pemuatan secara otomatis saat tautan dimuat! + Profil %d + Wifi + Pengaturan default + Gunakan + Edit + Profil + Kualitas + Latar belakang profil + \ 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 6dca2e3a..9a90b6e9 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -551,4 +551,21 @@ Aggiornando shows a cui sei iscritto L\'episodio %d è stato rilasciato! Qualità di visualizzazione preferita (Dati mobili) - + Qui puoi cambiare l\'ordine delle fonti. Se un video ha una priorità maggiore, apparirà più in alto nella selezione delle fonti. La somma tra la priorità della fonte e la priorità della qualità è la priorità video. +\n +\nFonte A: 3 +\nQualità B: 7 +\nAvranno una priorità video combinata di 10. +\n +\nNOTA: se la somma è pari o superiore a 10, il lettore salterà automaticamente il caricamento quando viene caricato quel link! + Profilo %d + Wi-Fi + Imposta predefinito + Usa + Modifica + Profili + Aiuto + Dati Mobili + Qualità + Sfondo profilo + \ 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 50e96c7c..e59cdd66 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -96,7 +96,7 @@ הורדת שפות שפת הכתוביות ייבא גופנים בהצבתם ב %s - להמשיך לצפות + המשך צפיה להסיר עוד מידע הספק הזה הוא טורנט, שימוש ב-VPN הוא מומלץ @@ -506,4 +506,4 @@ אלפביתי (ת\' עד א\') פתח עם נראה שהרשימה הזו ריקה, נסו לעבור לרשימה אחרת - + \ No newline at end of file diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 42eba3cc..7fd5504e 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -1,2 +1,40 @@ - + + Semua bahasa + Langkau %s + Pelayar web + Sejarah + Kosongkan sejarah + Pengenalan + Kredit + Pembukaan bercampur + Penamat + Pembukaan + Memasang kemaskini aplikasi… + Memuat turun kemaskini aplikasi… + Tidak + Ya + Adakah anda pasti anda ingin keluar\? + Keluarkan daripada ditonton + Tanda sebagai sudah ditonton + Tunjukkan pop timbul yang dilangkau untuk pembukaan/pengakhiran + Tidak dapat memasang versi aplikasi terkini + Terlalu banyak perkataan. Tidak dapat disimpan di papan klip. + Aplikasi tidak ditemui + Penamat bercampur + Imbas kembali + Episod %d akan disiarkan dalam + Pelakon:%s + Mod Selamat Hidup + %dd %dh %dm + %dh %dm + %dm + Poster Episod + Poster Utama + Pergi Kembali + Seterusnya Rawak + Tukar Pembekal + Kelajuan (%.2fx) + Poster + Poster + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index f56b0bfb..9054eada 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -551,4 +551,21 @@ \nWord lid van onze Discord of zoek online. Audiosporen Gesorteerd op - + Wifi + Mobiele data + Maak standaard + Aanpassen + Profielen + Help + Kwaliteiten + Profiel achtergrond + Gebruik + Hier kan je de volgorde van de bronnen veranderen. Als een video een hogere prioriteit heeft zal het hoger in de bronnenlijst staan. De som van de prioriteit van de bron en de prioriteit van de kwaliteit is de prioriteit van de video. +\n +\nBron A: 3 +\nKwaliteit B: 7 +\nHeeft een totale prioriteit van de video van 10. +\n +\nNOTITIE: Als de som 10 of hoger is zal de speler automatisch het laden overslaan wanneer die link is geladen! + Profiel %d + \ No newline at end of file diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index b3dda84f..135b7272 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -183,4 +183,17 @@ Varigheit Direktesendingar Programoppdateringar - + Trykk og hold inne for å nullstille til standardinnstillinger + Søk ved bruk av typer + Automatisk språkvalg + Last ned språk + Undertekstspråk + Metadata er ikke tilgjengelig fra nettsiden. Videoavspilling vil mislykkes hvis metadata ikke finnes på nettsiden. + Innstillinger for avspilling av undertekster + Plakat + Denne leverandøren er en torrent, og det anbefales å bruke en VPN-tjeneste. + For at denne leverandøren skal fungere korrekt, kan det være nødvendig å bruke en VPN-tjeneste + Bilde i bilde + Fortsett å sjå + Prøv tilkopling på nytt… + \ 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 92882faf..65b5a462 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 + Plakat Episode Plakat Main Plakat Neste tilfeldig @@ -104,7 +104,7 @@ Sveip for å søke Sveip til venstre eller høyre for å kontrollere tiden i videospilleren Sveip for å endre innstillinger - Sveip på venstre eller høyre side for å endre lysstyrke eller volum + Sveip opp eller ned på venstre eller høyre side for å endre lysstyrke eller volum Dobbelttrykk til å søke Trykk to ganger på høyre eller venstre for å søke fremover eller bakover Bruk systemets lysstyrke @@ -117,7 +117,7 @@ Sender ingen rapportere Vis filler-episode til anime Vis appoppdateringer - Søk automatisk etter nye oppdateringer + Søk automatisk etter nye oppdateringer etter at appen har startet. Oppdatering til forhåndsutgivelser Søk etter oppdateringer før utgivelse i stedet for fullstendige utgivelser Github @@ -186,7 +186,7 @@ Hoppe OP Ikke vis igjen Oppdater - Foretrukket klokkekvalitet + Foretrukket avspillingskvalitet (WiFi) DNS rundt HTTPS Nyttig til omgå Internettleverandør hinder Vis dubbet/subbed Anime @@ -296,7 +296,7 @@ Chromecast-undertekster Autospill neste episode Historikk - Trykk i midten for å sette på pause + Trykk to ganger midt på skjermen for å pause Fant ingen episoder Fyllstoff Spill direktestrøm @@ -323,7 +323,7 @@ Hovedrolle Støtterolle Neste - Referent + Referanse Programtillegg slettet Pakkebrønnsnavn Pakkebrønnsnettadresse @@ -364,7 +364,7 @@ programtillegg Trygt modus påslått Sett visningsstatus - Auto-synkroniser din nåværende episode-framdrift + Synkroniser automatisk fremdriften din for gjeldende episode %d-%d Offentlig liste programtillegg @@ -399,7 +399,7 @@ Rulletekst Introduksjon Lagringstilgang mangler. Prøv igjen. - Vis forfilmer + Vis trailere Dubbingsetikett Undertekstetikett Videomellomlagringsstørrelse @@ -407,11 +407,11 @@ Videohurtiglager på disk Tøm video- og bildehurtiglager Legg til en klone av en eksisterende side, med en annen nettadresse - Installer alle programtillegg som ikke er installert enda fra de pakkebrønnene som er lagt til. + Installer automatisk alle plugins som ikke er installert fra de tilføyde depotene. Kvalitetsetikett Slå av/på grensesnittselementer på plakat Hopp over denne oppdateringen - Forårsaker tilfeldige krasj hvis satt for høyt. Ikke endre dette hvis du ikke har lite minne. + Kan forårsake krasj hvis satt for høyt på enheter med lite minne, for eksempel en Android TV. @string/home_play Sikkerhetskopier data Data lagret @@ -425,13 +425,13 @@ @string/anime Skjul valgt videokvalitet i søkeresultater Lastet inn sikkerhetkopifil - Oppdateringer og sikkerhetskopi + Oppdateringer og sikkerhetskopier @string/ova Avslutt\? Sensurerbart Alle %s er allerede nedlastet Kunne ikke installere den nye versjonen av programmet - Installerer ny versjon av programmet … + Installerer appoppdatering… Laster ned ny versjon av programmet … Synkroniser %s innlogget @@ -449,7 +449,7 @@ TS TC Fjern døveteksting fra undertekster - Minimale undertekster + Fjern unødvendig informasjon fra undertekster Ekstra Filtrer etter foretrukket mediaspråk Vev-videosending @@ -488,7 +488,7 @@ Undertekster Sikkerhetskopi APK-installatør - Noen enheter støtter ikke den nye pakkeinstallatøren. Prøv gammeldags alternativ hvis ting ikke installeres. + Noen telefoner støtter ikke den nye pakkeinstallatøren. Prøv det eldre alternativet hvis oppdateringer ikke blir installert. Oppdatering startet Programtillegg nedlastet Programmet vil oppgraderes når du avslutter det @@ -511,7 +511,7 @@ Vist avspiller — blafringsmengde Oppdatert (gammel til ny) Vellykket - Episode %d sluppet. + Episode %d sluppet! Foretrukket visningskvalitet (mobildata) Stopp Fjern fra sette @@ -525,7 +525,7 @@ Abonnerer på %s Blafringsmengde med skjult avspiller Velg bibliotek - Omgår blokkering av GitHub ved bruk av jsDelivr. Kan utsette oppdateringer et par dager. + Omgår blokkering av GitHub ved hjelp av jsDelivr. Dette kan forårsake forsinkelser på oppdateringer med noen få dager. ISP-omgåelser Denne listen er tom. Prøv å bytte til en annen. Sorter @@ -533,4 +533,4 @@ \nLaster ikke inn noen utvidelser ved oppstart til filen er fjernet. Biblioteket ditt er tomt :( \nLogg inn på en bibliotekkonto eller legg til programmer i ditt lokale bibliotek. - + \ No newline at end of file diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index eaa76652..19b071e4 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -27,7 +27,7 @@ ଅଧ୍ୟାୟ ଚଲାଅ କୌଣସି ଅଧ୍ୟାୟ ମିଳିଲା ନାହିଁ ଟି ଅଧ୍ୟାୟ - ଟିଏ ଅଧ୍ୟାୟ + ଅଧ୍ୟାୟ %s‌ରେ ଚଲାଅ ବ୍ରାଉଜର୍‌ରେ ଚଲାଅ ଉପଶୀର୍ଷକ ଡାଉନଲୋଡ୍ କରିବା @@ -146,4 +146,11 @@ ଉପଶୀର୍ଷକ +୩୦ ଵର୍ଷ - + ଅନ୍ତଃ-ଷ୍ଟୋରେଜ୍ + ପ୍ରଦାତା ବଦଳାଅ + ପ୍ରଦାତା ଵ୍ୟଵହାର କରି ଖୋଜିବା + ଶୀଘ୍ର ଆସୁଅଛି… + ଆପ୍ ଅଦ୍ୟତନ ଦେଖାଇବା + ଅଦ୍ୟତନ ଆରମ୍ଭ ହୋଇଛି + ସନ୍ଧାନ କରିବା… + \ 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 2961cb47..d08a760d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -532,4 +532,21 @@ Obchodzi blokadę GitHuba za pomocą jsDelivr. może spowodować opóźnienie aktualizacji o kilka dni. Nie udało się połączyć z GitHub, włączono serwer pośredniczący jsDelivr… Domyślna jakość (dane mobilne) - + W tym miejscu można zmienić kolejność źródeł. Jeśli wideo ma wyższy priorytet, pojawi się wyżej w wyborze źródła. Priorytet wideo jest sumą priorytetu źródła i priorytetu jakości. +\n +\nŹródło A: 3 +\nJakość B: 7 +\nŁączny priorytet wideo będzie wynosił 10. +\n +\nUWAGA: Jeśli suma wynosi 10 lub więcej, odtwarzacz automatycznie pominie ładowanie po załadowaniu tego łącza! + Profil %d + Wi-Fi + Dane mobilne + Ustaw domyślny + Użyj + Edytuj + Profile + Pomoc + Jakości + Tło profilu + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 705285eb..d91e79f8 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -529,4 +529,21 @@ Configurações padrão SD Faixas de áudio - + Perfil %d + Wi-Fi + Dados móveis + Definir predefinição + Utilização + Editar + Perfis + Ajuda + Qualidades + Perfil de fundo + Aqui pode alterar a forma como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais elevada, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte com a prioridade da qualidade é a prioridade do vídeo. +\n +\nFonte A: 3 +\nQualidade B: 7 +\nTerá uma prioridade de vídeo combinada de 10. +\n +\nNOTA: Se a soma for 10 ou mais, o leitor saltará automaticamente o carregamento quando essa ligação for carregada! + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 170c3679..372552bc 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -551,13 +551,13 @@ Alfabetik (Z - A) Kütüphane Seçin Şununla aç - Görünüşe göre bu liste boş, başka bir listeye geçmeyi deneyin + Görünüşe göre bu liste boş, başka bir listeye geçmeyi deneyin. Derecelendirme (Yüksekten Düşüğe) Derecelendirme (Düşükten Yükseğe) Yeniden başlat Oynatıcı gizlenmişken atlanacak süre İSS Kısıtlamaları - GitHub\'a ulaşılamadı, jsdelivr vekil sunucusu etkinleştiriliyor. + GitHub\'a ulaşılamadı. jsDelivr vekil sunucusu etkinleştiriliyor… Başlat Başarılı oldu raw.githubusercontent.com vekil sunucusu (proxy) @@ -577,4 +577,21 @@ %s kanalı aboneliğinden çıkıldı Günlük Oynatıcı görünür durumdayken atlanacak süre miktarı - + Wi-Fi + Profiller + Yardım + Profil %d + Kullan + Mobil veri + Varsayılanı ayarla + Düzenle + Burada kaynakların nasıl sıralandığını değiştirebilirsiniz. Bir video daha yüksek bir önceliğe sahipse, kaynak seçiminde daha yüksek görünecektir. Kaynak önceliği ve kalite önceliğinin toplamı video önceliğidir. +\n +\nKaynak A: 3 +\nKalite B: 7 +\nBirleştirilmiş video önceliği 10 olacaktır. +\n +\nNOT: Toplam 10 veya daha fazlaysa, bu bağlantı yüklendiğinde oynatıcı otomatik olarak yüklemeyi atlayacaktır! + Kaliteler + Profil arkaplanı + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 82527c95..09fd40d5 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -529,4 +529,21 @@ Обходи ISP Обхід блокування GitHub за допомогою jsDelivr. Можлива затримка оновлень на кілька днів. Бажана якість перегляду (Мобільні дані) - + Встановити за замовчуванням + Профілі + Допомога + Тут ви можете змінити порядок джерел. Якщо відео має вищий пріоритет, воно з\'явиться вище у списку джерел. Сума пріоритету джерела та пріоритету якості є пріоритетом відео. +\n +\nДжерело A: 3 +\nЯкість B: 7 +\nСумарний пріоритет відео дорівнюватиме 10. +\n +\nПРИМІТКА: Якщо сума пріоритетів дорівнює 10 або більше, плеєр автоматично пропустить завантаження при завантаженні цього посилання! + Профіль %d + Wi-Fi + Мобільні дані + Використовувати + Редагувати + Якості + Фон профілю + \ No newline at end of file diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index df2e9a8b..4c9091dd 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -165,9 +165,9 @@ ڈیفالٹ سیٹنگز پر ری سیٹ کرنے کے لیے دبائیں اور تھامیں سائٹ کی طرف سے میٹا ڈیٹا فراہم نہیں کیا گیا ہے، اور اگر ویڈیو سائٹ میں موجود نہیں ہے تو وہ لوڈ ہونے میں ناکام ہو جائے گا. پلیئر کے ترجمہ کی ترتیبات - ویڈیو پلیئر میں وقت کو کنٹرول کرنے کے لیے بائیں یا دائیں سوائپ کریں + ویڈیو میں اپنی پوزیشن کو کنٹرول کرنے کے لیے ایک طرف سے دوسری طرف سوائپ کریں سکرین کی روشنی یا والیوم تبدیل کرنے کے لیے بائیں یا دائیں سوائپ کریں - کنٹرول کریں کہ پلیئر کتنا آگے ہے + کنٹرول کریں کہ پلیئر کتنا آگے ہے(سیکنڈوں میں) توقف کرنے کے لیے درمیان میں دبائیں سٹوریج کی اجازتیں غائب ہیں، براہ کرم دوبارہ کوشش کریں۔ کٹسو سے پوسٹر نمایش کریں @@ -263,7 +263,7 @@ دوبارہ نہ دکھائیں اس اپ ڈیٹ کو چھوڑ دیں اپ ڈیٹ - ترجیحی ویڈیو کا معیار + ترجیحی ویڈیو کا معیار(وائ فائ) ویڈیو پلیئر کے عنوان کے زیادہ سے زیادہ حروف ویڈیو پلیئر ریزولوشن ویڈیو بفر سائز @@ -356,4 +356,175 @@ %d / 10 اٹھایا اگر سب ٹائٹلز %d ms بہت جلد دکھائے جائیں تو اسے استعمال کریں - + کریڈٹس + اضافی + مرکزی + ترتیب + قرارداد اور عنوان + HDR + %s سے ان سبسکرائب کیا گیا + ویب ویڈیو کاسٹ + 18+ + لاگ + سلسلہ سے لنک کریں + اینڈرائیڈ ٹی وی + پچھلا + ٹریلر + حادثے کی معلومات دیکھیں + باہر نکلنے پر ایپ کو اپ ڈیٹ کر دیا جائے گا + اس ذخیرہ سے تمام پلگ ان ڈاؤن لوڈ کریں؟ + قسط %d ریلیز ہو گئی! + ایپ کا نیا ورژن انسٹال نہیں ہو سکا + تجویز کردہ + دوبارہ شروع کریں + لوڈ %s + انٹرنیٹ سے لوڈ کریں + ڈاؤن لوڈڈ فائل + پس منظر + ذریعہ + کیم + کیم + کیم + ایچ ڈی + ٹی ایس + ٹی سی + ڈبلیو پی + 4کے + SDR + پلیئر + عنوان + قرارداد + غلط ڈیٹا + خرابی + سب ٹائٹلز سے بلوٹ کو ہٹا دیں + ترجیحی میڈیا زبان کے لحاظ سے فلٹر کریں + حوالہ دینے والا + ان زبانوں میں ویڈیوز دیکھیں + سیٹ اپ کو چھوڑ دیں + کریش رپورٹنگ + ہو گیا + ایکسٹینشنز + ذخیرہ URL + پلگ ان لوڈ ہو گیا + %s کو لوڈ نہیں کیا جا سکا + ڈاؤن لوڈ ہونا شروع ہو گیا %d %s… + ڈاؤن لوڈ کیا گیا %d %s + سبھی %s پہلے ہی ڈاؤن لوڈ ہو چکے ہیں + رابطہ بحال کرو + پلگ ان + ذخیرہ کو حذف کریں + ان سائٹس کی فہرست ڈاؤن لوڈ کریں جنہیں آپ استعمال کرنا چاہتے ہیں + ڈاؤن لوڈڈ: %d + غیر فعال: %d + اپ ڈیٹ کردہ %d پلگ ان + تمام سب ٹائٹلز کو بڑے کریں + %s (غیر فعال) + ٹریکس + آڈیو ٹریکس + ری اسٹارٹ پر اپلائی کریں + سیف موڈ آن + درجہ بندی: %s + تفصیل + ورژن + حالت + سائز + زبان + وی ایل سی + ترجیحی ویڈیو پلیئر + اندرونی پلیئر + ایم پی وی + ویب براؤزر + ایپ نہیں ملی + Recap + مخلوط اختتام + مخلوط افتتاحی + کھولنے/ختم کرنے کے لیے سکپ پاپ اپ دکھائیں + دیکھا گیا کے طور پر نشان زد کریں + نہیں + ایپ اپ ڈیٹ ڈاؤن لوڈ ہو رہا ہے… + میراث + اپ ڈیٹ شروع ہو گیا + پلگ ان ڈاؤن لوڈ ہو گیا + دیکھے گئے سے ہٹا دیں + پیکیج انسٹالر + درجہ بندی (اعلی سے کم) + درجہ بندی (کم سے زیادہ) + اپ ڈیٹ کیا گیا (پرانا سے نیا) + حروف تہجی (A سے Z) + حروف تہجی (Z سے A) + لائبریری کو منتخب کریں + پلیئر دکھایا گیا - مقدار تلاش کریں + پلیئر کے نظر آنے پر استعمال کی جانے والی مقدار + پلیئر کے چھپنے پر استعمال ہونے والی تلاش کی مقدار + سیف موڈ فائل مل گئی! +\nفائل کو ہٹانے تک اسٹارٹ اپ پر کوئی ایکسٹینشن لوڈ نہیں کرنا۔ + شروع کریں + ناکام + کامیاب ہو گیا + فراہم کنندہ ٹیسٹ + رک جاؤ + سبسکرائب شدہ شوز کو اپ ڈیٹ کرنا + دیکھنے کا ترجیحی معیار (موبائل ڈیٹا) + raw.githubusercontent.com پراکسی + آئی ایس پی بائی پاسز + %s کو سبسکرائب کیا + jsDelivr کا استعمال کرتے ہوئے GitHub کو بلاک کرنے کو نظرانداز کرتا ہے۔ اپ ڈیٹس میں کچھ دنوں کی تاخیر ہو سکتی ہے۔ + آپ کی لائبریری خالی ہے:( +\nلائبریری اکاؤنٹ میں لاگ ان کریں یا اپنی مقامی لائبریری میں شوز شامل کریں۔ + غلط URL + براؤزر + ویب + مددگار + ایچ کیو + حمایت کی + ہاں + سبسکرائب کیا ہوا + تازہ کاری (نئے سے پرانے) + ڈی وی ڈی + ماضی مٹا دو + تعارف + تاریخ + کے ساتھ کھولیں + غلط ID + مخزن کا نام + ختم ہونے والا + کھل رہا + کیا آپ کو یقین ہے کہ آپ ہیاں سے نکلنا چاہتے ہیں؟ + ایس ڈی + تمام زبانیں + پلیئر پوشیدہ - مقدار تلاش کریں + لائبریری + بلو رے + واپس لوٹنا + ذخیرہ شامل کریں + آپ کیا دیکھنا چاہتے ہیں + ‪کمیونٹی ریپوزٹریز دیکھیں + ڈاؤن لوڈ نہیں ہوا: %d + عوامی فہرست + فائل سے لوڈ کریں + ویڈیو ٹریکس + HLS پلے لسٹ + پوسٹر کی تصویر + UHD + %s کو چھوڑ دیں + بے ترتیب + ترتیب دیں + مصنفین + اس سے تمام ریپوزٹری پلگ ان بھی حذف ہو جائیں گے + پلگ ان کو حذف کر دیا گیا + بیچ ڈاؤن لوڈ + GitHub تک نہیں پہنچ سکا۔ jsDelivr پراکسی کو آن کیا جا رہا ہے… + تیز بھوری لومڑی سست کتے پر چھلانگ لگاتی ہے + جلد آرہا ہے… + سب ٹائٹلز سے بند کیپشنز کو ہٹا دیں + اپنے آلے کے مطابق ایپ کی شکل تبدیل کریں + اگلے + CloudStream میں بذریعہ ڈیفالٹ کوئی سائٹ انسٹال نہیں ہے۔ آپ کو ریپوزٹری سے سائٹس انسٹال کرنے کی ضرورت ہے۔ +\nSky UK Limited 🤮 کی طرف سے بے دماغ DMCA ہٹانے کی وجہ سے ہم ایپ میں ریپوزٹری سائٹ کو لنک نہیں کر سکتے۔ +\nہمارے ڈسکارڈ میں شامل ہوں یا آن لائن تلاش کریں۔ + تمام ایکسٹینشنز کریش کی وجہ سے آف کر دی گئیں تاکہ آپ کو پریشانی کا باعث تلاش کرنے میں مدد مل سکے۔ + پہلے ایکسٹینشن انسٹال کریں + بہت زیادہ متن۔ کلپ بورڈ میں محفوظ کرنے سے قاصر۔ + ایپ اپ ڈیٹ انسٹال ہو رہا ہے… + یہ فہرست خالی ہے۔ کسی اور پر سوئچ کرنے کی کوشش کریں۔ + \ No newline at end of file From 9237817bd3ab3c48548e6ee69152cdb8c35ac8a6 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 06:36:01 +0000 Subject: [PATCH 093/570] chore(locales): fix locale issues --- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-iw/strings.xml | 2 +- app/src/main/res/values-ms/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-nn/strings.xml | 2 +- app/src/main/res/values-no/strings.xml | 2 +- app/src/main/res/values-or/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-ur/strings.xml | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 6b722e43..c423b239 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -578,4 +578,4 @@ \nملاحظة: إذا كان المجموع 10 أو أكثر ، فسيتخطى المشغل التحميل تلقائيًا عند تحميل هذا الرابط! النوعيات خلفية الملف الشخصي - \ No newline at end of file + diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index cc272a3a..ba754fa5 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -149,4 +149,4 @@ আইজেনগ্রাভি মোড আপডেট শুরু হয়েছে ব্রাউজার - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 5cbdb7db..cc9f0ad4 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -570,4 +570,4 @@ \nBudou mít celkovou prioritu videa 10. \n \nPOZNÁMKA: Pokud je součet 10 nebo vyšší, přehrávač automaticky přeskočí načítání při načtení daného odkazu! - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 69a850b3..45a6a66c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -546,4 +546,4 @@ \nWerden eine kombinierte Videopriorität von 10 haben. \n \nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird! - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4326bccd..bc830084 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -546,4 +546,4 @@ Usar Calidades Perfil del fondo - \ 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 e519d062..722352a6 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -569,4 +569,4 @@ Profil Kualitas Latar belakang profil - \ 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 9a90b6e9..431b2a8c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -568,4 +568,4 @@ Dati Mobili Qualità Sfondo profilo - \ 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 e59cdd66..aaa65897 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -506,4 +506,4 @@ אלפביתי (ת\' עד א\') פתח עם נראה שהרשימה הזו ריקה, נסו לעבור לרשימה אחרת - \ No newline at end of file + diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 7fd5504e..17eeb883 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -37,4 +37,4 @@ Kelajuan (%.2fx) Poster Poster - \ No newline at end of file + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 9054eada..dff95be7 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -568,4 +568,4 @@ \n \nNOTITIE: Als de som 10 of hoger is zal de speler automatisch het laden overslaan wanneer die link is geladen! Profiel %d - \ No newline at end of file + diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 135b7272..592ff22c 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -196,4 +196,4 @@ Bilde i bilde Fortsett å sjå Prøv tilkopling på nytt… - \ 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 65b5a462..dac15d61 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -533,4 +533,4 @@ \nLaster ikke inn noen utvidelser ved oppstart til filen er fjernet. Biblioteket ditt er tomt :( \nLogg inn på en bibliotekkonto eller legg til programmer i ditt lokale bibliotek. - \ No newline at end of file + diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 19b071e4..9b9385c2 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -153,4 +153,4 @@ ଆପ୍ ଅଦ୍ୟତନ ଦେଖାଇବା ଅଦ୍ୟତନ ଆରମ୍ଭ ହୋଇଛି ସନ୍ଧାନ କରିବା… - \ 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 d08a760d..7d4bf3e3 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -549,4 +549,4 @@ Pomoc Jakości Tło profilu - \ No newline at end of file + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index d91e79f8..773598bf 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -546,4 +546,4 @@ \nTerá uma prioridade de vídeo combinada de 10. \n \nNOTA: Se a soma for 10 ou mais, o leitor saltará automaticamente o carregamento quando essa ligação for carregada! - \ No newline at end of file + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 372552bc..eef9bc95 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -594,4 +594,4 @@ \nNOT: Toplam 10 veya daha fazlaysa, bu bağlantı yüklendiğinde oynatıcı otomatik olarak yüklemeyi atlayacaktır! Kaliteler Profil arkaplanı - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 09fd40d5..0fad744c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -546,4 +546,4 @@ Редагувати Якості Фон профілю - \ No newline at end of file + diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 4c9091dd..48b73efa 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -527,4 +527,4 @@ بہت زیادہ متن۔ کلپ بورڈ میں محفوظ کرنے سے قاصر۔ ایپ اپ ڈیٹ انسٹال ہو رہا ہے… یہ فہرست خالی ہے۔ کسی اور پر سوئچ کرنے کی کوشش کریں۔ - \ No newline at end of file + From 927453d9fe0bb370ae811ea6df10f385eb0862b6 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Wed, 12 Jul 2023 23:15:25 +0700 Subject: [PATCH 094/570] Extractor: added Moviesapi and fix some extractors (#504) * Extractor: added Pixeldrain, Wibufile and fix some extractors * Extractor: added Moviesapi and fix some extractors --------- Co-authored-by: Sofie99 --- .../cloudstream3/extractors/Chillx.kt | 9 ++++-- .../cloudstream3/extractors/Filesim.kt | 32 +++++++------------ .../cloudstream3/utils/ExtractorApi.kt | 1 + 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index 1c548e74..b4f3d897 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -12,6 +12,11 @@ import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.SecretKeySpec +class Moviesapi : Chillx() { + override val name = "Moviesapi" + override val mainUrl = "https://w1.moviesapi.club" +} + class Bestx : Chillx() { override val name = "Bestx" override val mainUrl = "https://bestx.stream" @@ -27,7 +32,7 @@ open class Chillx : ExtractorApi() { override val requiresReferer = true companion object { - private const val KEY = "4VqE3#N7zt&HEP^a" + private const val KEY = "11x&W5UBrcqn\$9Yl" } override suspend fun getUrl( @@ -45,7 +50,7 @@ open class Chillx : ExtractorApi() { val encData = AppUtils.tryParseJson(base64Decode(master ?: return)) val decrypt = cryptoAESHandler(encData ?: return, KEY, false) - val source = Regex("""sources:\s*\[\{"file":"([^"]+)""").find(decrypt)?.groupValues?.get(1) + val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1) // required 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 be0efd0c..a1148bb8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt @@ -65,27 +65,19 @@ open class Filesim : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val response = app.get(url, referer = referer).document - response.select("script[type=text/javascript]").map { script -> - if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) { - val unpackedscript = getAndUnpack(script.data()) - val m3u8Regex = Regex("file.\"(.*?m3u8.*?)\"") - val m3u8 = m3u8Regex.find(unpackedscript)?.destructured?.component1() ?: "" - if (m3u8.isNotEmpty()) { - generateM3u8( - name, - m3u8, - mainUrl - ).forEach(callback) - } - } + val response = app.get(url, referer = referer) + val script = if (!getPacked(response.text).isNullOrEmpty()) { + getAndUnpack(response.text) + } else { + response.document.selectFirst("script:containsData(sources:)")?.data() } + val m3u8 = + Regex("file:\\s*\"(.*?m3u8.*?)\"").find(script ?: return)?.groupValues?.getOrNull(1) + generateM3u8( + name, + m3u8 ?: return, + mainUrl + ).forEach(callback) } - /* private data class ResponseSource( - @JsonProperty("file") val file: String, - @JsonProperty("type") val type: String?, - @JsonProperty("label") val label: String? - ) */ - } \ No newline at end of file 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 f0c1ea3b..f6f76fe7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -357,6 +357,7 @@ val extractorApis: MutableList = arrayListOf( DesuDrive(), Chillx(), + Moviesapi(), Watchx(), Bestx(), Keephealth(), From 05a0d3cd817c5484a44acd9bb6e8199e52ced387 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 13 Jul 2023 23:18:37 +0200 Subject: [PATCH 095/570] migrated some items to viewbindings + removed Some --- app/build.gradle.kts | 5 + .../cloudstream3/mvvm/ArchComponentExt.kt | 26 -- .../ui/download/DownloadChildFragment.kt | 44 ++- .../ui/download/DownloadFragment.kt | 98 +++--- .../cloudstream3/ui/home/HomeFragment.kt | 325 ++++++++++-------- .../ui/home/HomeParentItemAdapterPreview.kt | 4 +- .../ui/library/LibraryFragment.kt | 198 ++++++----- .../ui/library/LibraryViewModel.kt | 1 - .../cloudstream3/ui/library/PageAdapter.kt | 30 +- .../ui/library/ViewpagerAdapter.kt | 70 ++-- .../ui/quicksearch/QuickSearchFragment.kt | 65 ++-- .../cloudstream3/ui/result/ActorAdaptor.kt | 77 ++--- .../cloudstream3/ui/result/EpisodeAdapter.kt | 296 ++++++++++------ .../cloudstream3/ui/result/ImageAdapter.kt | 14 +- .../cloudstream3/ui/result/ResultFragment.kt | 133 +++---- .../ui/result/ResultFragmentPhone.kt | 106 +++--- .../ui/result/ResultFragmentTv.kt | 77 ++--- .../ui/result/ResultViewModel2.kt | 194 ++++++----- .../cloudstream3/ui/result/SelectAdaptor.kt | 13 +- .../cloudstream3/ui/result/UiText.kt | 9 - .../cloudstream3/ui/search/SearchAdaptor.kt | 2 +- .../cloudstream3/ui/search/SearchFragment.kt | 128 ++++--- .../ui/search/SearchHistoryAdaptor.kt | 30 +- .../ui/search/SearchResultBuilder.kt | 20 +- .../ui/settings/AccountAdapter.kt | 22 +- .../ui/settings/SettingsAccount.kt | 2 +- .../ui/settings/SettingsFragment.kt | 4 +- .../settings/extensions/ExtensionsFragment.kt | 42 ++- .../extensions/ExtensionsViewModel.kt | 7 +- .../ui/settings/extensions/PluginsFragment.kt | 74 ++-- .../ui/setup/SetupFragmentExtensions.kt | 79 +++-- .../ui/setup/SetupFragmentLanguage.kt | 53 +-- .../ui/setup/SetupFragmentLayout.kt | 101 +++--- .../ui/setup/SetupFragmentMedia.kt | 89 +++-- .../ui/setup/SetupFragmentProviderLanguage.kt | 50 ++- .../subtitles/ChromecastSubtitlesFragment.kt | 90 +++-- .../ui/subtitles/SubtitlesFragment.kt | 2 +- .../lagradost/cloudstream3/utils/UIHelper.kt | 13 +- app/src/main/res/layout/fragment_home_tv.xml | 11 + app/src/main/res/layout/fragment_plugins.xml | 2 +- app/src/main/res/layout/fragment_search.xml | 2 +- .../main/res/layout/fragment_search_tv.xml | 2 +- .../main/res/layout/home_select_mainpage.xml | 2 +- .../res/layout/library_viewpager_page.xml | 2 - .../main/res/layout/result_episode_large.xml | 213 ++++++------ .../main/res/layout/tvtypes_chips_scroll.xml | 2 +- 46 files changed, 1565 insertions(+), 1264 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ebde6187..86d91147 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,6 +28,11 @@ android { testOptions { unitTests.isReturnDefaultValues = true } + + viewBinding { + enable = true + } + signingConfigs { create("prerelease") { if (prereleaseStoreFile != null) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index bb15bc85..28b552d1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -57,32 +57,6 @@ fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> liveData.observe(this) { action(it) } } -inline fun some(value: T?): Some { - return if (value == null) { - Some.None - } else { - Some.Success(value) - } -} - -sealed class Some { - data class Success(val value: T) : Some() - object None : Some() - - override fun toString(): String { - return when (this) { - is None -> "None" - is Success -> "Some(${value.toString()})" - } - } -} - -sealed class ResourceSome { - data class Success(val value: T) : ResourceSome() - object None : ResourceSome() - data class Loading(val data: Any? = null) : ResourceSome() -} - sealed class Resource { data class Success(val value: T) : Resource() data class Failure( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index 477a18e0..0cef49b1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey @@ -15,13 +16,12 @@ import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager -import kotlinx.android.synthetic.main.fragment_child_downloads.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class DownloadChildFragment : Fragment() { companion object { - fun newInstance(headerName: String, folder: String) : Bundle { + fun newInstance(headerName: String, folder: String): Bundle { return Bundle().apply { putString("folder", folder) putString("name", headerName) @@ -30,13 +30,21 @@ class DownloadChildFragment : Fragment() { } override fun onDestroyView() { - (download_child_list?.adapter as DownloadChildAdapter?)?.killAdapter() + (binding?.downloadChildList?.adapter as DownloadChildAdapter?)?.killAdapter() downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it } + binding = null super.onDestroyView() } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_child_downloads, container, false) + var binding: FragmentChildDownloadsBinding? = null + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root//inflater.inflate(R.layout.fragment_child_downloads, container, false) } private fun updateList(folder: String) = main { @@ -50,14 +58,15 @@ class DownloadChildFragment : Fragment() { ?: return@mapNotNull null VisualDownloadChildCached(info.fileLength, info.totalBytes, it) } - }.sortedBy { it.data.episode + (it.data.season?: 0)*100000 } + }.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 } if (eps.isEmpty()) { activity?.onBackPressed() return@main } - (download_child_list?.adapter as DownloadChildAdapter? ?: return@main).cardList = eps - download_child_list?.adapter?.notifyDataSetChanged() + (binding?.downloadChildList?.adapter as DownloadChildAdapter? ?: return@main).cardList = + eps + binding?.downloadChildList?.adapter?.notifyDataSetChanged() } } @@ -72,14 +81,17 @@ class DownloadChildFragment : Fragment() { activity?.onBackPressed() // TODO FIX return } - context?.fixPaddingStatusbar(download_child_root) + fixPaddingStatusbar(binding?.downloadChildRoot) - download_child_toolbar.title = name - download_child_toolbar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - download_child_toolbar.setNavigationOnClickListener { - activity?.onBackPressed() + binding?.downloadChildToolbar?.apply { + title = name + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressed() + } } + val adapter: RecyclerView.Adapter = DownloadChildAdapter( ArrayList(), @@ -88,7 +100,7 @@ class DownloadChildFragment : Fragment() { } downloadDeleteEventListener = { id: Int -> - val list = (download_child_list?.adapter as DownloadChildAdapter?)?.cardList + val list = (binding?.downloadChildList?.adapter as DownloadChildAdapter?)?.cardList if (list != null) { if (list.any { it.data.id == id }) { updateList(folder) @@ -98,8 +110,8 @@ class DownloadChildFragment : Fragment() { downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } - download_child_list.adapter = adapter - download_child_list.layoutManager = GridLayoutManager(context, 1) + binding?.downloadChildList?.adapter = adapter + binding?.downloadChildList?.layoutManager = GridLayoutManager(context, 1) updateList(folder) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index e80a8fa5..629ab11a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -34,10 +34,10 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager -import kotlinx.android.synthetic.main.fragment_downloads.* -import kotlinx.android.synthetic.main.stream_input.* import android.text.format.Formatter.formatShortFileSize import androidx.core.widget.doOnTextChanged +import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding +import com.lagradost.cloudstream3.databinding.StreamInputBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.player.BasicLink import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings @@ -60,8 +60,8 @@ class DownloadFragment : Fragment() { private fun setList(list: List) { main { - (download_list?.adapter as DownloadHeaderAdapter?)?.cardList = list - download_list?.adapter?.notifyDataSetChanged() + (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list + binding?.downloadList?.adapter?.notifyDataSetChanged() } } @@ -70,10 +70,13 @@ class DownloadFragment : Fragment() { VideoDownloadManager.downloadDeleteEvent -= downloadDeleteEventListener!! downloadDeleteEventListener = null } - (download_list?.adapter as DownloadHeaderAdapter?)?.killAdapter() + (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.killAdapter() + binding = null super.onDestroyView() } + var binding : FragmentDownloadsBinding? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -82,7 +85,9 @@ class DownloadFragment : Fragment() { downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] - return inflater.inflate(R.layout.fragment_downloads, container, false) + val localBinding = FragmentDownloadsBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root//inflater.inflate(R.layout.fragment_downloads, container, false) } private var downloadDeleteEventListener: ((Int) -> Unit)? = null @@ -92,36 +97,40 @@ class DownloadFragment : Fragment() { hideKeyboard() observe(downloadsViewModel.noDownloadsText) { - text_no_downloads.text = it + binding?.textNoDownloads?.text = it } observe(downloadsViewModel.headerCards) { setList(it) - download_loading.isVisible = false + binding?.downloadLoading?.isVisible = false } observe(downloadsViewModel.availableBytes) { - download_free_txt?.text = + binding?.downloadFreeTxt?.text = getString(R.string.storage_size_format).format( getString(R.string.free_storage), formatShortFileSize(view.context, it) ) - download_free?.setLayoutWidth(it) + binding?.downloadFree?.setLayoutWidth(it) } observe(downloadsViewModel.usedBytes) { - download_used_txt?.text = - getString(R.string.storage_size_format).format( - getString(R.string.used_storage), - formatShortFileSize(view.context, it) - ) - download_used?.setLayoutWidth(it) - download_storage_appbar?.isVisible = it > 0 + binding?.apply { + downloadUsedTxt.text = + getString(R.string.storage_size_format).format( + getString(R.string.used_storage), + formatShortFileSize(view.context, it) + ) + downloadUsed.setLayoutWidth(it) + downloadStorageAppbar.isVisible = it > 0 + } } observe(downloadsViewModel.downloadBytes) { - download_app_txt?.text = - getString(R.string.storage_size_format).format( - getString(R.string.app_storage), - formatShortFileSize(view.context, it) - ) - download_app?.setLayoutWidth(it) + binding?.apply { + downloadAppTxt.text = + getString(R.string.storage_size_format).format( + getString(R.string.app_storage), + formatShortFileSize(view.context, it) + ) + downloadApp.setLayoutWidth(it) + } } val adapter: RecyclerView.Adapter = @@ -164,7 +173,7 @@ class DownloadFragment : Fragment() { ) downloadDeleteEventListener = { id -> - val list = (download_list?.adapter as DownloadHeaderAdapter?)?.cardList + val list = (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList if (list != null) { if (list.any { it.data.id == id }) { context?.let { ctx -> @@ -177,31 +186,36 @@ class DownloadFragment : Fragment() { downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } - download_list?.adapter = adapter - download_list?.layoutManager = GridLayoutManager(context, 1) + binding?.downloadList?.apply { + this.adapter = adapter + layoutManager = GridLayoutManager(context, 1) + } // Should be visible in emulator layout - download_stream_button?.isGone = isTrueTvSettings() - download_stream_button?.setOnClickListener { + binding?.downloadStreamButton?.isGone = isTrueTvSettings() + binding?.downloadStreamButton?.setOnClickListener { val dialog = Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom) - dialog.setContentView(R.layout.stream_input) + + val binding = StreamInputBinding.inflate(dialog.layoutInflater) + + dialog.setContentView(binding.root) dialog.show() // If user has clicked the switch do not interfere var preventAutoSwitching = false - dialog.hls_switch?.setOnClickListener { + binding.hlsSwitch.setOnClickListener { preventAutoSwitching = true } fun activateSwitchOnHls(text: String?) { - dialog.hls_switch?.isChecked = normalSafeApiCall { + binding.hlsSwitch.isChecked = normalSafeApiCall { URI(text).path?.substringAfterLast(".")?.contains("m3u") } == true } - dialog.stream_referer?.doOnTextChanged { text, _, _, _ -> + binding.streamReferer.doOnTextChanged { text, _, _, _ -> if (!preventAutoSwitching) activateSwitchOnHls(text?.toString()) } @@ -210,16 +224,16 @@ class DownloadFragment : Fragment() { 0 )?.text?.toString()?.let { copy -> val fixedText = copy.trim() - dialog.stream_url?.setText(fixedText) + binding.streamUrl.setText(fixedText) activateSwitchOnHls(fixedText) } - dialog.apply_btt?.setOnClickListener { - val url = dialog.stream_url.text?.toString() + binding.applyBtt.setOnClickListener { + val url = binding.streamUrl.text?.toString() if (url.isNullOrEmpty()) { showToast(activity, R.string.error_invalid_url, Toast.LENGTH_SHORT) } else { - val referer = dialog.stream_referer.text?.toString() + val referer = binding.streamReferer.text?.toString() activity?.navigate( R.id.global_to_navigation_player, @@ -228,7 +242,7 @@ class DownloadFragment : Fragment() { listOf(BasicLink(url)), extract = true, referer = referer, - isM3u8 = dialog.hls_switch?.isChecked + isM3u8 = binding.hlsSwitch.isChecked ) ) ) @@ -237,22 +251,22 @@ class DownloadFragment : Fragment() { } } - dialog.cancel_btt?.setOnClickListener { + binding.cancelBtt.setOnClickListener { dialog.dismissSafe(activity) } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - download_list?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> + binding?.downloadList?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> val dy = scrollY - oldScrollY if (dy > 0) { //check for scroll down - download_stream_button?.shrink() // hide + binding?.downloadStreamButton?.shrink() // hide } else if (dy < -5) { - download_stream_button?.extend() // show + binding?.downloadStreamButton?.extend() // show } } } downloadsViewModel.updateList(requireContext()) - context?.fixPaddingStatusbar(download_root) + fixPaddingStatusbar(binding?.downloadRoot) } } \ No newline at end of file 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 5cf6fc8e..99ce7c3b 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 @@ -34,12 +34,15 @@ 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.databinding.FragmentHomeBinding +import com.lagradost.cloudstream3.databinding.HomeEpisodesExpandedBinding +import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding +import com.lagradost.cloudstream3.databinding.TvtypesChipsBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi -import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.search.* @@ -64,24 +67,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API -import kotlinx.android.synthetic.main.activity_main_tv.* -import kotlinx.android.synthetic.main.fragment_home.* -import kotlinx.android.synthetic.main.fragment_home.home_api_fab -import kotlinx.android.synthetic.main.fragment_home.home_change_api_loading -import kotlinx.android.synthetic.main.fragment_home.home_loading -import kotlinx.android.synthetic.main.fragment_home.home_loading_error -import kotlinx.android.synthetic.main.fragment_home.home_loading_shimmer -import kotlinx.android.synthetic.main.fragment_home.home_loading_statusbar -import kotlinx.android.synthetic.main.fragment_home.home_master_recycler -import kotlinx.android.synthetic.main.fragment_home.home_reload_connection_open_in_browser -import kotlinx.android.synthetic.main.fragment_home.home_reload_connectionerror -import kotlinx.android.synthetic.main.fragment_home.result_error_text -import kotlinx.android.synthetic.main.fragment_home_tv.* -import kotlinx.android.synthetic.main.fragment_result.* -import kotlinx.android.synthetic.main.fragment_search.* -import kotlinx.android.synthetic.main.home_episodes_expanded.* -import kotlinx.android.synthetic.main.tvtypes_chips.* -import kotlinx.android.synthetic.main.tvtypes_chips.view.* + import java.util.* @@ -125,22 +111,26 @@ class HomeFragment : Fragment() { expand: HomeViewModel.ExpandableHomepageList, deleteCallback: (() -> Unit)? = null, expandCallback: (suspend (String) -> HomeViewModel.ExpandableHomepageList?)? = null, - dismissCallback : (() -> Unit), + dismissCallback: (() -> Unit), ): BottomSheetDialog { val context = this val bottomSheetDialogBuilder = BottomSheetDialog(context) - - bottomSheetDialogBuilder.setContentView(R.layout.home_episodes_expanded) - val title = bottomSheetDialogBuilder.findViewById(R.id.home_expanded_text)!! + val binding: HomeEpisodesExpandedBinding = HomeEpisodesExpandedBinding.inflate( + bottomSheetDialogBuilder.layoutInflater, + null, + false + ) + bottomSheetDialogBuilder.setContentView(binding.root) + //val title = bottomSheetDialogBuilder.findViewById(R.id.home_expanded_text)!! //title.findViewTreeLifecycleOwner().lifecycle.addObserver() val item = expand.list - title.text = item.name - val recycle = - bottomSheetDialogBuilder.findViewById(R.id.home_expanded_recycler)!! - val titleHolder = - bottomSheetDialogBuilder.findViewById(R.id.home_expanded_drag_down)!! + binding.homeExpandedText.text = item.name + // val recycle = + // bottomSheetDialogBuilder.findViewById(R.id.home_expanded_recycler)!! + //val titleHolder = + // bottomSheetDialogBuilder.findViewById(R.id.home_expanded_drag_down)!! // main { //(bottomSheetDialogBuilder.ownerActivity as androidx.fragment.app.FragmentActivity?)?.supportFragmentManager?.fragments?.lastOrNull()?.viewLifecycleOwner?.apply { @@ -159,10 +149,10 @@ class HomeFragment : Fragment() { // }) //} // } - val delete = bottomSheetDialogBuilder.home_expanded_delete - delete.isGone = deleteCallback == null + //val delete = bottomSheetDialogBuilder.home_expanded_delete + binding.homeExpandedDelete.isGone = deleteCallback == null if (deleteCallback != null) { - delete.setOnClickListener { + binding.homeExpandedDelete.setOnClickListener { try { val builder: AlertDialog.Builder = AlertDialog.Builder(context) val dialogClickListener = @@ -172,6 +162,7 @@ class HomeFragment : Fragment() { deleteCallback.invoke() bottomSheetDialogBuilder.dismissSafe(this) } + DialogInterface.BUTTON_NEGATIVE -> {} } } @@ -191,26 +182,27 @@ class HomeFragment : Fragment() { } } } - - titleHolder.setOnClickListener { + binding.homeExpandedDragDown.setOnClickListener { bottomSheetDialogBuilder.dismissSafe(this) } // Span settings - recycle.spanCount = currentSpan + binding.homeExpandedRecycler.spanCount = currentSpan - recycle.adapter = SearchAdapter(item.list.toMutableList(), recycle) { callback -> - handleSearchClickCallback(this, callback) - if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) { - bottomSheetDialogBuilder.ownHide() // we hide here because we want to resume it later - //bottomSheetDialogBuilder.dismissSafe(this) + binding.homeExpandedRecycler.adapter = + SearchAdapter(item.list.toMutableList(), binding.homeExpandedRecycler) { callback -> + handleSearchClickCallback(this, callback) + if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) { + bottomSheetDialogBuilder.ownHide() // we hide here because we want to resume it later + //bottomSheetDialogBuilder.dismissSafe(this) + } + }.apply { + hasNext = expand.hasNext } - }.apply { - hasNext = expand.hasNext - } - recycle.addOnScrollListener(object : RecyclerView.OnScrollListener() { + binding.homeExpandedRecycler.addOnScrollListener(object : + RecyclerView.OnScrollListener() { var expandCount = 0 val name = expand.list.name @@ -238,7 +230,7 @@ class HomeFragment : Fragment() { }) val spanListener = { span: Int -> - recycle.spanCount = span + binding.homeExpandedRecycler.spanCount = span //(recycle.adapter as SearchAdapter).notifyDataSetChanged() } @@ -280,19 +272,19 @@ class HomeFragment : Fragment() { ) } - private fun getPairList(header: ChipGroup) = getPairList( - header.home_select_anime, - header.home_select_cartoons, - header.home_select_tv_series, - header.home_select_documentaries, - header.home_select_movies, - header.home_select_asian, - header.home_select_livestreams, - header.home_select_nsfw, - header.home_select_others + private fun getPairList(header: TvtypesChipsBinding) = getPairList( + header.homeSelectAnime, + header.homeSelectCartoons, + header.homeSelectTvSeries, + header.homeSelectDocumentaries, + header.homeSelectMovies, + header.homeSelectAsian, + header.homeSelectLivestreams, + header.homeSelectNsfw, + header.homeSelectOthers ) - fun validateChips(header: ChipGroup?, validTypes: List) { + fun validateChips(header: TvtypesChipsBinding?, validTypes: List) { if (header == null) return val pairList = getPairList(header) for ((button, types) in pairList) { @@ -301,7 +293,7 @@ class HomeFragment : Fragment() { } } - fun updateChips(header: ChipGroup?, selectedTypes: List) { + fun updateChips(header: TvtypesChipsBinding?, selectedTypes: List) { if (header == null) return val pairList = getPairList(header) for ((button, types) in pairList) { @@ -311,7 +303,7 @@ class HomeFragment : Fragment() { } fun bindChips( - header: ChipGroup?, + header: TvtypesChipsBinding?, selectedTypes: List, validTypes: List, callback: (List) -> Unit @@ -344,7 +336,13 @@ class HomeFragment : Fragment() { BottomSheetDialog(this) builder.behavior.state = BottomSheetBehavior.STATE_EXPANDED - builder.setContentView(R.layout.home_select_mainpage) + val binding: HomeSelectMainpageBinding = HomeSelectMainpageBinding.inflate( + builder.layoutInflater, + null, + false + ) + + builder.setContentView(binding.root) builder.show() builder.let { dialog -> val isMultiLang = getApiProviderLangSettings().let { set -> @@ -408,7 +406,7 @@ class HomeFragment : Fragment() { } bindChips( - dialog.home_select_group, + binding.tvtypesChipsScroll.tvtypesChips, preSelectedTypes, validAPIs.flatMap { it.supportedTypes }.distinct() ) { list -> @@ -423,6 +421,9 @@ class HomeFragment : Fragment() { private val homeViewModel: HomeViewModel by activityViewModels() + var binding: FragmentHomeBinding? = null + + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -433,11 +434,24 @@ class HomeFragment : Fragment() { bottomSheetDialog?.ownShow() val layout = if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home - return inflater.inflate(layout, container, false) + + /* val binding = FragmentHomeTvBinding.inflate(layout, container, false) + binding.homeLoadingError + + val binding2 = FragmentHomeBinding.inflate(layout, container, false) + binding2.homeLoadingError*/ + val root = inflater.inflate(layout, container, false) + binding = FragmentHomeBinding.bind(root) + //val localBinding = FragmentHomeBinding.inflate(inflater) + //binding = localBinding + return root + + //return inflater.inflate(layout, container, false) } override fun onDestroyView() { bottomSheetDialog?.ownHide() + binding = null super.onDestroyView() } @@ -467,7 +481,7 @@ class HomeFragment : Fragment() { fixGrid() } - fun bookmarksUpdated(_data : Boolean) { + fun bookmarksUpdated(_data: Boolean) { reloadStored() } @@ -525,14 +539,18 @@ class HomeFragment : Fragment() { private var bottomSheetDialog: BottomSheetDialog? = null + @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) fixGrid() - home_change_api_loading?.setOnClickListener(apiChangeClickListener) - home_api_fab?.setOnClickListener(apiChangeClickListener) - home_random?.setOnClickListener { + binding?.homeChangeApiLoading?.setOnClickListener(apiChangeClickListener) + + + binding?.homeChangeApiLoading?.setOnClickListener(apiChangeClickListener) + binding?.homeApiFab?.setOnClickListener(apiChangeClickListener) + binding?.homeRandom?.setOnClickListener { if (listHomepageItems.isNotEmpty()) { activity.loadSearchResult(listHomepageItems.random()) } @@ -542,91 +560,100 @@ class HomeFragment : Fragment() { context?.let { val settingsManager = PreferenceManager.getDefaultSharedPreferences(it) toggleRandomButton = - settingsManager.getBoolean(getString(R.string.random_button_key), false) - home_random?.visibility = View.GONE + settingsManager.getBoolean( + getString(R.string.random_button_key), + false + ) && !isTvSettings() + binding?.homeRandom?.visibility = View.GONE } observe(homeViewModel.preview) { preview -> - (home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setPreviewData( + (binding?.homeMasterRecycler?.adapter as? HomeParentItemAdapterPreview?)?.setPreviewData( preview ) } observe(homeViewModel.apiName) { apiName -> currentApiName = apiName - home_api_fab?.text = apiName - (home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setApiName( + binding?.homeApiFab?.text = apiName + (binding?.homeMasterRecycler?.adapter as? HomeParentItemAdapterPreview?)?.setApiName( apiName ) } observe(homeViewModel.page) { data -> - when (data) { - is Resource.Success -> { - home_loading_shimmer?.stopShimmer() + binding?.apply { + when (data) { + is Resource.Success -> { + homeLoadingShimmer.stopShimmer() - val d = data.value - val mutableListOfResponse = mutableListOf() - listHomepageItems.clear() + val d = data.value + val mutableListOfResponse = mutableListOf() + listHomepageItems.clear() - (home_master_recycler?.adapter as? ParentItemAdapter)?.updateList( - d.values.toMutableList(), - home_master_recycler - ) + (homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList( + d.values.toMutableList(), + homeMasterRecycler + ) - home_loading?.isVisible = false - home_loading_error?.isVisible = false - home_master_recycler?.isVisible = true - //home_loaded?.isVisible = true - if (toggleRandomButton) { - //Flatten list - d.values.forEach { dlist -> - mutableListOfResponse.addAll(dlist.list.list) + homeLoading.isVisible = false + homeLoadingError.isVisible = false + homeMasterRecycler.isVisible = true + //home_loaded?.isVisible = true + if (toggleRandomButton) { + //Flatten list + d.values.forEach { dlist -> + mutableListOfResponse.addAll(dlist.list.list) + } + listHomepageItems.addAll(mutableListOfResponse.distinctBy { it.url }) + + homeRandom.isVisible = listHomepageItems.isNotEmpty() + } else { + homeRandom.isGone = true } - listHomepageItems.addAll(mutableListOfResponse.distinctBy { it.url }) - home_random?.isVisible = listHomepageItems.isNotEmpty() - } else { - home_random?.isGone = true } - } - is Resource.Failure -> { - home_loading_shimmer?.stopShimmer() - result_error_text.text = data.errorString + is Resource.Failure -> { - home_reload_connectionerror.setOnClickListener(apiChangeClickListener) + homeLoadingShimmer.stopShimmer() - home_reload_connection_open_in_browser.setOnClickListener { view -> - val validAPIs = apis//.filter { api -> api.hasMainPage } + resultErrorText.text = data.errorString - view.popupMenuNoIconsAndNoStringRes(validAPIs.mapIndexed { index, api -> - Pair( - index, - api.name - ) - }) { - try { - val i = Intent(Intent.ACTION_VIEW) - i.data = Uri.parse(validAPIs[itemId].mainUrl) - startActivity(i) - } catch (e: Exception) { - logError(e) + homeReloadConnectionerror.setOnClickListener(apiChangeClickListener) + + homeReloadConnectionOpenInBrowser.setOnClickListener { view -> + val validAPIs = apis//.filter { api -> api.hasMainPage } + + view.popupMenuNoIconsAndNoStringRes(validAPIs.mapIndexed { index, api -> + Pair( + index, + api.name + ) + }) { + try { + val i = Intent(Intent.ACTION_VIEW) + i.data = Uri.parse(validAPIs[itemId].mainUrl) + startActivity(i) + } catch (e: Exception) { + logError(e) + } } } + + homeLoading.isVisible = false + homeLoadingError.isVisible = true + homeMasterRecycler.isVisible = false + //home_loaded?.isVisible = false } - home_loading?.isVisible = false - home_loading_error?.isVisible = true - home_master_recycler?.isVisible = false - //home_loaded?.isVisible = false - } - is Resource.Loading -> { - (home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(listOf()) - home_loading_shimmer?.startShimmer() - home_loading?.isVisible = true - home_loading_error?.isVisible = false - home_master_recycler?.isVisible = false - //home_loaded?.isVisible = false + is Resource.Loading -> { + (homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(listOf()) + homeLoadingShimmer.startShimmer() + homeLoading.isVisible = true + homeLoadingError.isVisible = false + homeMasterRecycler.isVisible = false + //home_loaded?.isVisible = false + } } } } @@ -638,19 +665,19 @@ class HomeFragment : Fragment() { HOME_BOOKMARK_VALUE_LIST, availableWatchStatusTypes.first.map { it.internalId }.toIntArray() ) - (home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setAvailableWatchStatusTypes( + (binding?.homeMasterRecycler?.adapter as? HomeParentItemAdapterPreview?)?.setAvailableWatchStatusTypes( availableWatchStatusTypes ) } observe(homeViewModel.bookmarks) { data -> - (home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setBookmarkData( + (binding?.homeMasterRecycler?.adapter as? HomeParentItemAdapterPreview?)?.setBookmarkData( data ) } observe(homeViewModel.resumeWatching) { resumeWatching -> - (home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setResumeWatchingData( + (binding?.homeMasterRecycler?.adapter as? HomeParentItemAdapterPreview?)?.setResumeWatchingData( resumeWatching ) if (isTrueTvSettings()) { @@ -665,9 +692,9 @@ class HomeFragment : Fragment() { //context?.fixPaddingStatusbarView(home_statusbar) //context?.fixPaddingStatusbar(home_padding) - context?.fixPaddingStatusbar(home_loading_statusbar) + fixPaddingStatusbar(binding?.homeLoadingStatusbar) - home_master_recycler?.adapter = + binding?.homeMasterRecycler?.adapter = HomeParentItemAdapterPreview(mutableListOf(), { callback -> homeHandleSearch(callback) }, { item -> @@ -699,18 +726,22 @@ class HomeFragment : Fragment() { reloadStored() loadHomePage(false) - home_master_recycler?.addOnScrollListener(object : RecyclerView.OnScrollListener() { + binding?.homeMasterRecycler?.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - if (dy > 0) { //check for scroll down - home_api_fab?.shrink() // hide - home_random?.shrink() - } else if (dy < -5) { - if (!isTvSettings()) { - home_api_fab?.extend() // show - home_random?.extend() + + binding?.apply { + if (dy > 0) { //check for scroll down + homeApiFab.shrink() // hide + homeRandom.shrink() + } else if (dy < -5) { + if (!isTvSettings()) { + homeApiFab.extend() // show + homeRandom.extend() + } } } + super.onScrolled(recyclerView, dx, dy) } }) @@ -718,18 +749,20 @@ class HomeFragment : Fragment() { // nice profile pic on homepage //home_profile_picture_holder?.isVisible = false // just in case - if (isTvSettings()) { - home_api_fab?.isVisible = false - if (isTrueTvSettings()) { - home_change_api_loading?.isVisible = true - home_change_api_loading?.isFocusable = true - home_change_api_loading?.isFocusableInTouchMode = true + binding?.apply { + if (isTvSettings()) { + homeApiFab.isVisible = false + if (isTrueTvSettings()) { + homeChangeApiLoading.isVisible = true + homeChangeApiLoading.isFocusable = true + homeChangeApiLoading.isFocusableInTouchMode = true + } + // home_bookmark_select?.isFocusable = true + // home_bookmark_select?.isFocusableInTouchMode = true + } else { + homeApiFab.isVisible = true + homeChangeApiLoading.isVisible = false } - // home_bookmark_select?.isFocusable = true - // home_bookmark_select?.isFocusableInTouchMode = true - } else { - home_api_fab?.isVisible = true - home_change_api_loading?.isVisible = false } //TODO READD THIS /*for (syncApi in OAuth2Apis) { 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 94a1a526..715f1867 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 @@ -544,7 +544,7 @@ class HomeParentItemAdapterPreview( } } - itemView.home_search?.context?.fixPaddingStatusbar(itemView.home_search) + fixPaddingStatusbar(itemView.home_search) itemView.home_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { @@ -575,7 +575,7 @@ class HomeParentItemAdapterPreview( layoutParams = params } } else { - itemView.home_none_padding?.context?.fixPaddingStatusbarView(itemView.home_none_padding) + fixPaddingStatusbarView(itemView.home_none_padding) } when (preview) { is Resource.Success -> { 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 index d7c06c4e..a20cd5c6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -6,7 +6,6 @@ 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 @@ -14,6 +13,7 @@ import android.view.animation.AlphaAnimation import androidx.annotation.StringRes import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible +import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import com.google.android.material.tabs.TabLayoutMediator import com.lagradost.cloudstream3.APIHolder @@ -22,6 +22,7 @@ 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.databinding.FragmentLibraryBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.observe @@ -37,7 +38,6 @@ 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" @@ -73,14 +73,25 @@ class LibraryFragment : Fragment() { private val libraryViewModel: LibraryViewModel by activityViewModels() + var binding: FragmentLibraryBinding? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_library, container, false) + ): View { + val localBinding = FragmentLibraryBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + + //return inflater.inflate(R.layout.fragment_library, container, false) + } + + override fun onDestroyView() { + binding = null + super.onDestroyView() } override fun onSaveInstanceState(outState: Bundle) { - viewpager?.currentItem?.let { currentItem -> + binding?.viewpager?.currentItem?.let { currentItem -> outState.putInt(VIEWPAGER_ITEM_KEY, currentItem) } super.onSaveInstanceState(outState) @@ -88,9 +99,9 @@ class LibraryFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(search_status_bar_padding) + fixPaddingStatusbar(binding?.searchStatusBarPadding) - sort_fab?.setOnClickListener { + binding?.sortFab?.setOnClickListener { val methods = libraryViewModel.sortingMethods.map { txt(it.stringRes).asString(view.context) } @@ -106,7 +117,7 @@ class LibraryFragment : Fragment() { }) } - main_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { libraryViewModel.sort(ListSorting.Query, query) return true @@ -129,7 +140,7 @@ class LibraryFragment : Fragment() { libraryViewModel.reloadPages(false) - list_selector?.setOnClickListener { + binding?.listSelector?.setOnClickListener { val items = libraryViewModel.availableApiNames val currentItem = libraryViewModel.currentApiName.value @@ -209,20 +220,22 @@ class LibraryFragment : Fragment() { } } - provider_selector?.setOnClickListener { + binding?.providerSelector?.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 -> + binding?.viewpager?.setPageTransformer(LibraryScrollTransformer()) + binding?.viewpager?.adapter = + binding?.viewpager?.adapter ?: ViewpagerAdapter( + mutableListOf(), + { isScrollingDown: Boolean -> + if (isScrollingDown) { + binding?.sortFab?.shrink() + } else { + binding?.sortFab?.extend() + } + }) callback@{ searchClickCallback -> // To prevent future accidents debugAssert({ searchClickCallback.card !is SyncAPI.LibraryItem @@ -267,6 +280,7 @@ class LibraryFragment : Fragment() { ) } } + LibraryOpenerType.None -> {} LibraryOpenerType.Provider -> savedSelection.providerData?.apiName?.let { apiName -> @@ -275,8 +289,10 @@ class LibraryFragment : Fragment() { apiName, ) } + LibraryOpenerType.Browser -> openBrowser(searchClickCallback.card.url) + LibraryOpenerType.Search -> { QuickSearchFragment.pushSearch( activity, @@ -288,22 +304,28 @@ class LibraryFragment : Fragment() { } } - viewpager?.offscreenPageLimit = 2 - viewpager?.reduceDragSensitivity() + binding?.apply { + 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 + binding?.apply { + gridview.numColumns = context?.getSpanCount() ?: 3 + gridview.adapter = + context?.let { LoadingPosterAdapter(it, 6 * 3) } + libraryLoadingOverlay.isVisible = true + libraryLoadingShimmer.startShimmer() + emptyListTextview.isVisible = false + } } val stopLoading = Runnable { - gridview?.adapter = null - library_loading_overlay?.isVisible = false - library_loading_shimmer?.stopShimmer() + binding?.apply { + gridview.adapter = null + libraryLoadingOverlay.isVisible = false + libraryLoadingShimmer.stopShimmer() + } } val handler = Handler(Looper.getMainLooper()) @@ -314,65 +336,75 @@ class LibraryFragment : Fragment() { 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) + + + binding?.apply { + emptyListTextview.isVisible = showNotice + if (showNotice) { + if (libraryViewModel.availableApiNames.size > 1) { + emptyListTextview.setText(R.string.empty_library_logged_in_message) + } else { + emptyListTextview.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( + libraryTabLayout, + viewpager, + ) { tab, position -> + tab.text = pages.getOrNull(position)?.title?.asStringNull(context) + tab.view.setOnClickListener { + val currentItem = + binding?.viewpager?.currentItem ?: return@setOnClickListener + val distance = abs(position - currentItem) + hideViewpager(distance) + } + }.attach() } - - (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 :( @@ -383,7 +415,7 @@ class LibraryFragment : Fragment() { } override fun onConfigurationChanged(newConfig: Configuration) { - (viewpager.adapter as? ViewpagerAdapter)?.rebind() + (binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind() super.onConfigurationChanged(newConfig) } } 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 index 5f64880c..14d31356 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -11,7 +11,6 @@ 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), 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 index 2435f8be..05b05f44 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt @@ -3,23 +3,21 @@ 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.databinding.SearchResultGridExpandedBinding 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 @@ -32,8 +30,7 @@ class PageAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return LibraryItemViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.search_result_grid_expanded, parent, false) + SearchResultGridExpandedBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) } @@ -57,8 +54,7 @@ class PageAdapter( } } - inner class LibraryItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val cardView: ImageView = itemView.imageView + inner class LibraryItemViewHolder(val binding : SearchResultGridExpandedBinding) : RecyclerView.ViewHolder(binding.root) { private val compactView = false//itemView.context.getGridIsCompact() private val coverHeight: Int = @@ -85,11 +81,11 @@ class PageAdapter( val fg = getDifferentColor(bg)//palette.getVibrantColor(ContextCompat.getColor(ctx,R.color.ratingColor)) - itemView.text_rating.apply { + binding.textRating.apply { setTextColor(ColorStateList.valueOf(fg)) } - itemView.text_rating_holder?.backgroundTintList = ColorStateList.valueOf(bg) - itemView.watchProgress?.apply { + binding.textRatingHolder.backgroundTintList = ColorStateList.valueOf(bg) + binding.watchProgress.apply { progressTintList = ColorStateList.valueOf(fg) progressBackgroundTintList = ColorStateList.valueOf(bg) } @@ -99,7 +95,7 @@ class PageAdapter( // See searchAdaptor for this, it basically fixes the height if (!compactView) { - cardView.apply { + binding.imageView.apply { layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, coverHeight @@ -108,22 +104,22 @@ class PageAdapter( } val showProgress = item.episodesCompleted != null && item.episodesTotal != null - itemView.watchProgress.isVisible = showProgress + binding.watchProgress.isVisible = showProgress if (showProgress) { - itemView.watchProgress.max = item.episodesTotal!! - itemView.watchProgress.progress = item.episodesCompleted!! + binding.watchProgress.max = item.episodesTotal!! + binding.watchProgress.progress = item.episodesCompleted!! } - itemView.imageText.text = item.name + binding.imageText.text = item.name val showRating = (item.personalRating ?: 0) != 0 - itemView.text_rating_holder.isVisible = showRating + binding.textRatingHolder.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" + binding.textRating.text = "★ $rating" } } } 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 index 33a40386..441d6adc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -2,16 +2,14 @@ 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.databinding.LibraryViewpagerPageBinding 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, @@ -20,8 +18,7 @@ class ViewpagerAdapter( ) : 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) + LibraryViewpagerPageBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) } @@ -34,6 +31,7 @@ class ViewpagerAdapter( } 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 @@ -43,44 +41,46 @@ class ViewpagerAdapter( this.notifyItemRangeChanged(0, pages.size) } - inner class PageViewHolder(private val itemViewTest: View) : - RecyclerView.ViewHolder(itemViewTest) { + inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) : + RecyclerView.ViewHolder(binding.root) { 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 - ) + binding.pageRecyclerview.apply { + spanCount = + this@PageViewHolder.itemView.context.getSpanCount() ?: 3 + if (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 + doOnAttach { + adapter = PageAdapter( + page.items.toMutableList(), + this, + clickCallback + ) + } + } else { + (adapter as? PageAdapter)?.updateList(page.items) + scrollToPosition(0) } - } 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 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + 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 + scrollCallback.invoke(diff > 0) + } + } else { + onFlingListener = object : OnFlingListener() { + override fun onFling(velocityX: Int, velocityY: Int): Boolean { + scrollCallback.invoke(velocityY > 0) + return false + } } } } + } } 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 ba57d2de..9e5ca6ba 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 @@ -21,6 +21,7 @@ import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.QuickSearchBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe @@ -37,7 +38,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage -import kotlinx.android.synthetic.main.quick_search.* import java.util.concurrent.locks.ReentrantLock class QuickSearchFragment : Fragment() { @@ -72,6 +72,8 @@ class QuickSearchFragment : Fragment() { private var providers: Set? = null private lateinit var searchViewModel: SearchViewModel + var binding: QuickSearchBinding? = null + private var bottomSheetDialog: BottomSheetDialog? = null @@ -79,13 +81,21 @@ class QuickSearchFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { activity?.window?.setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE ) searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java] bottomSheetDialog?.ownShow() - return inflater.inflate(R.layout.quick_search, container, false) + val localBinding = QuickSearchBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.quick_search, container, false) + } + + override fun onDestroyView() { + binding = null + super.onDestroyView() } override fun onDestroy() { @@ -111,7 +121,7 @@ class QuickSearchFragment : Fragment() { activity?.getSpanCount()?.let { HomeFragment.currentSpan = it } - quick_search_autofit_results.spanCount = HomeFragment.currentSpan + binding?.quickSearchAutofitResults?.spanCount = HomeFragment.currentSpan HomeFragment.currentSpan = HomeFragment.currentSpan HomeFragment.configEvent.invoke(HomeFragment.currentSpan) } @@ -123,7 +133,7 @@ class QuickSearchFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(quick_search_root) + fixPaddingStatusbar(binding?.quickSearchRoot) fixGrid() arguments?.getStringArray(PROVIDER_KEY)?.let { @@ -136,21 +146,22 @@ class QuickSearchFragment : Fragment() { } else false if (isSingleProvider) { - quick_search_autofit_results.adapter = activity?.let { - SearchAdapter( + binding?.quickSearchAutofitResults?.apply { + adapter = SearchAdapter( ArrayList(), - quick_search_autofit_results, + this, ) { callback -> SearchHelper.handleSearchClickCallback(activity, callback) } } + try { - quick_search?.queryHint = getString(R.string.search_hint_site).format(providers?.first()) + binding?.quickSearch?.queryHint = getString(R.string.search_hint_site).format(providers?.first()) } catch (e: Exception) { logError(e) } } else { - quick_search_master_recycler?.adapter = + binding?.quickSearchMasterRecycler?.adapter = ParentItemAdapter(mutableListOf(), { callback -> SearchHelper.handleSearchClickCallback(activity, callback) //when (callback.action) { @@ -164,18 +175,17 @@ class QuickSearchFragment : Fragment() { bottomSheetDialog = null }) }) - quick_search_master_recycler?.layoutManager = GridLayoutManager(context, 1) + binding?.quickSearchMasterRecycler?.layoutManager = GridLayoutManager(context, 1) } - - quick_search_autofit_results?.isVisible = isSingleProvider - quick_search_master_recycler?.isGone = isSingleProvider + binding?.quickSearchAutofitResults?.isVisible = isSingleProvider + binding?.quickSearchMasterRecycler?.isGone = isSingleProvider val listLock = ReentrantLock() observe(searchViewModel.currentSearch) { list -> try { // https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist listLock.lock() - (quick_search_master_recycler?.adapter as ParentItemAdapter?)?.apply { + (binding?.quickSearchMasterRecycler?.adapter as ParentItemAdapter?)?.apply { updateList(list.map { ongoing -> val ongoingList = HomePageList( ongoing.apiName, @@ -192,19 +202,18 @@ class QuickSearchFragment : Fragment() { } val searchExitIcon = - quick_search?.findViewById(androidx.appcompat.R.id.search_close_btn) + binding?.quickSearch?.findViewById(androidx.appcompat.R.id.search_close_btn) //val searchMagIcon = - // quick_search?.findViewById(androidx.appcompat.R.id.search_mag_icon) + // binding.quickSearch.findViewById(androidx.appcompat.R.id.search_mag_icon) //searchMagIcon?.scaleX = 0.65f //searchMagIcon?.scaleY = 0.65f - - quick_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + binding?.quickSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { if (search(context, query, false)) - UIHelper.hideKeyboard(quick_search) + UIHelper.hideKeyboard(binding?.quickSearch) return true } @@ -214,27 +223,26 @@ class QuickSearchFragment : Fragment() { return true } }) - - quick_search_loading_bar.alpha = 0f + binding?.quickSearchLoadingBar?.alpha = 0f observe(searchViewModel.searchResponse) { when (it) { is Resource.Success -> { it.value.let { data -> - (quick_search_autofit_results?.adapter as? SearchAdapter)?.updateList( + (binding?.quickSearchAutofitResults?.adapter as? SearchAdapter)?.updateList( context?.filterSearchResultByFilmQuality(data) ?: data ) } searchExitIcon?.alpha = 1f - quick_search_loading_bar?.alpha = 0f + binding?.quickSearchLoadingBar?.alpha = 0f } is Resource.Failure -> { // Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show() searchExitIcon?.alpha = 1f - quick_search_loading_bar?.alpha = 0f + binding?.quickSearchLoadingBar?.alpha = 0f } is Resource.Loading -> { searchExitIcon?.alpha = 0f - quick_search_loading_bar?.alpha = 1f + binding?.quickSearchLoadingBar?.alpha = 1f } } } @@ -246,13 +254,12 @@ class QuickSearchFragment : Fragment() { // UIHelper.showInputMethod(view.findFocus()) // } //} - - quick_search_back.setOnClickListener { + binding?.quickSearchBack?.setOnClickListener { activity?.popCurrentPage() } arguments?.getString(AUTOSEARCH_KEY)?.let { - quick_search?.setQuery(it, true) + binding?.quickSearch?.setQuery(it, true) arguments?.remove(AUTOSEARCH_KEY) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 92cecc37..7b415d78 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -1,20 +1,17 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.ActorData import com.lagradost.cloudstream3.ActorRole import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.CastItemBinding import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.cast_item.view.* -class ActorAdaptor() : RecyclerView.Adapter() { +class ActorAdaptor : RecyclerView.Adapter() { data class ActorMetaData( var isInverted: Boolean, val actor: ActorData, @@ -24,7 +21,7 @@ class ActorAdaptor() : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return CardViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.cast_item, parent, false), + CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) } @@ -68,15 +65,9 @@ class ActorAdaptor() : RecyclerView.Adapter() { private class CardViewHolder constructor( - itemView: View, + val binding: CastItemBinding, ) : - RecyclerView.ViewHolder(itemView) { - private val actorImage: ImageView = itemView.actor_image - private val actorName: TextView = itemView.actor_name - private val actorExtra: TextView = itemView.actor_extra - private val voiceActorImage: ImageView = itemView.voice_actor_image - private val voiceActorImageHolder: View = itemView.voice_actor_image_holder - private val voiceActorName: TextView = itemView.voice_actor_name + RecyclerView.ViewHolder(binding.root) { fun bind(actor: ActorData, isInverted: Boolean, position: Int, callback: (Int) -> Unit) { val (mainImg, vaImage) = if (!isInverted || actor.voiceActor?.image.isNullOrBlank()) { @@ -89,39 +80,43 @@ class ActorAdaptor() : RecyclerView.Adapter() { callback(position) } - actorImage.setImage(mainImg) + binding.apply { + actorImage.setImage(mainImg) - actorName.text = actor.actor.name - actor.role?.let { - actorExtra.context?.getString( - when (it) { - ActorRole.Main -> { - R.string.actor_main - } - ActorRole.Supporting -> { - R.string.actor_supporting - } - ActorRole.Background -> { - R.string.actor_background + actorName.text = actor.actor.name + actor.role?.let { + actorExtra.context?.getString( + when (it) { + ActorRole.Main -> { + R.string.actor_main + } + + ActorRole.Supporting -> { + R.string.actor_supporting + } + + ActorRole.Background -> { + R.string.actor_background + } } + )?.let { text -> + actorExtra.isVisible = true + actorExtra.text = text } - )?.let { text -> + } ?: actor.roleString?.let { actorExtra.isVisible = true - actorExtra.text = text + actorExtra.text = it + } ?: run { + actorExtra.isVisible = false } - } ?: actor.roleString?.let { - actorExtra.isVisible = true - actorExtra.text = it - } ?: run { - actorExtra.isVisible = false - } - if (actor.voiceActor == null) { - voiceActorImageHolder.isVisible = false - voiceActorName.isVisible = false - } else { - voiceActorName.text = actor.voiceActor.name - voiceActorImageHolder.isVisible = voiceActorImage.setImage(vaImage) + if (actor.voiceActor == null) { + voiceActorImageHolder.isVisible = false + voiceActorName.isVisible = false + } else { + voiceActorName.text = actor.voiceActor.name + voiceActorImageHolder.isVisible = voiceActorImage.setImage(vaImage) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 0932b001..216d95a4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -3,34 +3,25 @@ package com.lagradost.cloudstream3.ui.result import android.annotation.SuppressLint import android.content.Context import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.core.view.isGone import androidx.core.view.isVisible -import androidx.core.widget.ContentLoadingProgressBar import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding +import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DownloadButtonViewHolder import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.ui.download.EasyDownloadButton import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager -import kotlinx.android.synthetic.main.result_episode.view.* -import kotlinx.android.synthetic.main.result_episode.view.episode_text -import kotlinx.android.synthetic.main.result_episode_large.view.* -import kotlinx.android.synthetic.main.result_episode_large.view.episode_filler -import kotlinx.android.synthetic.main.result_episode_large.view.episode_progress -import kotlinx.android.synthetic.main.result_episode_large.view.result_episode_download -import kotlinx.android.synthetic.main.result_episode_large.view.result_episode_progress_downloaded import java.util.* const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 @@ -144,26 +135,52 @@ class EpisodeAdapter( diffResult.dispatchUpdatesTo(this) } - var layout = R.layout.result_episode_both + fun getItem(position: Int) : ResultEpisode { + return cardList[position] + } + + override fun getItemViewType(position: Int): Int { + val item = getItem(position) + return if(item.poster.isNullOrBlank()) 0 else 1 + } + + + // private val layout = R.layout.result_episode_both override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { /*val layout = if (cardList.filter { it.poster != null }.size >= cardList.size / 2) R.layout.result_episode_large else R.layout.result_episode*/ - return EpisodeCardViewHolder( - LayoutInflater.from(parent.context) - .inflate(layout, parent, false), - hasDownloadSupport, - clickCallback, - downloadClickCallback - ) + return when(viewType) { + 0 -> { + EpisodeCardViewHolderSmall( + ResultEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false), + hasDownloadSupport, + clickCallback, + downloadClickCallback + ) + } + 1 -> { + EpisodeCardViewHolderLarge( + ResultEpisodeLargeBinding.inflate(LayoutInflater.from(parent.context), parent, false), + hasDownloadSupport, + clickCallback, + downloadClickCallback + ) + } + else -> throw NotImplementedError() + } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { - is EpisodeCardViewHolder -> { - holder.bind(cardList[position]) + is EpisodeCardViewHolderLarge -> { + holder.bind(getItem(position)) + mBoundViewHolders.add(holder) + } + is EpisodeCardViewHolderSmall -> { + holder.bind(getItem(position)) mBoundViewHolders.add(holder) } } @@ -173,94 +190,81 @@ class EpisodeAdapter( return cardList.size } - class EpisodeCardViewHolder + class EpisodeCardViewHolderLarge constructor( - itemView: View, + val binding : ResultEpisodeLargeBinding, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, private val downloadClickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder { + ) : RecyclerView.ViewHolder(binding.root), DownloadButtonViewHolder { override var downloadButton = EasyDownloadButton() - var episodeDownloadBar: ContentLoadingProgressBar? = null - var episodeDownloadImage: ImageView? = null + // TODO TV + var localCard: ResultEpisode? = null @SuppressLint("SetTextI18n") fun bind(card: ResultEpisode) { localCard = card + binding.episodeLinHolder.layoutParams.apply { + width = if(isTvSettings()) ViewGroup.LayoutParams.WRAP_CONTENT else ViewGroup.LayoutParams.MATCH_PARENT + } + val isTrueTv = isTrueTvSettings() - val (parentView, otherView) = if (card.poster == null) { - itemView.episode_holder to itemView.episode_holder_large - } else { - itemView.episode_holder_large to itemView.episode_holder - } - parentView.isVisible = true - otherView.isVisible = false + binding.apply { - val episodeText: TextView = parentView.episode_text - val episodeFiller: MaterialButton? = parentView.episode_filler - val episodeRating: TextView? = parentView.episode_rating - val episodeDescript: TextView? = parentView.episode_descript - val episodeProgress: ContentLoadingProgressBar? = parentView.episode_progress - val episodePoster: ImageView? = parentView.episode_poster + val name = + if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}" + episodeFiller.isVisible = card.isFiller == true + episodeText.text = + name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name + episodeText.isSelected = true // is needed for text repeating - episodeDownloadBar = - parentView.result_episode_progress_downloaded - episodeDownloadImage = parentView.result_episode_download - - val name = - if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}" - episodeFiller?.isVisible = card.isFiller == true - episodeText.text = - name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name - episodeText.isSelected = true // is needed for text repeating - - if (card.videoWatchState == VideoWatchState.Watched) { - // This cannot be done in getDisplayPosition() as when you have not watched something - // the duration and position is 0 - episodeProgress?.max = 1 - episodeProgress?.progress = 1 - episodeProgress?.isVisible = true - } else { - val displayPos = card.getDisplayPosition() - episodeProgress?.max = (card.duration / 1000).toInt() - episodeProgress?.progress = (displayPos / 1000).toInt() - episodeProgress?.isVisible = displayPos > 0L - } - - episodePoster?.isVisible = episodePoster?.setImage(card.poster) == true - - if (card.rating != null) { - episodeRating?.text = episodeRating?.context?.getString(R.string.rated_format) - ?.format(card.rating.toFloat() / 10f) - } else { - episodeRating?.text = "" - } - - episodeRating?.isGone = episodeRating?.text.isNullOrBlank() - - episodeDescript?.apply { - text = card.description.html() - isGone = text.isNullOrBlank() - setOnClickListener { - clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card)) - } - } - - if (!isTrueTv) { - episodePoster?.setOnClickListener { - clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) + if (card.videoWatchState == VideoWatchState.Watched) { + // This cannot be done in getDisplayPosition() as when you have not watched something + // the duration and position is 0 + episodeProgress.max = 1 + episodeProgress.progress = 1 + episodeProgress.isVisible = true + } else { + val displayPos = card.getDisplayPosition() + episodeProgress.max = (card.duration / 1000).toInt() + episodeProgress.progress = (displayPos / 1000).toInt() + episodeProgress.isVisible = displayPos > 0L } - episodePoster?.setOnLongClickListener { - clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_TOAST, card)) - return@setOnLongClickListener true + episodePoster.isVisible = episodePoster.setImage(card.poster) == true + + if (card.rating != null) { + episodeRating.text = episodeRating.context?.getString(R.string.rated_format) + ?.format(card.rating.toFloat() / 10f) + } else { + episodeRating.text = "" + } + + episodeRating.isGone = episodeRating.text.isNullOrBlank() + + episodeDescript.apply { + text = card.description.html() + isGone = text.isNullOrBlank() + setOnClickListener { + clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card)) + } + } + + if (!isTrueTv) { + episodePoster.setOnClickListener { + clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) + } + + episodePoster.setOnLongClickListener { + clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_TOAST, card)) + return@setOnLongClickListener true + } } } - itemView.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } @@ -276,8 +280,8 @@ class EpisodeAdapter( return@setOnLongClickListener true } - episodeDownloadImage?.isVisible = hasDownloadSupport - episodeDownloadBar?.isVisible = hasDownloadSupport + binding.resultEpisodeDownload.isVisible = hasDownloadSupport + binding.resultEpisodeProgressDownloaded.isVisible = hasDownloadSupport reattachDownloadButton() } @@ -285,9 +289,6 @@ class EpisodeAdapter( downloadButton.dispose() val card = localCard if (hasDownloadSupport && card != null) { - if (episodeDownloadBar == null || - episodeDownloadImage == null - ) return val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( itemView.context, card.id @@ -296,8 +297,109 @@ class EpisodeAdapter( downloadButton.setUpButton( downloadInfo?.fileLength, downloadInfo?.totalBytes, - episodeDownloadBar ?: return, - episodeDownloadImage ?: return, + binding.resultEpisodeProgressDownloaded, + binding.resultEpisodeDownload, + null, + VideoDownloadHelper.DownloadEpisodeCached( + card.name, + card.poster, + card.episode, + card.season, + card.id, + card.parentId, + card.rating, + card.description, + System.currentTimeMillis(), + ) + ) { + if (it.action == DOWNLOAD_ACTION_DOWNLOAD) { + clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card)) + } else { + downloadClickCallback.invoke(it) + } + } + } + } + } + + class EpisodeCardViewHolderSmall + constructor( + val binding : ResultEpisodeBinding, + private val hasDownloadSupport: Boolean, + private val clickCallback: (EpisodeClickEvent) -> Unit, + private val downloadClickCallback: (DownloadClickEvent) -> Unit, + ) : RecyclerView.ViewHolder(binding.root), DownloadButtonViewHolder { + override var downloadButton = EasyDownloadButton() + + var localCard: ResultEpisode? = null + + @SuppressLint("SetTextI18n") + fun bind(card: ResultEpisode) { + localCard = card + + val isTrueTv = isTrueTvSettings() + + binding.episodeHolder.layoutParams.apply { + width = if(isTvSettings()) ViewGroup.LayoutParams.WRAP_CONTENT else ViewGroup.LayoutParams.MATCH_PARENT + } + + binding.apply { + val name = + if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}" + episodeFiller.isVisible = card.isFiller == true + episodeText.text = + name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name + episodeText.isSelected = true // is needed for text repeating + + if (card.videoWatchState == VideoWatchState.Watched) { + // This cannot be done in getDisplayPosition() as when you have not watched something + // the duration and position is 0 + episodeProgress.max = 1 + episodeProgress.progress = 1 + episodeProgress.isVisible = true + } else { + val displayPos = card.getDisplayPosition() + episodeProgress.max = (card.duration / 1000).toInt() + episodeProgress.progress = (displayPos / 1000).toInt() + episodeProgress.isVisible = displayPos > 0L + } + + itemView.setOnClickListener { + clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) + } + + if (isTrueTv) { + itemView.isFocusable = true + itemView.isFocusableInTouchMode = true + //itemView.touchscreenBlocksFocus = false + } + + itemView.setOnLongClickListener { + clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) + return@setOnLongClickListener true + } + + binding.resultEpisodeDownload.isVisible = hasDownloadSupport + binding.resultEpisodeProgressDownloaded.isVisible = hasDownloadSupport + reattachDownloadButton() + } + } + + override fun reattachDownloadButton() { + downloadButton.dispose() + val card = localCard + if (hasDownloadSupport && card != null) { + + val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( + itemView.context, + card.id + ) + + downloadButton.setUpButton( + downloadInfo?.fileLength, + downloadInfo?.totalBytes, + binding.resultEpisodeProgressDownloaded, + binding.resultEpisodeDownload, null, VideoDownloadHelper.DownloadEpisodeCached( card.name, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt index ebd6a658..ca2934ef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt @@ -1,11 +1,10 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.ImageView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings /* @@ -24,7 +23,6 @@ const val IMAGE_CLICK = 0 const val IMAGE_LONG_CLICK = 1 class ImageAdapter( - val layout: Int, val clickCallback: ((Int) -> Unit)? = null, val nextFocusUp: Int? = null, val nextFocusDown: Int? = null, @@ -34,7 +32,9 @@ class ImageAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ImageViewHolder( - LayoutInflater.from(parent.context).inflate(layout, parent, false) + //result_mini_image + ResultMiniImageBinding.inflate(LayoutInflater.from(parent.context), parent, false) + // LayoutInflater.from(parent.context).inflate(layout, parent, false) ) } @@ -66,15 +66,15 @@ class ImageAdapter( } class ImageViewHolder - constructor(itemView: View) : - RecyclerView.ViewHolder(itemView) { + constructor(val binding: ResultMiniImageBinding) : + RecyclerView.ViewHolder(binding.root) { fun bind( img: Int, clickCallback: ((Int) -> Unit)?, nextFocusUp: Int?, nextFocusDown: Int?, ) { - (itemView as? ImageView?)?.apply { + binding.root.apply { setImageResource(img) if (nextFocusDown != null) { this.nextFocusDownId = nextFocusDown 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 5a3e28b4..68fc87e6 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 @@ -310,6 +310,7 @@ open class ResultFragment : ResultTrailerPlayer() { result_finish_loading?.isVisible = false result_loading_error?.isVisible = false } + 1 -> { result_bookmark_fab?.isGone = true result_loading?.isVisible = false @@ -317,6 +318,7 @@ open class ResultFragment : ResultTrailerPlayer() { result_loading_error?.isVisible = true result_reload_connection_open_in_browser?.isVisible = true } + 2 -> { result_bookmark_fab?.isGone = isTrueTvSettings() result_bookmark_fab?.extend() @@ -350,9 +352,9 @@ open class ResultFragment : ResultTrailerPlayer() { viewModel.reloadEpisodes() } - open fun updateMovie(data: ResourceSome>) { + open fun updateMovie(data: Resource>?) { when (data) { - is ResourceSome.Success -> { + is Resource.Success -> { data.value.let { (text, ep) -> result_play_movie.setText(text) result_play_movie?.setOnClickListener { @@ -410,6 +412,7 @@ open class ResultFragment : ResultTrailerPlayer() { EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, ep) ) } + else -> handleDownloadClick(activity, click) } } @@ -417,6 +420,7 @@ open class ResultFragment : ResultTrailerPlayer() { } } } + else -> { result_movie_progress_downloaded_holder?.isVisible = false result_play_movie?.isVisible = false @@ -424,17 +428,14 @@ open class ResultFragment : ResultTrailerPlayer() { } } - open fun updateEpisodes(episodes: ResourceSome>) { + open fun updateEpisodes(episodes: Resource>?) { when (episodes) { - is ResourceSome.None -> { - result_episode_loading?.isVisible = false - result_episodes?.isVisible = false - } - is ResourceSome.Loading -> { + is Resource.Loading -> { result_episode_loading?.isVisible = true result_episodes?.isVisible = false } - is ResourceSome.Success -> { + + is Resource.Success -> { result_episodes?.isVisible = true result_episode_loading?.isVisible = false @@ -471,6 +472,11 @@ open class ResultFragment : ResultTrailerPlayer() { result_episodes?.requestFocus() } } + + else -> { + result_episode_loading?.isVisible = false + result_episodes?.isVisible = false + } } } @@ -565,7 +571,7 @@ open class ResultFragment : ResultTrailerPlayer() { context?.updateHasTrailers() activity?.loadCache() - activity?.fixPaddingStatusbar(result_top_bar) + fixPaddingStatusbar(result_top_bar) //activity?.fixPaddingStatusbar(result_barstatus) /* val backParameter = result_back.layoutParams as FrameLayout.LayoutParams @@ -588,7 +594,7 @@ open class ResultFragment : ResultTrailerPlayer() { result_episodes?.adapter = EpisodeAdapter( - api?.hasDownloadSupport == true, + api?.hasDownloadSupport == true && !isTvSettings(), { episodeClick -> viewModel.handleAction(activity, episodeClick) }, @@ -738,10 +744,12 @@ open class ResultFragment : ResultTrailerPlayer() { viewModel.setMeta(d, syncModel.getSyncs()) } + is Resource.Loading -> { result_sync_max_episodes?.text = result_sync_max_episodes?.context?.getString(R.string.sync_total_episodes_none) } + else -> {} } } @@ -755,11 +763,13 @@ open class ResultFragment : ResultTrailerPlayer() { result_sync_holder?.isVisible = false closed = true } + is Resource.Loading -> { result_sync_loading_shimmer?.startShimmer() result_sync_loading_shimmer?.isVisible = true result_sync_holder?.isVisible = false } + is Resource.Success -> { result_sync_loading_shimmer?.stopShimmer() result_sync_loading_shimmer?.isVisible = false @@ -789,6 +799,7 @@ open class ResultFragment : ResultTrailerPlayer() { } } } + null -> { closed = false } @@ -796,58 +807,56 @@ open class ResultFragment : ResultTrailerPlayer() { result_overlapping_panels?.setStartPanelLockState(if (closed) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED) } - observe(viewModel.resumeWatching) { resume -> - when (resume) { - is Some.Success -> { - result_resume_parent?.isVisible = true - val value = resume.value - value.progress?.let { progress -> - result_resume_series_title?.apply { - isVisible = !value.isMovie - text = - if (value.isMovie) null else activity?.getNameFull( - value.result.name, - value.result.episode, - value.result.season - ) - } - result_resume_series_progress_text.setText(progress.progressLeft) - result_resume_series_progress?.apply { - isVisible = true - this.max = progress.maxProgress - this.progress = progress.progress - } - result_resume_progress_holder?.isVisible = true - } ?: run { - result_resume_progress_holder?.isVisible = false - result_resume_series_progress?.isVisible = false - result_resume_series_title?.isVisible = false - result_resume_series_progress_text?.isVisible = false - } - - result_resume_series_button?.isVisible = !value.isMovie - result_resume_series_button_play?.isVisible = !value.isMovie - - val click = View.OnClickListener { - viewModel.handleAction( - activity, - EpisodeClickEvent( - storedData?.playerAction ?: ACTION_PLAY_EPISODE_IN_PLAYER, - value.result - ) - ) - } - - result_resume_series_button?.setOnClickListener(click) - result_resume_series_button_play?.setOnClickListener(click) - } - is Some.None -> { - result_resume_parent?.isVisible = false - } + observeNullable(viewModel.resumeWatching) { resume -> + if (resume == null) { + result_resume_parent?.isVisible = false + return@observeNullable } + result_resume_parent?.isVisible = true + resume.progress?.let { progress -> + result_resume_series_title?.apply { + isVisible = !resume.isMovie + text = + if (resume.isMovie) null else activity?.getNameFull( + resume.result.name, + resume.result.episode, + resume.result.season + ) + } + result_resume_series_progress_text?.setText(progress.progressLeft) + result_resume_series_progress?.apply { + isVisible = true + this.max = progress.maxProgress + this.progress = progress.progress + } + result_resume_progress_holder?.isVisible = true + } ?: run { + result_resume_progress_holder?.isVisible = false + result_resume_series_progress?.isVisible = false + result_resume_series_title?.isVisible = false + result_resume_series_progress_text?.isVisible = false + } + + result_resume_series_button?.isVisible = !resume.isMovie + result_resume_series_button_play?.isVisible = !resume.isMovie + + val click = View.OnClickListener { + viewModel.handleAction( + activity, + EpisodeClickEvent( + storedData?.playerAction ?: ACTION_PLAY_EPISODE_IN_PLAYER, + resume.result + ) + ) + } + + result_resume_series_button?.setOnClickListener(click) + result_resume_series_button_play?.setOnClickListener(click) } - observe(viewModel.episodes) { episodes -> + + + observeNullable(viewModel.episodes) { episodes -> updateEpisodes(episodes) } @@ -868,7 +877,7 @@ open class ResultFragment : ResultTrailerPlayer() { setRecommendations(recommendations, null) } - observe(viewModel.movie) { data -> + observeNullable(viewModel.movie) { data -> updateMovie(data) } @@ -1046,10 +1055,12 @@ open class ResultFragment : ResultTrailerPlayer() { }*/ //} } + is Resource.Failure -> { result_error_text.text = storedData?.url?.plus("\n") + data.errorString updateVisStatus(1) } + is Resource.Loading -> { updateVisStatus(0) } 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 2f232995..571d4b0b 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 @@ -20,9 +20,9 @@ import com.google.android.gms.cast.framework.CastState import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.updateHasTrailers -import com.lagradost.cloudstream3.mvvm.Some import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper @@ -212,7 +212,6 @@ class ResultFragmentPhone : ResultFragment() { }*/ result_mini_sync?.adapter = ImageAdapter( - R.layout.result_mini_image, nextFocusDown = R.id.result_sync_set_score, clickCallback = { action -> if (action == IMAGE_CLICK || action == IMAGE_LONG_CLICK) { @@ -271,73 +270,68 @@ class ResultFragmentPhone : ResultFragment() { } } - observe(viewModel.episodesCountText) { count -> + observeNullable(viewModel.episodesCountText) { count -> result_episodes_text.setText(count) } - observe(viewModel.selectPopup) { popup -> - when (popup) { - is Some.Success -> { - popupDialog?.dismissSafe(activity) - - popupDialog = activity?.let { act -> - val pop = popup.value - val options = pop.getOptions(act) - val title = pop.getTitle(act) - - act.showBottomDialogInstant( - options, title, { - popupDialog = null - pop.callback(null) - }, { - popupDialog = null - pop.callback(it) - } - ) - } - } - is Some.None -> { - popupDialog?.dismissSafe(activity) - popupDialog = null - } + observeNullable(viewModel.selectPopup) { popup -> + if (popup == null) { + popupDialog?.dismissSafe(activity) + popupDialog = null + return@observeNullable } + popupDialog?.dismissSafe(activity) + + popupDialog = activity?.let { act -> + val options = popup.getOptions(act) + val title = popup.getTitle(act) + + act.showBottomDialogInstant( + options, title, { + popupDialog = null + popup.callback(null) + }, { + popupDialog = null + popup.callback(it) + } + ) + } + + } observe(viewModel.loadedLinks) { load -> - when (load) { - is Some.Success -> { - if (loadingDialog?.isShowing != true) { - loadingDialog?.dismissSafe(activity) - loadingDialog = null - } - loadingDialog = loadingDialog ?: context?.let { ctx -> - val builder = - BottomSheetDialog(ctx) - builder.setContentView(R.layout.bottom_loading) - builder.setOnDismissListener { - loadingDialog = null - viewModel.cancelLinks() - } - //builder.setOnCancelListener { - // it?.dismiss() - //} - builder.setCanceledOnTouchOutside(true) - builder.show() - builder - } - } - is Some.None -> { - loadingDialog?.dismissSafe(activity) + if (load == null) { + loadingDialog?.dismissSafe(activity) + loadingDialog = null + return@observe + } + if (loadingDialog?.isShowing != true) { + loadingDialog?.dismissSafe(activity) + loadingDialog = null + } + loadingDialog = loadingDialog ?: context?.let { ctx -> + val builder = + BottomSheetDialog(ctx) + builder.setContentView(R.layout.bottom_loading) + builder.setOnDismissListener { loadingDialog = null + viewModel.cancelLinks() } + //builder.setOnCancelListener { + // it?.dismiss() + //} + builder.setCanceledOnTouchOutside(true) + builder.show() + builder } } - observe(viewModel.selectedSeason) { text -> + observeNullable(viewModel.selectedSeason) { text -> result_season_button.setText(text) selectSeason = - (if (text is Some.Success) text.value else null)?.asStringNull(result_season_button?.context) + text?.asStringNull(result_season_button?.context) // If the season button is visible the result season button will be next focus down if (result_season_button?.isVisible == true) if (result_resume_parent?.isVisible == true) @@ -346,7 +340,7 @@ class ResultFragmentPhone : ResultFragment() { // setFocusUpAndDown(result_bookmark_button, result_season_button) } - observe(viewModel.selectedDubStatus) { status -> + observeNullable(viewModel.selectedDubStatus) { status -> result_dub_select?.setText(status) if (result_dub_select?.isVisible == true) @@ -357,7 +351,7 @@ class ResultFragmentPhone : ResultFragment() { // setFocusUpAndDown(result_bookmark_button, result_dub_select) } } - observe(viewModel.selectedRange) { range -> + observeNullable(viewModel.selectedRange) { range -> result_episode_select.setText(range) // If Season button is invisible then the bookmark button next focus is episode select 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 71ecb0e9..91354117 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 @@ -12,9 +12,9 @@ import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse -import com.lagradost.cloudstream3.mvvm.ResourceSome -import com.lagradost.cloudstream3.mvvm.Some +import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.search.SearchAdapter @@ -69,16 +69,16 @@ class ResultFragmentTv : ResultFragment() { return focus == this.result_root } - override fun updateEpisodes(episodes: ResourceSome>) { + override fun updateEpisodes(episodes: Resource>?) { super.updateEpisodes(episodes) - if (episodes is ResourceSome.Success && hasNoFocus()) { + if (episodes is Resource.Success && hasNoFocus()) { result_episodes?.requestFocus() } } - override fun updateMovie(data: ResourceSome>) { + override fun updateMovie(data: Resource>?) { super.updateMovie(data) - if (data is ResourceSome.Success && hasNoFocus()) { + if (data is Resource.Success && hasNoFocus()) { result_play_movie?.requestFocus() } } @@ -130,9 +130,9 @@ class ResultFragmentTv : ResultFragment() { LinearListLayout(result_episodes?.context).apply { setHorizontal() } - (result_episodes?.adapter as EpisodeAdapter?)?.apply { - layout = R.layout.result_episode_both_tv - } + // (result_episodes?.adapter as EpisodeAdapter?)?.apply { + // layout = R.layout.result_episode_both_tv + // } //result_episodes?.setMaxViewPoolSize(0, Int.MAX_VALUE) result_season_selection.setAdapter() @@ -140,37 +140,37 @@ class ResultFragmentTv : ResultFragment() { result_dub_selection.setAdapter() result_recommendations_filter_selection.setAdapter() - observe(viewModel.selectPopup) { popup -> - when (popup) { - is Some.Success -> { - popupDialog?.dismissSafe(activity) + observeNullable(viewModel.selectPopup) { popup -> + if(popup == null) { + popupDialog?.dismissSafe(activity) + popupDialog = null + return@observeNullable + } - popupDialog = activity?.let { act -> - val pop = popup.value - val options = pop.getOptions(act) - val title = pop.getTitle(act) + popupDialog?.dismissSafe(activity) - act.showBottomDialogInstant( - options, title, { - popupDialog = null - pop.callback(null) - }, { - popupDialog = null - pop.callback(it) - } - ) + popupDialog = activity?.let { act -> + val options = popup.getOptions(act) + val title = popup.getTitle(act) + + act.showBottomDialogInstant( + options, title, { + popupDialog = null + popup.callback(null) + }, { + popupDialog = null + popup.callback(it) } - } - is Some.None -> { - popupDialog?.dismissSafe(activity) - popupDialog = null - } + ) } } - observe(viewModel.loadedLinks) { load -> - when (load) { - is Some.Success -> { + observeNullable(viewModel.loadedLinks) { load -> + if(load == null) { + loadingDialog?.dismissSafe(activity) + loadingDialog = null + return@observeNullable + } if (loadingDialog?.isShowing != true) { loadingDialog?.dismissSafe(activity) loadingDialog = null @@ -189,16 +189,11 @@ class ResultFragmentTv : ResultFragment() { builder.show() builder } - } - is Some.None -> { - loadingDialog?.dismissSafe(activity) - loadingDialog = null - } - } + } - observe(viewModel.episodesCountText) { count -> + observeNullable(viewModel.episodesCountText) { count -> result_episodes_text.setText(count) } 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 46a8c9f6..dbd1dd40 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 @@ -145,15 +145,18 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { minute ) } + hours > 0 -> txt( R.string.next_episode_time_hour_format, hours, minute ) + minute > 0 -> txt( R.string.next_episode_time_min_format, minute ) + else -> null }?.also { nextAiringEpisode = txt(R.string.next_episode_format, airing.episode) @@ -305,6 +308,7 @@ fun SelectPopup.getOptions(context: Context): List { is SelectPopup.SelectArray -> { this.options.map { it.first.asString(context) } } + is SelectPopup.SelectText -> options.map { it.asString(context) } } } @@ -352,17 +356,17 @@ class ResultViewModel2 : ViewModel() { MutableLiveData(null) val page: LiveData?> = _page - private val _episodes: MutableLiveData>> = - MutableLiveData(ResourceSome.Loading()) - val episodes: LiveData>> = _episodes + private val _episodes: MutableLiveData>?> = + MutableLiveData(Resource.Loading()) + val episodes: LiveData>?> = _episodes - private val _movie: MutableLiveData>> = - MutableLiveData(ResourceSome.None) - val movie: LiveData>> = _movie + private val _movie: MutableLiveData>?> = + MutableLiveData(null) + val movie: LiveData>?> = _movie - private val _episodesCountText: MutableLiveData> = - MutableLiveData(Some.None) - val episodesCountText: LiveData> = _episodesCountText + private val _episodesCountText: MutableLiveData = + MutableLiveData(null) + val episodesCountText: LiveData = _episodesCountText private val _trailers: MutableLiveData> = MutableLiveData(mutableListOf()) @@ -384,16 +388,16 @@ class ResultViewModel2 : ViewModel() { MutableLiveData(emptyList()) val recommendations: LiveData> = _recommendations - private val _selectedRange: MutableLiveData> = - MutableLiveData(Some.None) - val selectedRange: LiveData> = _selectedRange + private val _selectedRange: MutableLiveData = + MutableLiveData(null) + val selectedRange: LiveData = _selectedRange - private val _selectedSeason: MutableLiveData> = - MutableLiveData(Some.None) - val selectedSeason: LiveData> = _selectedSeason + private val _selectedSeason: MutableLiveData = + MutableLiveData(null) + val selectedSeason: LiveData = _selectedSeason - private val _selectedDubStatus: MutableLiveData> = MutableLiveData(Some.None) - val selectedDubStatus: LiveData> = _selectedDubStatus + private val _selectedDubStatus: MutableLiveData = MutableLiveData(null) + val selectedDubStatus: LiveData = _selectedDubStatus private val _selectedRangeIndex: MutableLiveData = MutableLiveData(-1) @@ -406,12 +410,12 @@ class ResultViewModel2 : ViewModel() { private val _selectedDubStatusIndex: MutableLiveData = MutableLiveData(-1) val selectedDubStatusIndex: LiveData = _selectedDubStatusIndex - private val _loadedLinks: MutableLiveData> = MutableLiveData(Some.None) - val loadedLinks: LiveData> = _loadedLinks + private val _loadedLinks: MutableLiveData = MutableLiveData(null) + val loadedLinks: LiveData = _loadedLinks - private val _resumeWatching: MutableLiveData> = - MutableLiveData(Some.None) - val resumeWatching: LiveData> = _resumeWatching + private val _resumeWatching: MutableLiveData = + MutableLiveData(null) + val resumeWatching: LiveData = _resumeWatching private val _episodeSynopsis: MutableLiveData = MutableLiveData(null) val episodeSynopsis: LiveData = _episodeSynopsis @@ -800,8 +804,8 @@ class ResultViewModel2 : ViewModel() { private val _watchStatus: MutableLiveData = MutableLiveData(WatchType.NONE) val watchStatus: LiveData get() = _watchStatus - private val _selectPopup: MutableLiveData> = MutableLiveData(Some.None) - val selectPopup: LiveData> get() = _selectPopup + private val _selectPopup: MutableLiveData = MutableLiveData(null) + val selectPopup: LiveData = _selectPopup fun updateWatchStatus(status: WatchType) { @@ -885,23 +889,22 @@ class ResultViewModel2 : ViewModel() { } fun cancelLinks() { - println("called::cancelLinks") currentLoadLinkJob?.cancel() currentLoadLinkJob = null - _loadedLinks.postValue(Some.None) + _loadedLinks.postValue(null) } private fun postPopup(text: UiText, options: List, callback: suspend (Int?) -> Unit) { _selectPopup.postValue( - some(SelectPopup.SelectText( + SelectPopup.SelectText( text, options ) { value -> viewModelScope.launchSafe { - _selectPopup.postValue(Some.None) + _selectPopup.postValue(null) callback.invoke(value) } - }) + } ) } @@ -912,15 +915,15 @@ class ResultViewModel2 : ViewModel() { callback: suspend (Int?) -> Unit ) { _selectPopup.postValue( - some(SelectPopup.SelectArray( + SelectPopup.SelectArray( text, options, ) { value -> viewModelScope.launchSafe { - _selectPopup.value = Some.None + _selectPopup.postValue(null) callback.invoke(value) } - }) + } ) } @@ -988,7 +991,7 @@ class ResultViewModel2 : ViewModel() { val subs: MutableSet = mutableSetOf() fun updatePage() { if (isVisible && isActive) { - _loadedLinks.postValue(some(LinkProgress(links.size, subs.size))) + _loadedLinks.postValue(LinkProgress(links.size, subs.size)) } } try { @@ -1005,7 +1008,7 @@ class ResultViewModel2 : ViewModel() { } catch (e: Exception) { logError(e) } finally { - _loadedLinks.postValue(Some.None) + _loadedLinks.postValue(null) } return LinkLoadingResult(sortUrls(links), sortSubs(subs)) @@ -1233,6 +1236,7 @@ class ResultViewModel2 : ViewModel() { ) } } + ACTION_CLICK_DEFAULT -> { activity?.let { ctx -> if (ctx.isConnectedToChromecast()) { @@ -1249,6 +1253,7 @@ class ResultViewModel2 : ViewModel() { } } } + ACTION_SHOW_DESCRIPTION -> { _episodeSynopsis.postValue(click.data.description) } @@ -1286,9 +1291,11 @@ class ResultViewModel2 : ViewModel() { ) } } + ACTION_SHOW_TOAST -> { showToast(activity, R.string.play_episode_toast, Toast.LENGTH_SHORT) } + ACTION_DOWNLOAD_EPISODE -> { val response = currentResponse ?: return downloadEpisode( @@ -1303,6 +1310,7 @@ class ResultViewModel2 : ViewModel() { response.url ) } + ACTION_DOWNLOAD_MIRROR -> { val response = currentResponse ?: return acquireSingleLink( @@ -1332,6 +1340,7 @@ class ResultViewModel2 : ViewModel() { ) } } + ACTION_RELOAD_EPISODE -> { ioSafe { loadLinks( @@ -1342,6 +1351,7 @@ class ResultViewModel2 : ViewModel() { ) } } + ACTION_CHROME_CAST_MIRROR -> { acquireSingleLink( click.data, @@ -1351,6 +1361,7 @@ class ResultViewModel2 : ViewModel() { startChromecast(activity, click.data, result.links, result.subs, index) } } + ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( click.data, isCasting = true, @@ -1364,6 +1375,7 @@ class ResultViewModel2 : ViewModel() { logError(e) } } + ACTION_COPY_LINK -> { acquireSingleLink( click.data, @@ -1380,9 +1392,11 @@ class ResultViewModel2 : ViewModel() { showToast(act, R.string.copy_link_toast, Toast.LENGTH_SHORT) } } + ACTION_CHROME_CAST_EPISODE -> { startChromecast(activity, click.data) } + ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> { loadLinks(click.data, isVisible = true, isCasting = true) { links -> if (links.links.isEmpty()) { @@ -1397,6 +1411,7 @@ class ResultViewModel2 : ViewModel() { ) } } + ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink( click.data, isCasting = true, @@ -1413,6 +1428,7 @@ class ResultViewModel2 : ViewModel() { result.subs ) } + ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink( click.data, isCasting = true, @@ -1428,6 +1444,7 @@ class ResultViewModel2 : ViewModel() { result.subs ) } + ACTION_PLAY_EPISODE_IN_PLAYER -> { val data = currentResponse?.syncData?.toList() ?: emptyList() val list = @@ -1448,6 +1465,7 @@ class ResultViewModel2 : ViewModel() { ) ) } + ACTION_MARK_AS_WATCHED -> { val isWatched = DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched @@ -1672,10 +1690,10 @@ class ResultViewModel2 : ViewModel() { private fun postMovie() { val response = currentResponse - _episodes.postValue(ResourceSome.None) + _episodes.postValue(null) if (response == null) { - _movie.postValue(ResourceSome.None) + _movie.postValue(null) return } @@ -1692,11 +1710,11 @@ class ResultViewModel2 : ViewModel() { } ) val data = getMovie() - _episodes.postValue(ResourceSome.None) + _episodes.postValue(null) if (text == null || data == null) { - _movie.postValue(ResourceSome.None) + _movie.postValue(null) } else { - _movie.postValue(ResourceSome.Success(text to data)) + _movie.postValue(Resource.Success(text to data)) } } @@ -1705,14 +1723,14 @@ class ResultViewModel2 : ViewModel() { postMovie() } else { _episodes.postValue( - ResourceSome.Success( + Resource.Success( getEpisodes( currentIndex ?: return, currentRange ?: return ) ) ) - _movie.postValue(ResourceSome.None) + _movie.postValue(null) } postResume() } @@ -1755,14 +1773,14 @@ class ResultViewModel2 : ViewModel() { val size = currentEpisodes[indexer]?.size _episodesCountText.postValue( - some( - if (isMovie) null else - txt( - R.string.episode_format, - size, - txt(if (size == 1) R.string.episode else R.string.episodes), - ) - ) + + if (isMovie) null else + txt( + R.string.episode_format, + size, + txt(if (size == 1) R.string.episode else R.string.episodes), + ) + ) _selectedSeasonIndex.postValue( @@ -1770,29 +1788,29 @@ class ResultViewModel2 : ViewModel() { ) _selectedSeason.postValue( - some( - if (isMovie || currentSeasons.size <= 1) null else - when (indexer.season) { - 0 -> txt(R.string.no_season) - else -> { - val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames - val seasonData = seasonNames.getSeason(indexer.season) - // If displaySeason is null then only show the name! - if (seasonData?.name != null && seasonData.displaySeason == null) { - txt(seasonData.name) - } else { - val suffix = seasonData?.name?.let { " $it" } ?: "" - txt( - R.string.season_format, - txt(R.string.season), - seasonData?.displaySeason ?: indexer.season, - suffix - ) - } + if (isMovie || currentSeasons.size <= 1) null else + when (indexer.season) { + 0 -> txt(R.string.no_season) + else -> { + val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames + val seasonData = seasonNames.getSeason(indexer.season) + + // If displaySeason is null then only show the name! + if (seasonData?.name != null && seasonData.displaySeason == null) { + txt(seasonData.name) + } else { + val suffix = seasonData?.name?.let { " $it" } ?: "" + txt( + R.string.season_format, + txt(R.string.season), + seasonData?.displaySeason ?: indexer.season, + suffix + ) } } - ) + } + ) _selectedRangeIndex.postValue( @@ -1800,13 +1818,13 @@ class ResultViewModel2 : ViewModel() { ) _selectedRange.postValue( - some( - if (isMovie) null else if ((currentRanges[indexer]?.size ?: 0) > 1) { - txt(R.string.episodes_range, range.startEpisode, range.endEpisode) - } else { - null - } - ) + + if (isMovie) null else if ((currentRanges[indexer]?.size ?: 0) > 1) { + txt(R.string.episodes_range, range.startEpisode, range.endEpisode) + } else { + null + } + ) _selectedDubStatusIndex.postValue( @@ -1814,10 +1832,10 @@ class ResultViewModel2 : ViewModel() { ) _selectedDubStatus.postValue( - some( - if (isMovie || currentDubStatus.size <= 1) null else - txt(indexer.dubStatus) - ) + + if (isMovie || currentDubStatus.size <= 1) null else + txt(indexer.dubStatus) + ) currentId?.let { id -> @@ -1851,7 +1869,7 @@ class ResultViewModel2 : ViewModel() { } }*/ - _episodes.postValue(ResourceSome.Success(ret)) + _episodes.postValue(Resource.Success(ret)) } } @@ -1869,7 +1887,7 @@ class ResultViewModel2 : ViewModel() { } private suspend fun postEpisodes(loadResponse: LoadResponse, updateFillers: Boolean) { - _episodes.postValue(ResourceSome.Loading()) + _episodes.postValue(Resource.Loading()) val mainId = loadResponse.getId() currentId = mainId @@ -1924,6 +1942,7 @@ class ResultViewModel2 : ViewModel() { } episodes } + is TvSeriesLoadResponse -> { val episodes: MutableMap> = mutableMapOf() @@ -1968,6 +1987,7 @@ class ResultViewModel2 : ViewModel() { } episodes } + is MovieLoadResponse -> { singleMap( buildResultEpisode( @@ -1989,6 +2009,7 @@ class ResultViewModel2 : ViewModel() { ) ) } + is LiveStreamLoadResponse -> { singleMap( buildResultEpisode( @@ -2010,6 +2031,7 @@ class ResultViewModel2 : ViewModel() { ) ) } + is TorrentLoadResponse -> { singleMap( buildResultEpisode( @@ -2031,6 +2053,7 @@ class ResultViewModel2 : ViewModel() { ) ) } + else -> { mapOf() } @@ -2088,7 +2111,7 @@ class ResultViewModel2 : ViewModel() { } fun postResume() { - _resumeWatching.postValue(some(resume())) + _resumeWatching.postValue(resume()) } private fun resume(): ResumeWatchingStatus? { @@ -2196,6 +2219,7 @@ class ResultViewModel2 : ViewModel() { } } } + START_ACTION_LOAD_EP -> { val all = currentEpisodes.values.flatten() val episode = @@ -2227,7 +2251,7 @@ class ResultViewModel2 : ViewModel() { ) = ioSafe { _page.postValue(Resource.Loading(url)) - _episodes.postValue(ResourceSome.Loading()) + _episodes.postValue(Resource.Loading()) preferDubStatus = dubStatus currentShowFillers = showFillers @@ -2271,6 +2295,7 @@ class ResultViewModel2 : ViewModel() { is Resource.Failure -> { _page.postValue(data) } + is Resource.Success -> { if (!isActive) return@ioSafe val loadResponse = ioWork { @@ -2307,6 +2332,7 @@ class ResultViewModel2 : ViewModel() { if (!isActive) return@ioSafe handleAutoStart(activity, autostart) } + is Resource.Loading -> { debugException { "Invalid load result" } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt index 2e7ec529..bcf401ea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt @@ -1,12 +1,11 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton -import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.ResultSelectionBinding import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings typealias SelectData = Pair @@ -17,7 +16,9 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter Unit) : RecyclerView.Adapter?) { - setTextHtml(if (text is Some.Success) text.value else null) -} - -fun TextView?.setText(text: Some?) { - setText(if (text is Some.Success) text.value else null) -} \ No newline at end of file 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 649641c8..7fdd6e1d 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 @@ -78,7 +78,7 @@ class SearchAdapter( resView: AutofitRecyclerView ) : RecyclerView.ViewHolder(itemView) { - val cardView: ImageView = itemView.imageView + private val cardView: ImageView = itemView.imageView private val compactView = false//itemView.context.getGridIsCompact() private val coverHeight: Int = 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 b4a38216..e0f67d4a 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 @@ -33,6 +33,8 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent +import com.lagradost.cloudstream3.databinding.FragmentSearchBinding +import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe @@ -56,8 +58,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard -import kotlinx.android.synthetic.main.fragment_search.* -import kotlinx.android.synthetic.main.tvtypes_chips.* import java.util.concurrent.locks.ReentrantLock const val SEARCH_PREF_TAGS = "search_pref_tags" @@ -89,6 +89,7 @@ class SearchFragment : Fragment() { private val searchViewModel: SearchViewModel by activityViewModels() private var bottomSheetDialog: BottomSheetDialog? = null + var binding: FragmentSearchBinding? = null override fun onCreateView( inflater: LayoutInflater, @@ -99,18 +100,20 @@ class SearchFragment : Fragment() { WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE ) bottomSheetDialog?.ownShow() - return inflater.inflate( - if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search, - container, - false - ) + + val layout = if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search + + val root = inflater.inflate(layout, container, false) + binding = FragmentSearchBinding.bind(root) + + return root } private fun fixGrid() { activity?.getSpanCount()?.let { currentSpan = it } - search_autofit_results.spanCount = currentSpan + binding?.searchAutofitResults?.spanCount = currentSpan currentSpan = currentSpan HomeFragment.configEvent.invoke(currentSpan) } @@ -123,6 +126,7 @@ class SearchFragment : Fragment() { override fun onDestroyView() { hideKeyboard() bottomSheetDialog?.ownHide() + binding = null super.onDestroyView() } @@ -181,7 +185,7 @@ class SearchFragment : Fragment() { searchViewModel.reloadRepos() context?.filterProviderByPreferredMedia()?.let { validAPIs -> bindChips( - home_select_group, + binding?.tvtypesChipsScroll?.tvtypesChips, selectedSearchTypes, validAPIs.flatMap { api -> api.supportedTypes }.distinct() ) { list -> @@ -189,7 +193,7 @@ class SearchFragment : Fragment() { setKey(SEARCH_PREF_TAGS, selectedSearchTypes) selectedSearchTypes.clear() selectedSearchTypes.addAll(list) - search(main_search?.query?.toString()) + search(binding?.mainSearch?.query?.toString()) } } } @@ -199,24 +203,27 @@ class SearchFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(searchRoot) + fixPaddingStatusbar(binding?.searchRoot) fixGrid() reloadRepos() - val adapter: RecyclerView.Adapter? = activity?.let { - SearchAdapter( - ArrayList(), - search_autofit_results, - ) { callback -> - SearchHelper.handleSearchClickCallback(activity, callback) - } + binding?.apply { + val adapter: RecyclerView.Adapter? = + SearchAdapter( + ArrayList(), + searchAutofitResults, + ) { callback -> + SearchHelper.handleSearchClickCallback(activity, callback) + } + + + searchAutofitResults.adapter = adapter + searchLoadingBar.alpha = 0f } - search_autofit_results.adapter = adapter - search_loading_bar.alpha = 0f val searchExitIcon = - main_search.findViewById(androidx.appcompat.R.id.search_close_btn) + binding?.mainSearch?.findViewById(androidx.appcompat.R.id.search_close_btn) // val searchMagIcon = // main_search.findViewById(androidx.appcompat.R.id.search_mag_icon) //searchMagIcon.scaleX = 0.65f @@ -230,7 +237,7 @@ class SearchFragment : Fragment() { )!!.toMutableSet() } - search_filter.setOnClickListener { searchView -> + binding?.searchFilter?.setOnClickListener { searchView -> searchView?.context?.let { ctx -> val validAPIs = ctx.filterProviderByPreferredMedia(hasHomePageIsRequired = false) var currentValidApis = listOf() @@ -241,7 +248,13 @@ class SearchFragment : Fragment() { BottomSheetDialog(ctx) builder.behavior.state = BottomSheetBehavior.STATE_EXPANDED - builder.setContentView(R.layout.home_select_mainpage) + + val binding: HomeSelectMainpageBinding = HomeSelectMainpageBinding.inflate( + builder.layoutInflater, + null, + false + ) + builder.setContentView(binding.root) builder.show() builder.let { dialog -> val isMultiLang = ctx.getApiProviderLangSettings().let { set -> @@ -303,7 +316,7 @@ class SearchFragment : Fragment() { ?: mutableListOf(TvType.Movie, TvType.TvSeries) bindChips( - dialog.home_select_group, + binding.tvtypesChipsScroll.tvtypesChips, selectedSearchTypes, TvType.values().toList() ) { list -> @@ -343,15 +356,15 @@ class SearchFragment : Fragment() { ?: mutableListOf(TvType.Movie, TvType.TvSeries) if (isTrueTvSettings()) { - search_filter.isFocusable = true - search_filter.isFocusableInTouchMode = true + binding?.searchFilter?.isFocusable = true + binding?.searchFilter?.isFocusableInTouchMode = true } - main_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { search(query) - main_search?.let { + binding?.mainSearch?.let { hideKeyboard(it) } @@ -365,17 +378,17 @@ class SearchFragment : Fragment() { searchViewModel.clearSearch() searchViewModel.updateHistory() } - - search_history_holder?.isVisible = showHistory - - search_master_recycler?.isVisible = !showHistory && isAdvancedSearch - search_autofit_results?.isVisible = !showHistory && !isAdvancedSearch + binding?.apply { + searchHistoryHolder.isVisible = showHistory + searchMasterRecycler.isVisible = !showHistory && isAdvancedSearch + searchAutofitResults.isVisible = !showHistory && !isAdvancedSearch + } return true } }) - search_clear_call_history?.setOnClickListener { + binding?.searchClearCallHistory?.setOnClickListener { activity?.let { ctx -> val builder: AlertDialog.Builder = AlertDialog.Builder(ctx) val dialogClickListener = @@ -409,8 +422,8 @@ class SearchFragment : Fragment() { } observe(searchViewModel.currentHistory) { list -> - search_clear_call_history?.isVisible = list.isNotEmpty() - (search_history_recycler.adapter as? SearchHistoryAdaptor?)?.updateList(list) + binding?.searchClearCallHistory?.isVisible = list.isNotEmpty() + (binding?.searchHistoryRecycler?.adapter as? SearchHistoryAdaptor?)?.updateList(list) } searchViewModel.updateHistory() @@ -420,20 +433,20 @@ class SearchFragment : Fragment() { is Resource.Success -> { it.value.let { data -> if (data.isNotEmpty()) { - (search_autofit_results?.adapter as? SearchAdapter)?.updateList(data) + (binding?.searchAutofitResults?.adapter as? SearchAdapter)?.updateList(data) } } - searchExitIcon.alpha = 1f - search_loading_bar.alpha = 0f + searchExitIcon?.alpha = 1f + binding?.searchLoadingBar?.alpha = 0f } is Resource.Failure -> { // Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show() - searchExitIcon.alpha = 1f - search_loading_bar.alpha = 0f + searchExitIcon?.alpha = 1f + binding?.searchLoadingBar?.alpha = 0f } is Resource.Loading -> { - searchExitIcon.alpha = 0f - search_loading_bar.alpha = 1f + searchExitIcon?.alpha = 0f + binding?.searchLoadingBar?.alpha = 1f } } } @@ -443,7 +456,7 @@ class SearchFragment : Fragment() { try { // https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist listLock.lock() - (search_master_recycler?.adapter as ParentItemAdapter?)?.apply { + (binding?.searchMasterRecycler?.adapter as ParentItemAdapter?)?.apply { val newItems = list.map { ongoing -> val dataList = if (ongoing.data is Resource.Success) ongoing.data.value else ArrayList() @@ -490,8 +503,8 @@ class SearchFragment : Fragment() { SEARCH_HISTORY_OPEN -> { searchViewModel.clearSearch() if (searchItem.type.isNotEmpty()) - updateChips(home_select_group, searchItem.type.toMutableList()) - main_search?.setQuery(searchItem.searchText, true) + updateChips(binding?.tvtypesChipsScroll?.tvtypesChips, searchItem.type.toMutableList()) + binding?.mainSearch?.setQuery(searchItem.searchText, true) } SEARCH_HISTORY_REMOVE -> { removeKey(SEARCH_HISTORY_KEY, searchItem.key) @@ -503,20 +516,23 @@ class SearchFragment : Fragment() { } } - search_history_recycler?.adapter = historyAdapter - search_history_recycler?.layoutManager = GridLayoutManager(context, 1) + binding?.apply { + searchHistoryRecycler.adapter = historyAdapter + searchHistoryRecycler.layoutManager = GridLayoutManager(context, 1) - search_master_recycler?.adapter = masterAdapter - search_master_recycler?.layoutManager = GridLayoutManager(context, 1) + searchMasterRecycler.adapter = masterAdapter + searchMasterRecycler.layoutManager = GridLayoutManager(context, 1) - // Automatically search the specified query, this allows the app search to launch from intent - arguments?.getString(SEARCH_QUERY)?.let { query -> - if (query.isBlank()) return@let - main_search?.setQuery(query, true) - // Clear the query as to not make it request the same query every time the page is opened - arguments?.putString(SEARCH_QUERY, null) + // Automatically search the specified query, this allows the app search to launch from intent + arguments?.getString(SEARCH_QUERY)?.let { query -> + if (query.isBlank()) return@let + mainSearch.setQuery(query, true) + // Clear the query as to not make it request the same query every time the page is opened + arguments?.putString(SEARCH_QUERY, null) + } } + // SubtitlesFragment.push(activity) //searchViewModel.search("iron man") //(activity as AppCompatActivity).loadResult("https://shiro.is/overlord-dubbed", "overlord-dubbed", "Shiro") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt index 8132301b..0a2ecb81 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt @@ -10,7 +10,8 @@ import androidx.recyclerview.widget.RecyclerView import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType -import kotlinx.android.synthetic.main.search_history_item.view.* +import com.lagradost.cloudstream3.databinding.AccountSingleBinding +import com.lagradost.cloudstream3.databinding.SearchHistoryItemBinding data class SearchHistoryItem( @JsonProperty("searchedAt") val searchedAt: Long, @@ -34,8 +35,7 @@ class SearchHistoryAdaptor( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return CardViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.search_history_item, parent, false), + SearchHistoryItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), clickCallback, ) } @@ -65,22 +65,24 @@ class SearchHistoryAdaptor( class CardViewHolder constructor( - itemView: View, + val binding: SearchHistoryItemBinding, private val clickCallback: (SearchHistoryCallback) -> Unit, ) : - RecyclerView.ViewHolder(itemView) { - private val removeButton: ImageView = itemView.home_history_remove - private val openButton: View = itemView.home_history_tab - private val title: TextView = itemView.home_history_title + RecyclerView.ViewHolder(binding.root) { + // private val removeButton: ImageView = itemView.home_history_remove + // private val openButton: View = itemView.home_history_tab + // private val title: TextView = itemView.home_history_title fun bind(card: SearchHistoryItem) { - title.text = card.searchText + binding.apply { + homeHistoryTitle.text = card.searchText - removeButton.setOnClickListener { - clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_REMOVE)) - } - openButton.setOnClickListener { - clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_OPEN)) + homeHistoryRemove.setOnClickListener { + clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_REMOVE)) + } + homeHistoryTab.setOnClickListener { + clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_OPEN)) + } } } } 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 3447ee32..69812f22 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,7 +1,6 @@ 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 @@ -10,14 +9,29 @@ 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.AnimeSearchResponse +import com.lagradost.cloudstream3.DubStatus +import com.lagradost.cloudstream3.LiveSearchResponse +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SearchQuality +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.isMovieType import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.home_result_grid.view.* +import kotlinx.android.synthetic.main.home_result_grid.view.background_card +import kotlinx.android.synthetic.main.home_result_grid.view.imageText +import kotlinx.android.synthetic.main.home_result_grid.view.imageView +import kotlinx.android.synthetic.main.home_result_grid.view.search_item_download_play +import kotlinx.android.synthetic.main.home_result_grid.view.text_flag +import kotlinx.android.synthetic.main.home_result_grid.view.text_is_dub +import kotlinx.android.synthetic.main.home_result_grid.view.text_is_sub +import kotlinx.android.synthetic.main.home_result_grid.view.text_quality +import kotlinx.android.synthetic.main.home_result_grid.view.title_shadow +import kotlinx.android.synthetic.main.home_result_grid.view.watchProgress object SearchResultBuilder { private val showCache: MutableMap = mutableMapOf() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt index e879f0df..1dc79dc0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt @@ -3,11 +3,10 @@ package com.lagradost.cloudstream3.ui.settings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.AccountSingleBinding import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.utils.UIHelper.setImage @@ -15,14 +14,15 @@ class AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.Lo class AccountAdapter( val cardList: List, - val layout: Int = R.layout.account_single, private val clickCallback: (AccountClickCallback) -> Unit ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return CardViewHolder( - LayoutInflater.from(parent.context).inflate(layout, parent, false), clickCallback + AccountSingleBinding.inflate(LayoutInflater.from(parent.context), parent, false), //LayoutInflater.from(parent.context).inflate(layout, parent, false), + + clickCallback ) } @@ -43,18 +43,18 @@ class AccountAdapter( } class CardViewHolder - constructor(itemView: View, private val clickCallback: (AccountClickCallback) -> Unit) : - RecyclerView.ViewHolder(itemView) { - private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! - private val accountName: TextView = itemView.findViewById(R.id.account_name)!! + constructor(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) : + RecyclerView.ViewHolder(binding.root) { + // private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! + // private val accountName: TextView = itemView.findViewById(R.id.account_name)!! fun bind(card: AuthAPI.LoginInfo) { // just in case name is null account index will show, should never happened - accountName.text = card.name ?: "%s %d".format( - accountName.context.getString(R.string.account), + binding.accountName.text = card.name ?: "%s %d".format( + binding.accountName.context.getString(R.string.account), card.accountIndex ) - pfp.isVisible = pfp.setImage(card.profilePicture) + binding.accountProfilePicture.isVisible = binding.accountProfilePicture.setImage(card.profilePicture) itemView.setOnClickListener { clickCallback.invoke(AccountClickCallback(0, itemView, card)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 1ef3cb55..acf715b3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -96,7 +96,7 @@ class SettingsAccount : PreferenceFragmentCompat() { } } api.accountIndex = ogIndex - val adapter = AccountAdapter(items, R.layout.account_single) { + val adapter = AccountAdapter(items) { dialog?.dismissSafe(activity) api.changeAccount(it.card.accountIndex) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 40c996cc..453f93be 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -62,7 +62,7 @@ class SettingsFragment : Fragment() { activity?.onBackPressed() } } - context.fixPaddingStatusbar(settings_toolbar) + fixPaddingStatusbar(settings_toolbar) } fun Fragment?.setUpToolbar(@StringRes title: Int) { @@ -74,7 +74,7 @@ class SettingsFragment : Fragment() { activity?.onBackPressed() } } - context.fixPaddingStatusbar(settings_toolbar) + fixPaddingStatusbar(settings_toolbar) } fun getFolderSize(dir: File): Long { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 045ed92d..75ff8305 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -18,8 +18,8 @@ import androidx.navigation.fragment.findNavController import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.mvvm.Some import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings @@ -97,6 +97,7 @@ class ExtensionsFragment : Fragment() { extensionViewModel.loadRepositories() } } + DialogInterface.BUTTON_NEGATIVE -> {} } } @@ -138,29 +139,26 @@ class ExtensionsFragment : Fragment() { // } // } - observe(extensionViewModel.pluginStats) { - when (it) { - is Some.Success -> { - val value = it.value + observeNullable(extensionViewModel.pluginStats) { value -> + if (value == null) { + plugin_storage_appbar?.isVisible = false - plugin_storage_appbar?.isVisible = true - if (value.total == 0) { - plugin_download?.setLayoutWidth(1) - plugin_disabled?.setLayoutWidth(0) - plugin_not_downloaded?.setLayoutWidth(0) - } else { - plugin_download?.setLayoutWidth(value.downloaded) - plugin_disabled?.setLayoutWidth(value.disabled) - plugin_not_downloaded?.setLayoutWidth(value.notDownloaded) - } - plugin_not_downloaded_txt.setText(value.notDownloadedText) - plugin_disabled_txt.setText(value.disabledText) - plugin_download_txt.setText(value.downloadedText) - } - is Some.None -> { - plugin_storage_appbar?.isVisible = false - } + return@observeNullable } + + plugin_storage_appbar?.isVisible = true + if (value.total == 0) { + plugin_download?.setLayoutWidth(1) + plugin_disabled?.setLayoutWidth(0) + plugin_not_downloaded?.setLayoutWidth(0) + } else { + plugin_download?.setLayoutWidth(value.downloaded) + plugin_disabled?.setLayoutWidth(value.disabled) + plugin_not_downloaded?.setLayoutWidth(value.notDownloaded) + } + plugin_not_downloaded_txt.setText(value.notDownloadedText) + plugin_disabled_txt.setText(value.disabledText) + plugin_download_txt.setText(value.downloadedText) } plugin_storage_appbar?.setOnClickListener { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt index 63ed5357..866d167c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt @@ -7,7 +7,6 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.amap -import com.lagradost.cloudstream3.mvvm.Some import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager.getPluginsOnline @@ -40,8 +39,8 @@ class ExtensionsViewModel : ViewModel() { private val _repositories = MutableLiveData>() val repositories: LiveData> = _repositories - private val _pluginStats: MutableLiveData> = MutableLiveData(Some.None) - val pluginStats: LiveData> = _pluginStats + private val _pluginStats: MutableLiveData = MutableLiveData(null) + val pluginStats: LiveData = _pluginStats //TODO CACHE GET REQUESTS // DO not use viewModelScope.launchSafe, it will ANR on slow internet @@ -78,7 +77,7 @@ class ExtensionsViewModel : ViewModel() { debugAssert({ stats.downloaded + stats.notDownloaded + stats.disabled != stats.total }) { "downloaded(${stats.downloaded}) + notDownloaded(${stats.notDownloaded}) + disabled(${stats.disabled}) != total(${stats.total})" } - _pluginStats.postValue(Some.Success(stats)) + _pluginStats.postValue(stats) } private fun repos() = (getKey>(REPOSITORIES_KEY) 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 d328d226..1a6215db 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 @@ -12,6 +12,7 @@ import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.databinding.FragmentPluginsBinding import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings @@ -20,9 +21,6 @@ import com.lagradost.cloudstream3.ui.settings.appLanguages import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.android.synthetic.main.fragment_plugins.* -import kotlinx.android.synthetic.main.tvtypes_chips.* -import kotlinx.android.synthetic.main.tvtypes_chips_scroll.* const val PLUGINS_BUNDLE_NAME = "name" const val PLUGINS_BUNDLE_URL = "url" @@ -33,11 +31,19 @@ class PluginsFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { - return inflater.inflate(R.layout.fragment_plugins, container, false) + ): View { + val localBinding = FragmentPluginsBinding.inflate(inflater,container,false) + binding = localBinding + return localBinding.root//inflater.inflate(R.layout.fragment_plugins, container, false) + } + + override fun onDestroyView() { + binding = null + super.onDestroyView() } private val pluginViewModel: PluginsViewModel by activityViewModels() + var binding: FragmentPluginsBinding? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -66,8 +72,8 @@ class PluginsFragment : Fragment() { } setUpToolbar(name) - - settings_toolbar?.setOnMenuItemClickListener { menuItem -> + binding?.settingsToolbar?.apply { + setOnMenuItemClickListener { menuItem -> when (menuItem?.itemId) { R.id.download_all -> { PluginsViewModel.downloadAll(activity, url, pluginViewModel) @@ -99,67 +105,69 @@ class PluginsFragment : Fragment() { } val searchView = - settings_toolbar?.menu?.findItem(R.id.search_button)?.actionView as? SearchView + menu?.findItem(R.id.search_button)?.actionView as? SearchView // Don't go back if active query - settings_toolbar?.setNavigationOnClickListener { + setNavigationOnClickListener { if (searchView?.isIconified == false) { searchView.isIconified = true } else { activity?.onBackPressed() } } + searchView?.setOnQueryTextFocusChangeListener { _, hasFocus -> + if (!hasFocus) pluginViewModel.search(null) + } + searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + pluginViewModel.search(query) + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + pluginViewModel.search(newText) + return true + } + }) + } // searchView?.onActionViewCollapsed = { // pluginViewModel.search(null) // } // Because onActionViewCollapsed doesn't wanna work we need this workaround :( - searchView?.setOnQueryTextFocusChangeListener { _, hasFocus -> - if (!hasFocus) pluginViewModel.search(null) - } - - searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String?): Boolean { - pluginViewModel.search(query) - return true - } - - override fun onQueryTextChange(newText: String?): Boolean { - pluginViewModel.search(newText) - return true - } - }) - plugin_recycler_view?.adapter = + + binding?.pluginRecyclerView?.adapter = PluginAdapter { pluginViewModel.handlePluginAction(activity, url, it, isLocal) } if (isTvSettings()) { // Scrolling down does not reveal the whole RecyclerView on TV, add to bypass that. - plugin_recycler_view?.setPadding(0, 0, 0, 200.toPx) + binding?.pluginRecyclerView?.setPadding(0, 0, 0, 200.toPx) } observe(pluginViewModel.filteredPlugins) { (scrollToTop, list) -> - (plugin_recycler_view?.adapter as? PluginAdapter)?.updateList(list) + (binding?.pluginRecyclerView?.adapter as? PluginAdapter)?.updateList(list) if (scrollToTop) - plugin_recycler_view?.scrollToPosition(0) + binding?.pluginRecyclerView?.scrollToPosition(0) } if (isLocal) { // No download button and no categories on local - settings_toolbar?.menu?.findItem(R.id.download_all)?.isVisible = false - settings_toolbar?.menu?.findItem(R.id.lang_filter)?.isVisible = false + binding?.settingsToolbar?.menu?.findItem(R.id.download_all)?.isVisible = false + binding?.settingsToolbar?.menu?.findItem(R.id.lang_filter)?.isVisible = false pluginViewModel.updatePluginListLocal() - tv_types_scroll_view?.isVisible = false + + binding?.tvtypesChipsScroll?.root?.isVisible = false } else { pluginViewModel.updatePluginList(context, url) - tv_types_scroll_view?.isVisible = true + binding?.tvtypesChipsScroll?.root?.isVisible = true - bindChips(home_select_group, emptyList(), TvType.values().toList()) { list -> + bindChips(binding?.tvtypesChipsScroll?.tvtypesChips, emptyList(), TvType.values().toList()) { list -> pluginViewModel.tvTypes.clear() pluginViewModel.tvTypes.addAll(list.map { it.name }) pluginViewModel.updateFilteredPlugins() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt index b7d2fff6..138a31a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt @@ -8,21 +8,16 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings -import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentSetupExtensionsBinding +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel import com.lagradost.cloudstream3.ui.settings.extensions.RepoAdapter import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import kotlinx.android.synthetic.main.fragment_extensions.blank_repo_screen -import kotlinx.android.synthetic.main.fragment_extensions.repo_recycler_view -import kotlinx.android.synthetic.main.fragment_setup_media.next_btt -import kotlinx.android.synthetic.main.fragment_setup_media.prev_btt -import kotlinx.android.synthetic.main.fragment_setup_media.setup_root class SetupFragmentExtensions : Fragment() { @@ -39,13 +34,24 @@ class SetupFragmentExtensions : Fragment() { } } + var binding: FragmentSetupExtensionsBinding? = null + + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_setup_extensions, container, false) + ): View { + val localBinding = FragmentSetupExtensionsBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.fragment_setup_extensions, container, false) } + override fun onResume() { super.onResume() afterRepositoryLoadedEvent += ::setRepositories @@ -60,12 +66,12 @@ class SetupFragmentExtensions : Fragment() { main { val repositories = RepositoryManager.getRepositories() + PREBUILT_REPOSITORIES val hasRepos = repositories.isNotEmpty() - repo_recycler_view?.isVisible = hasRepos - blank_repo_screen?.isVisible = !hasRepos + binding?.repoRecyclerView?.isVisible = hasRepos + binding?.blankRepoScreen?.isVisible = !hasRepos // view_public_repositories_button?.isVisible = hasRepos if (hasRepos) { - repo_recycler_view?.adapter = RepoAdapter(true, {}, { + binding?.repoRecyclerView?.adapter = RepoAdapter(true, {}, { PluginsViewModel.downloadAll(activity, it.url, null) }).apply { updateList(repositories) } } @@ -80,39 +86,40 @@ class SetupFragmentExtensions : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(setup_root) + fixPaddingStatusbar(binding?.setupRoot) val isSetup = arguments?.getBoolean(SETUP_EXTENSION_BUNDLE_IS_SETUP) ?: false // view_public_repositories_button?.setOnClickListener { // openBrowser(PUBLIC_REPOSITORIES_LIST, isTvSettings(), this) // } - with(context) { - if (this == null) return + normalSafeApiCall { + // val ctx = context ?: return@normalSafeApiCall setRepositories() + binding?.apply { + if (!isSetup) { + nextBtt.setText(R.string.setup_done) + } + prevBtt.isVisible = isSetup - if (!isSetup) { - next_btt.setText(R.string.setup_done) - } - prev_btt?.isVisible = isSetup + nextBtt.setOnClickListener { + // Continue setup + if (isSetup) + if ( + // If any available languages + apis.distinctBy { it.lang }.size > 1 + ) { + findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages) + } else { + findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_media) + } + else + findNavController().navigate(R.id.navigation_home) + } - next_btt?.setOnClickListener { - // Continue setup - if (isSetup) - if ( - // If any available languages - apis.distinctBy { it.lang }.size > 1 - ) { - findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages) - } else { - findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_media) - } - else - findNavController().navigate(R.id.navigation_home) - } - - prev_btt?.setOnClickListener { - findNavController().navigate(R.id.navigation_setup_language) + prevBtt.setOnClickListener { + findNavController().navigate(R.id.navigation_setup_language) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt index 80db59ee..5c473b73 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt @@ -13,40 +13,49 @@ import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentSetupLanguageBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.ui.settings.appLanguages import com.lagradost.cloudstream3.ui.settings.getCurrentLocale import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import kotlinx.android.synthetic.main.fragment_setup_language.* -import kotlinx.android.synthetic.main.fragment_setup_media.listview1 -import kotlinx.android.synthetic.main.fragment_setup_media.next_btt const val HAS_DONE_SETUP_KEY = "HAS_DONE_SETUP" class SetupFragmentLanguage : Fragment() { + var binding: FragmentSetupLanguageBinding? = null + + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_setup_language, container, false) + ): View { + val localBinding = FragmentSetupLanguageBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.fragment_setup_language, container, false) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(setup_root) // We don't want a crash for all users normalSafeApiCall { - with(context) { - if (this == null) return@normalSafeApiCall - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + fixPaddingStatusbar(binding?.setupRoot) - val arrayAdapter = - ArrayAdapter(this, R.layout.sort_bottom_single_choice) + val ctx = context ?: return@normalSafeApiCall + val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) + val arrayAdapter = + ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) + + binding?.apply { // Icons may crash on some weird android versions? normalSafeApiCall { val drawable = when { @@ -54,10 +63,10 @@ class SetupFragmentLanguage : Fragment() { BuildConfig.BUILD_TYPE == "prerelease" -> R.drawable.cloud_2_gradient_beta else -> R.drawable.cloud_2_gradient } - app_icon_image?.setImageDrawable(ContextCompat.getDrawable(this, drawable)) + appIconImage.setImageDrawable(ContextCompat.getDrawable(ctx, drawable)) } - val current = getCurrentLocale(this) + val current = getCurrentLocale(ctx) val languageCodes = appLanguages.map { it.third } val languageNames = appLanguages.map { (emoji, name, iso) -> val flag = emoji.ifBlank { SubtitleHelper.getFlagFromIso(iso) ?: "ERROR" } @@ -66,18 +75,19 @@ class SetupFragmentLanguage : Fragment() { val index = languageCodes.indexOf(current) arrayAdapter.addAll(languageNames) - listview1?.adapter = arrayAdapter - listview1?.choiceMode = AbsListView.CHOICE_MODE_SINGLE - listview1?.setItemChecked(index, true) + listview1.adapter = arrayAdapter + listview1.choiceMode = AbsListView.CHOICE_MODE_SINGLE + listview1.setItemChecked(index, true) - listview1?.setOnItemClickListener { _, _, position, _ -> + listview1.setOnItemClickListener { _, _, position, _ -> val code = languageCodes[position] CommonActivity.setLocale(activity, code) - settingsManager.edit().putString(getString(R.string.locale_key), code).apply() + settingsManager.edit().putString(getString(R.string.locale_key), code) + .apply() activity?.recreate() } - next_btt?.setOnClickListener { + nextBtt.setOnClickListener { // If no plugins go to plugins page val nextDestination = if ( PluginManager.getPluginsOnline().isEmpty() @@ -92,10 +102,11 @@ class SetupFragmentLanguage : Fragment() { ) } - skip_btt?.setOnClickListener { + skipBtt.setOnClickListener { findNavController().navigate(R.id.navigation_home) } } + } } 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 50fb37d6..98803818 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 @@ -10,30 +10,39 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentSetupLayoutBinding +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import kotlinx.android.synthetic.main.fragment_setup_layout.* -import kotlinx.android.synthetic.main.fragment_setup_media.listview1 -import kotlinx.android.synthetic.main.fragment_setup_media.next_btt -import kotlinx.android.synthetic.main.fragment_setup_media.prev_btt -import kotlinx.android.synthetic.main.fragment_setup_media.setup_root import org.acra.ACRA class SetupFragmentLayout : Fragment() { + + var binding: FragmentSetupLayoutBinding? = null + + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_setup_layout, container, false) + ): View { + val localBinding = FragmentSetupLayoutBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.fragment_setup_layout, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(setup_root) + fixPaddingStatusbar(binding?.setupRoot) - with(context) { - if (this == null) return - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + normalSafeApiCall { + val ctx = context ?: return@normalSafeApiCall + + val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) val prefNames = resources.getStringArray(R.array.app_layout) val prefValues = resources.getIntArray(R.array.app_layout_values) @@ -42,48 +51,48 @@ class SetupFragmentLayout : Fragment() { settingsManager.getInt(getString(R.string.app_layout_key), -1) val arrayAdapter = - ArrayAdapter(this, R.layout.sort_bottom_single_choice) + ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) arrayAdapter.addAll(prefNames.toList()) - listview1?.adapter = arrayAdapter - listview1?.choiceMode = AbsListView.CHOICE_MODE_SINGLE - listview1?.setItemChecked( - prefValues.indexOf(currentLayout), true - ) - - listview1?.setOnItemClickListener { _, _, position, _ -> - settingsManager.edit() - .putInt(getString(R.string.app_layout_key), prefValues[position]) - .apply() - activity?.recreate() - } - - acra_switch?.setOnCheckedChangeListener { _, enableCrashReporting -> - // Use same pref as in settings - settingsManager.edit().putBoolean(ACRA.PREF_DISABLE_ACRA, !enableCrashReporting) - .apply() - val text = - if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on - crash_reporting_text?.text = getText(text) - } - - val enableCrashReporting = !settingsManager.getBoolean(ACRA.PREF_DISABLE_ACRA, true) - acra_switch.isChecked = enableCrashReporting - crash_reporting_text.text = - getText( - if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on + binding?.apply { + listview1.adapter = arrayAdapter + listview1.choiceMode = AbsListView.CHOICE_MODE_SINGLE + listview1.setItemChecked( + prefValues.indexOf(currentLayout), true ) + listview1.setOnItemClickListener { _, _, position, _ -> + settingsManager.edit() + .putInt(getString(R.string.app_layout_key), prefValues[position]) + .apply() + activity?.recreate() + } + acraSwitch.setOnCheckedChangeListener { _, enableCrashReporting -> + // Use same pref as in settings + settingsManager.edit().putBoolean(ACRA.PREF_DISABLE_ACRA, !enableCrashReporting) + .apply() + val text = + if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on + crashReportingText.text = getText(text) + } - next_btt?.setOnClickListener { - findNavController().navigate(R.id.navigation_home) - } + val enableCrashReporting = !settingsManager.getBoolean(ACRA.PREF_DISABLE_ACRA, true) - prev_btt?.setOnClickListener { - findNavController().popBackStack() + acraSwitch.isChecked = enableCrashReporting + crashReportingText.text = + getText( + if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on + ) + + + nextBtt.setOnClickListener { + findNavController().navigate(R.id.navigation_home) + } + + prevBtt.setOnClickListener { + findNavController().popBackStack() + } } } } - - } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index 257ce5c1..6916cafe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -10,72 +10,85 @@ import androidx.core.util.forEach import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.utils.DataStore.removeKey -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API +import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import kotlinx.android.synthetic.main.fragment_setup_media.* +import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API class SetupFragmentMedia : Fragment() { + var binding: FragmentSetupMediaBinding? = null + + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_setup_media, container, false) + ): View { + val localBinding = FragmentSetupMediaBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.fragment_setup_media, container, false) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(setup_root) + normalSafeApiCall { + fixPaddingStatusbar(binding?.setupRoot) - with(context) { - if (this == null) return - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val ctx = context ?: return@normalSafeApiCall + val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) val arrayAdapter = - ArrayAdapter(this, R.layout.sort_bottom_single_choice) + ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) val names = enumValues().sorted().map { it.name } val selected = mutableListOf() arrayAdapter.addAll(names) - listview1?.let { - it.adapter = arrayAdapter - it.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE + binding?.apply { + listview1.let { + it.adapter = arrayAdapter + it.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE - it.setOnItemClickListener { _, _, _, _ -> - it.checkedItemPositions?.forEach { key, value -> - if (value) { - selected.add(key) - } else { - selected.remove(key) + it.setOnItemClickListener { _, _, _, _ -> + it.checkedItemPositions?.forEach { key, value -> + if (value) { + selected.add(key) + } else { + selected.remove(key) + } } + val prefValues = selected.mapNotNull { pos -> + val item = + it.getItemAtPosition(pos)?.toString() ?: return@mapNotNull null + val itemVal = TvType.valueOf(item) + itemVal.ordinal.toString() + }.toSet() + settingsManager.edit() + .putStringSet(getString(R.string.prefer_media_type_key), prefValues) + .apply() + + // Regenerate set homepage + removeKey(USER_SELECTED_HOMEPAGE_API) } - val prefValues = selected.mapNotNull { pos -> - val item = it.getItemAtPosition(pos)?.toString() ?: return@mapNotNull null - val itemVal = TvType.valueOf(item) - itemVal.ordinal.toString() - }.toSet() - settingsManager.edit() - .putStringSet(getString(R.string.prefer_media_type_key), prefValues) - .apply() - - // Regenerate set homepage - removeKey(USER_SELECTED_HOMEPAGE_API) } - } - next_btt?.setOnClickListener { - findNavController().navigate(R.id.navigation_setup_media_to_navigation_setup_layout) - } + nextBtt.setOnClickListener { + findNavController().navigate(R.id.navigation_setup_media_to_navigation_setup_layout) + } - prev_btt?.setOnClickListener { - findNavController().popBackStack() + prevBtt.setOnClickListener { + findNavController().popBackStack() + } } } } - - } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index 51abee90..8637fc99 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -14,31 +14,43 @@ import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentSetupProviderLanguagesBinding +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import kotlinx.android.synthetic.main.fragment_setup_media.* class SetupFragmentProviderLanguage : Fragment() { + var binding: FragmentSetupProviderLanguagesBinding? = null + + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_setup_provider_languages, container, false) + ): View { + val localBinding = FragmentSetupProviderLanguagesBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.fragment_setup_provider_languages, container, false) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(setup_root) + fixPaddingStatusbar(binding?.setupRoot) - with(context) { - if (this == null) return - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + normalSafeApiCall { + val ctx = context ?: return@normalSafeApiCall + + val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) val arrayAdapter = - ArrayAdapter(this, R.layout.sort_bottom_single_choice) + ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) - val current = this.getApiProviderLangSettings() + val current = ctx.getApiProviderLangSettings() val langs = APIHolder.apis.map { it.lang }.toSet() .sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName @@ -56,31 +68,31 @@ class SetupFragmentProviderLanguage : Fragment() { } arrayAdapter.addAll(languageNames) - - listview1?.adapter = arrayAdapter - listview1?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE + binding?.apply { + listview1.adapter = arrayAdapter + listview1.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE currentList.forEach { listview1.setItemChecked(it, true) } - listview1?.setOnItemClickListener { _, _, _, _ -> + listview1.setOnItemClickListener { _, _, _, _ -> val currentLanguages = mutableListOf() - listview1?.checkedItemPositions?.forEach { key, value -> + listview1.checkedItemPositions?.forEach { key, value -> if (value) currentLanguages.add(langs[key]) } settingsManager.edit().putStringSet( - this.getString(R.string.provider_lang_key), + ctx.getString(R.string.provider_lang_key), currentLanguages.toSet() ).apply() } - next_btt?.setOnClickListener { + nextBtt.setOnClickListener { findNavController().navigate(R.id.navigation_setup_provider_languages_to_navigation_setup_media) } - prev_btt?.setOnClickListener { + prevBtt.setOnClickListener { findNavController().popBackStack() - } + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt index 83d134cb..40bf8417 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt @@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.ChromecastSubtitleSettingsBinding import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.Event @@ -31,7 +32,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage -import kotlinx.android.synthetic.main.subtitle_settings.* const val CHROME_SUBTITLE_KEY = "chome_subtitle_settings" @@ -137,12 +137,21 @@ class ChromecastSubtitlesFragment : Fragment() { //subtitle_text?.setStyle(fromSaveToStyle(state)) } + var binding : ChromecastSubtitleSettingsBinding? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { - return inflater.inflate(R.layout.chromecast_subtitle_settings, container, false) + ): View { + val localBinding = ChromecastSubtitleSettingsBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root//inflater.inflate(R.layout.chromecast_subtitle_settings, container, false) + } + + override fun onDestroyView() { + binding = null + super.onDestroyView() } private lateinit var state: SaveChromeCaptionStyle @@ -159,7 +168,7 @@ class ChromecastSubtitlesFragment : Fragment() { onColorSelectedEvent += ::onColorSelected onDialogDismissedEvent += ::onDialogDismissed - context?.fixPaddingStatusbar(subs_root) + fixPaddingStatusbar(binding?.subsRoot) state = getCurrentSavedStyle() context?.updateState() @@ -190,17 +199,20 @@ class ChromecastSubtitlesFragment : Fragment() { } } - subs_text_color.setup(0) - subs_outline_color.setup(1) - subs_background_color.setup(2) + binding?.apply { + subsTextColor.setup(0) + subsOutlineColor.setup(1) + subsBackgroundColor.setup(2) + } + val dismissCallback = { if (hide) activity?.hideSystemUI() } - subs_edge_type.setFocusableInTv() - subs_edge_type.setOnClickListener { textView -> + binding?.subsEdgeType?.setFocusableInTv() + binding?.subsEdgeType?.setOnClickListener { textView -> val edgeTypes = listOf( Pair( EDGE_TYPE_NONE, @@ -237,15 +249,15 @@ class ChromecastSubtitlesFragment : Fragment() { } } - subs_edge_type.setOnLongClickListener { + binding?.subsEdgeType?.setOnLongClickListener { state.edgeType = defaultState.edgeType it.context.updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } - subs_font_size.setFocusableInTv() - subs_font_size.setOnClickListener { textView -> + binding?.subsFontSize?.setFocusableInTv() + binding?.subsFontSize?.setOnClickListener { textView -> val fontSizes = listOf( Pair(0.75f, "75%"), Pair(0.80f, "80%"), @@ -278,24 +290,26 @@ class ChromecastSubtitlesFragment : Fragment() { } } - subs_font_size.setOnLongClickListener { _ -> + binding?.subsFontSize?.setOnLongClickListener { _ -> state.fontScale = defaultState.fontScale //textView.context.updateState() // font size not changed showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } - subs_font.setFocusableInTv() - subs_font.setOnClickListener { textView -> + + + binding?.subsFont?.setFocusableInTv() + binding?.subsFont?.setOnClickListener { textView -> val fontTypes = listOf( - Pair(null, textView.context.getString(R.string.normal)), - Pair("Droid Sans", "Droid Sans"), - Pair("Droid Sans Mono", "Droid Sans Mono"), - Pair("Droid Serif Regular", "Droid Serif Regular"), - Pair("Cutive Mono", "Cutive Mono"), - Pair("Short Stack", "Short Stack"), - Pair("Quintessential", "Quintessential"), - Pair("Alegreya Sans SC", "Alegreya Sans SC"), + null to textView.context.getString(R.string.normal), + "Droid Sans" to "Droid Sans", + "Droid Sans Mono" to "Droid Sans Mono", + "Droid Serif Regular" to "Droid Serif Regular", + "Cutive Mono" to "Cutive Mono", + "Short Stack" to "Short Stack", + "Quintessential" to "Quintessential", + "Alegreya Sans SC" to "Alegreya Sans SC", ) //showBottomDialog @@ -310,35 +324,35 @@ class ChromecastSubtitlesFragment : Fragment() { textView.context.updateState() } } - - subs_font.setOnLongClickListener { textView -> + binding?.subsFont?.setOnLongClickListener { textView -> state.fontFamily = defaultState.fontFamily textView.context.updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } - cancel_btt.setOnClickListener { + binding?.cancelBtt?.setOnClickListener { activity?.popCurrentPage() } - apply_btt.setOnClickListener { + binding?.applyBtt?.setOnClickListener { it.context.saveStyle(state) applyStyleEvent.invoke(state) //it.context.fromSaveToStyle(state) activity?.popCurrentPage() } - - subtitle_text.setCues( - listOf( - Cue.Builder() - .setTextSize( - getPixels(TypedValue.COMPLEX_UNIT_SP, 25.0f).toFloat(), - Cue.TEXT_SIZE_TYPE_ABSOLUTE - ) - .setText(subtitle_text.context.getString(R.string.subtitles_example_text)) - .build() + binding?.subtitleText?.apply { + setCues( + listOf( + Cue.Builder() + .setTextSize( + getPixels(TypedValue.COMPLEX_UNIT_SP, 25.0f).toFloat(), + Cue.TEXT_SIZE_TYPE_ABSOLUTE + ) + .setText(context.getString(R.string.subtitles_example_text)) + .build() + ) ) - ) + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index ff0e0e82..8db205ef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -238,7 +238,7 @@ class SubtitlesFragment : Fragment() { context?.getExternalFilesDir(null)?.absolutePath.toString() + "/Fonts" ) - context?.fixPaddingStatusbar(subs_root) + fixPaddingStatusbar(subs_root) state = getCurrentSavedStyle() context?.updateState() 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 7d798204..4ed1aee6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -397,21 +397,22 @@ object UIHelper { return result } - fun Context?.fixPaddingStatusbar(v: View?) { - if (v == null || this == null) return + fun fixPaddingStatusbar(v: View?) { + if (v == null) return + val ctx = v.context ?: return v.setPadding( v.paddingLeft, - v.paddingTop + getStatusBarHeight(), + v.paddingTop + ctx.getStatusBarHeight(), v.paddingRight, v.paddingBottom ) } - fun Context.fixPaddingStatusbarView(v: View?) { + fun fixPaddingStatusbarView(v: View?) { if (v == null) return - + val ctx = v.context ?: return val params = v.layoutParams - params.height = getStatusBarHeight() + params.height = ctx.getStatusBarHeight() v.layoutParams = params } diff --git a/app/src/main/res/layout/fragment_home_tv.xml b/app/src/main/res/layout/fragment_home_tv.xml index ebcd3e9f..ac7c4abd 100644 --- a/app/src/main/res/layout/fragment_home_tv.xml +++ b/app/src/main/res/layout/fragment_home_tv.xml @@ -172,4 +172,15 @@ app:icon="@drawable/ic_baseline_filter_list_24" tools:ignore="ContentDescription" tools:visibility="visible" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_plugins.xml b/app/src/main/res/layout/fragment_plugins.xml index 40a0299c..c207b2c3 100644 --- a/app/src/main/res/layout/fragment_plugins.xml +++ b/app/src/main/res/layout/fragment_plugins.xml @@ -25,7 +25,7 @@ app:titleTextColor="?attr/textColor" tools:title="Overlord" /> - + - + diff --git a/app/src/main/res/layout/fragment_search_tv.xml b/app/src/main/res/layout/fragment_search_tv.xml index 0a85a471..bb59d503 100644 --- a/app/src/main/res/layout/fragment_search_tv.xml +++ b/app/src/main/res/layout/fragment_search_tv.xml @@ -89,7 +89,7 @@ app:tint="?attr/textColor" /> - + diff --git a/app/src/main/res/layout/home_select_mainpage.xml b/app/src/main/res/layout/home_select_mainpage.xml index a4bb686c..1d2d1780 100644 --- a/app/src/main/res/layout/home_select_mainpage.xml +++ b/app/src/main/res/layout/home_select_mainpage.xml @@ -26,7 +26,7 @@ android:layout_gravity="bottom" android:layout_width="match_parent" android:layout_height="60dp"> - + - - + app:cardCornerRadius="@dimen/rounded_image_radius"> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:foreground="?android:attr/selectableItemBackgroundBorderless" + android:orientation="vertical" + android:padding="10dp"> + android:id="@+id/episode_lin_holder" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + android:layout_width="126dp" + android:layout_height="72dp" + android:foreground="@drawable/outline_drawable"> + android:foreground="?android:attr/selectableItemBackgroundBorderless" + android:nextFocusRight="@id/result_episode_download" + android:scaleType="centerCrop" + tools:src="@drawable/example_poster" /> + android:layout_width="36dp" + android:layout_height="36dp" + android:layout_gravity="center" + android:contentDescription="@string/play_episode" + android:src="@drawable/play_button" /> + android:id="@+id/episode_progress" + style="@android:style/Widget.Material.ProgressBar.Horizontal" + android:layout_width="match_parent" + android:layout_height="5dp" + android:layout_gravity="bottom" + android:layout_marginBottom="-1.5dp" + android:progressBackgroundTint="?attr/colorPrimary" + android:progressTint="?attr/colorPrimary" + tools:progress="50" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="15dp" + android:layout_marginEnd="50dp" + android:orientation="vertical"> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal"> + android:id="@+id/episode_filler" + style="@style/SmallBlackButton" + android:layout_gravity="start" + android:layout_marginEnd="10dp" + android:text="@string/filler" /> + android:id="@+id/episode_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:textColor="?attr/textColor" + android:textStyle="bold" + tools:text="1. Jobless" /> + android:id="@+id/episode_rating" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?attr/grayTextColor" + tools:text="Rated: 8.8" /> + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="end" + android:layout_marginStart="-50dp"> + android:id="@+id/result_episode_progress_downloaded" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_gravity="end|center_vertical" + android:layout_margin="5dp" + android:layout_marginStart="10dp" + android:layout_marginEnd="10dp" + android:background="@drawable/circle_shape" + android:indeterminate="false" + android:max="100" + android:progress="0" + android:progressDrawable="@drawable/circular_progress_bar" + android:visibility="visible" /> + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:background="?selectableItemBackgroundBorderless" + android:contentDescription="@string/download" + android:nextFocusLeft="@id/episode_poster" + android:padding="10dp" + android:src="@drawable/ic_baseline_play_arrow_24" + android:visibility="visible" + app:tint="?attr/white" /> + android:id="@+id/episode_descript" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="4" + android:paddingTop="10dp" + android:paddingBottom="10dp" + android:textColor="?attr/grayTextColor" + tools:text="Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart. Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart." /> \ No newline at end of file diff --git a/app/src/main/res/layout/tvtypes_chips_scroll.xml b/app/src/main/res/layout/tvtypes_chips_scroll.xml index 45b27dbc..66c7efda 100644 --- a/app/src/main/res/layout/tvtypes_chips_scroll.xml +++ b/app/src/main/res/layout/tvtypes_chips_scroll.xml @@ -6,5 +6,5 @@ android:requiresFadingEdge="horizontal" xmlns:android="http://schemas.android.com/apk/res/android"> - + \ No newline at end of file From 166a21f74eacd239be86aa1f4e60380edc571b12 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 14 Jul 2023 02:28:49 +0200 Subject: [PATCH 096/570] more views -> viewbinding --- .../lagradost/cloudstream3/MainActivity.kt | 1 + .../cloudstream3/ui/EasterEggMonke.kt | 21 +-- .../cloudstream3/ui/WebviewFragment.kt | 33 ++-- .../ui/download/DownloadChildAdapter.kt | 57 +++---- .../ui/download/DownloadHeaderAdapter.kt | 60 +++---- .../ui/home/HomeParentItemAdapter.kt | 25 --- .../ui/library/LoadingPosterAdapter.kt | 8 - .../ui/player/FullScreenPlayer.kt | 6 - .../cloudstream3/ui/player/GeneratorPlayer.kt | 2 +- .../ui/player/PlayerEpisodeAdapter.kt | 158 ------------------ .../cloudstream3/ui/search/SearchAdaptor.kt | 31 ++-- .../cloudstream3/ui/search/SearchFragment.kt | 1 + .../ui/settings/SettingsAccount.kt | 104 ++++++------ .../ui/settings/SettingsFragment.kt | 77 +++++---- .../ui/settings/SettingsGeneral.kt | 27 +-- .../ui/settings/SettingsUpdates.kt | 16 +- .../settings/extensions/ExtensionsFragment.kt | 95 ++++++----- .../ui/settings/extensions/RepoAdapter.kt | 72 ++++++-- .../ui/settings/testing/TestFragment.kt | 120 ++++++------- 19 files changed, 412 insertions(+), 502 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerEpisodeAdapter.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index d054f504..f409c10f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -743,6 +743,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) updateTv() + if (isTvSettings()) { setContentView(R.layout.activity_main_tv) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt index 556ebd34..c7041776 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt @@ -16,14 +16,16 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatImageView import androidx.core.view.isVisible import com.lagradost.cloudstream3.R -import kotlinx.android.synthetic.main.activity_easter_egg_monke.* -import java.util.* +import com.lagradost.cloudstream3.databinding.ActivityEasterEggMonkeBinding class EasterEggMonke : AppCompatActivity() { + lateinit var binding : ActivityEasterEggMonkeBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_easter_egg_monke) + + binding = ActivityEasterEggMonkeBinding.inflate(layoutInflater) + setContentView(binding.root) val handler = Handler(mainLooper) lateinit var runnable: Runnable @@ -32,15 +34,14 @@ class EasterEggMonke : AppCompatActivity() { handler.postDelayed(runnable, 300) } handler.postDelayed(runnable, 1000) - } private fun shower() { - val containerW = frame.width - val containerH = frame.height - var starW: Float = monke.width.toFloat() - var starH: Float = monke.height.toFloat() + val containerW = binding.frame.width + val containerH = binding.frame.height + var starW: Float = binding.monke.width.toFloat() + var starH: Float = binding.monke.height.toFloat() val newStar = AppCompatImageView(this) val idx = (monkeys.size * Math.random()).toInt() @@ -48,7 +49,7 @@ class EasterEggMonke : AppCompatActivity() { newStar.isVisible = true newStar.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT) - frame.addView(newStar) + binding.frame.addView(newStar) newStar.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX newStar.scaleY = newStar.scaleX @@ -70,7 +71,7 @@ class EasterEggMonke : AppCompatActivity() { set.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - frame.removeView(newStar) + binding.frame.removeView(newStar) } }) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt index 19e24f74..9ed58e2c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt @@ -12,20 +12,23 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.findNavController import com.lagradost.cloudstream3.MainActivity -import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.USER_AGENT +import com.lagradost.cloudstream3.databinding.FragmentWebviewBinding import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.utils.AppUtils.loadRepository -import kotlinx.android.synthetic.main.fragment_webview.* + class WebviewFragment : Fragment() { + + var binding: FragmentWebviewBinding? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val url = arguments?.getString(WEBVIEW_URL) ?: "".also { findNavController().popBackStack() } - web_view.webViewClient = object : WebViewClient() { + binding?.webView?.webViewClient = object : WebViewClient() { override fun shouldOverrideUrlLoading( view: WebView?, request: WebResourceRequest? @@ -40,24 +43,28 @@ class WebviewFragment : Fragment() { return super.shouldOverrideUrlLoading(view, request) } } + binding?.webView?.apply { + WebViewResolver.webViewUserAgent = settings.userAgentString - WebViewResolver.webViewUserAgent = web_view.settings.userAgentString - - web_view.addJavascriptInterface(RepoApi(activity), "RepoApi") - web_view.settings.javaScriptEnabled = true - web_view.settings.userAgentString = USER_AGENT - web_view.settings.domStorageEnabled = true + addJavascriptInterface(RepoApi(activity), "RepoApi") + settings.javaScriptEnabled = true + settings.userAgentString = USER_AGENT + settings.domStorageEnabled = true // WebView.setWebContentsDebuggingEnabled(true) - web_view.loadUrl(url) + loadUrl(url) + } + } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { + val localBinding = FragmentWebviewBinding.inflate(inflater, container, false) + binding = localBinding // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_webview, container, false) + return localBinding.root//inflater.inflate(R.layout.fragment_webview, container, false) } companion object { @@ -70,7 +77,7 @@ class WebviewFragment : Fragment() { private class RepoApi(val activity: FragmentActivity?) { @JavascriptInterface - fun installRepo(repoUrl: String) { + fun installRepo(repoUrl: String) { activity?.loadRepository(repoUrl) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt index a541171b..1a3e2db3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt @@ -3,18 +3,13 @@ package com.lagradost.cloudstream3.ui.download import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.cardview.widget.CardView -import androidx.core.widget.ContentLoadingProgressBar import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.VideoDownloadHelper -import kotlinx.android.synthetic.main.download_child_episode.view.* -import java.util.* +import java.util.Collections const val DOWNLOAD_ACTION_PLAY_FILE = 0 const val DOWNLOAD_ACTION_DELETE_FILE = 1 @@ -68,7 +63,7 @@ class DownloadChildAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return DownloadChildViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.download_child_episode, parent, false), + DownloadChildEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false), clickCallback ) } @@ -88,17 +83,17 @@ class DownloadChildAdapter( class DownloadChildViewHolder constructor( - itemView: View, + val binding: DownloadChildEpisodeBinding, private val clickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder { + ) : RecyclerView.ViewHolder(binding.root), DownloadButtonViewHolder { override var downloadButton = EasyDownloadButton() - private val title: TextView = itemView.download_child_episode_text + /*private val title: TextView = itemView.download_child_episode_text private val extraInfo: TextView = itemView.download_child_episode_text_extra private val holder: CardView = itemView.download_child_episode_holder private val progressBar: ContentLoadingProgressBar = itemView.download_child_episode_progress private val progressBarDownload: ContentLoadingProgressBar = itemView.download_child_episode_progress_downloaded - private val downloadImage: ImageView = itemView.download_child_episode_download + private val downloadImage: ImageView = itemView.download_child_episode_download*/ private var localCard: VisualDownloadChildCached? = null @@ -107,29 +102,35 @@ class DownloadChildAdapter( val d = card.data val posDur = getViewPos(d.id) - if (posDur != null) { - val visualPos = posDur.fixVisual() - progressBar.max = (visualPos.duration / 1000).toInt() - progressBar.progress = (visualPos.position / 1000).toInt() - progressBar.visibility = View.VISIBLE - } else { - progressBar.visibility = View.GONE + binding.downloadChildEpisodeProgress.apply { + if (posDur != null) { + val visualPos = posDur.fixVisual() + max = (visualPos.duration / 1000).toInt() + progress = (visualPos.position / 1000).toInt() + visibility = View.VISIBLE + } else { + visibility = View.GONE + } + } + + + binding.downloadChildEpisodeText.apply { + text = context.getNameFull(d.name, d.episode, d.season) + isSelected = true // is needed for text repeating } - title.text = title.context.getNameFull(d.name, d.episode, d.season) - title.isSelected = true // is needed for text repeating downloadButton.setUpButton( card.currentBytes, card.totalBytes, - progressBarDownload, - downloadImage, - extraInfo, + binding.downloadChildEpisodeProgressDownloaded, + binding.downloadChildEpisodeDownload, + binding.downloadChildEpisodeTextExtra, card.data, clickCallback ) - holder.setOnClickListener { + binding.downloadChildEpisodeHolder.setOnClickListener { clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) } } @@ -141,9 +142,9 @@ class DownloadChildAdapter( downloadButton.setUpButton( card.currentBytes, card.totalBytes, - progressBarDownload, - downloadImage, - extraInfo, + binding.downloadChildEpisodeProgressDownloaded, + binding.downloadChildEpisodeDownload, + binding.downloadChildEpisodeTextExtra, card.data, clickCallback ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt index 29bb303a..1634009b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt @@ -5,16 +5,12 @@ import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.cardview.widget.CardView -import androidx.core.widget.ContentLoadingProgressBar import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.VideoDownloadHelper -import kotlinx.android.synthetic.main.download_header_episode.view.* import java.util.* data class VisualDownloadHeaderCached( @@ -66,7 +62,7 @@ class DownloadHeaderAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return DownloadHeaderViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.download_header_episode, parent, false), + DownloadHeaderEpisodeBinding.inflate(LayoutInflater.from(parent.context),parent,false), clickCallback, movieClickCallback ) @@ -87,20 +83,20 @@ class DownloadHeaderAdapter( class DownloadHeaderViewHolder constructor( - itemView: View, + val binding: DownloadHeaderEpisodeBinding, private val clickCallback: (DownloadHeaderClickEvent) -> Unit, private val movieClickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder { + ) : RecyclerView.ViewHolder(binding.root), DownloadButtonViewHolder { override var downloadButton = EasyDownloadButton() - private val poster: ImageView? = itemView.download_header_poster + /*private val poster: ImageView? = itemView.download_header_poster private val title: TextView = itemView.download_header_title private val extraInfo: TextView = itemView.download_header_info private val holder: CardView = itemView.episode_holder private val downloadBar: ContentLoadingProgressBar = itemView.download_header_progress_downloaded private val downloadImage: ImageView = itemView.download_header_episode_download - private val normalImage: ImageView = itemView.download_header_goto_child + private val normalImage: ImageView = itemView.download_header_goto_child*/ private var localCard: VisualDownloadHeaderCached? = null @SuppressLint("SetTextI18n") @@ -108,19 +104,24 @@ class DownloadHeaderAdapter( localCard = card val d = card.data - poster?.setImage(d.poster) - poster?.setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(1, d)) + binding.downloadHeaderPoster.apply { + setImage(d.poster) + setOnClickListener { + clickCallback.invoke(DownloadHeaderClickEvent(1, d)) + } } - title.text = d.name + binding.apply { + + binding.downloadHeaderTitle.text = d.name val mbString = formatShortFileSize(itemView.context, card.totalBytes) //val isMovie = d.type.isMovieType() if (card.child != null) { - downloadBar.visibility = View.VISIBLE - downloadImage.visibility = View.VISIBLE - normalImage.visibility = View.GONE + downloadHeaderProgressDownloaded.visibility = View.VISIBLE + + downloadHeaderEpisodeDownload.visibility = View.VISIBLE + binding.downloadHeaderGotoChild.visibility = View.GONE /*setUpButton( card.currentBytes, card.totalBytes, @@ -131,34 +132,35 @@ class DownloadHeaderAdapter( movieClickCallback )*/ - holder.setOnClickListener { + episodeHolder.setOnClickListener { movieClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child)) } } else { - downloadBar.visibility = View.GONE - downloadImage.visibility = View.GONE - normalImage.visibility = View.VISIBLE + downloadHeaderProgressDownloaded.visibility = View.GONE + downloadHeaderEpisodeDownload.visibility = View.GONE + binding.downloadHeaderGotoChild.visibility = View.VISIBLE try { - extraInfo.text = - extraInfo.context.getString(R.string.extra_info_format).format( + downloadHeaderInfo.text = + downloadHeaderInfo.context.getString(R.string.extra_info_format).format( card.totalDownloads, - if (card.totalDownloads == 1) extraInfo.context.getString(R.string.episode) else extraInfo.context.getString( + if (card.totalDownloads == 1) downloadHeaderInfo.context.getString(R.string.episode) else downloadHeaderInfo.context.getString( R.string.episodes ), mbString ) } catch (t : Throwable) { // you probably formatted incorrectly - extraInfo.text = "Error" + downloadHeaderInfo.text = "Error" logError(t) } - holder.setOnClickListener { + episodeHolder.setOnClickListener { clickCallback.invoke(DownloadHeaderClickEvent(0, d)) } } + } } override fun reattachDownloadButton() { @@ -168,9 +170,9 @@ class DownloadHeaderAdapter( downloadButton.setUpButton( card.currentBytes, card.totalBytes, - downloadBar, - downloadImage, - extraInfo, + binding.downloadHeaderProgressDownloaded, + binding.downloadHeaderEpisodeDownload, + binding.downloadHeaderInfo, card.child, movieClickCallback ) 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 58c6dbe0..7ce9e67d 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 @@ -3,49 +3,24 @@ package com.lagradost.cloudstream3.ui.home import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.LinearLayout import android.widget.TextView -import androidx.core.content.ContextCompat -import androidx.core.view.isGone -import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView -import androidx.transition.ChangeBounds -import androidx.transition.TransitionManager -import androidx.viewpager2.widget.ViewPager2 -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 -import com.lagradost.cloudstream3.mvvm.Resource -import com.lagradost.cloudstream3.ui.WatchType -import com.lagradost.cloudstream3.ui.result.LinearListLayout -import com.lagradost.cloudstream3.ui.result.ResultViewModel2 -import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST import com.lagradost.cloudstream3.ui.result.setLinearListLayout -import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable -import com.lagradost.cloudstream3.utils.AppUtils.loadResult -import com.lagradost.cloudstream3.utils.DataStoreHelper -import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog -import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView import kotlinx.android.synthetic.main.activity_main_tv.* import kotlinx.android.synthetic.main.activity_main_tv.view.* import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.view.* import kotlinx.android.synthetic.main.fragment_home_head_tv.* import kotlinx.android.synthetic.main.fragment_home_head_tv.view.* -import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview -import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_viewpager import kotlinx.android.synthetic.main.homepage_parent.view.* class LoadClickCallback( 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 index a637133b..160fbe2b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LoadingPosterAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LoadingPosterAdapter.kt @@ -5,15 +5,7 @@ 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() { 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 9ff1c52d..37fb0373 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 @@ -97,12 +97,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected var isShowing = false protected var isLocked = false - //private var episodes: List = listOf() - protected fun setEpisodes(ep: List) { - //hasEpisodes = ep.size > 1 // if has 2 episodes or more because you dont want to switch to your current episode - //(player_episode_list?.adapter as? PlayerEpisodeAdapter?)?.updateList(ep) - } - protected var hasEpisodes = false private set //protected val hasEpisodes 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 fd29d998..4f29468c 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 @@ -163,7 +163,7 @@ class GeneratorPlayer : FullScreenPlayer() { currentSelectedLink = link currentMeta = viewModel.getMeta() nextMeta = viewModel.getNextMeta() - setEpisodes(viewModel.getAllMeta() ?: emptyList()) + // setEpisodes(viewModel.getAllMeta() ?: emptyList()) isActive = true setPlayerDimen(null) setTitle() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerEpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerEpisodeAdapter.kt deleted file mode 100644 index cfe27a30..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerEpisodeAdapter.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.lagradost.cloudstream3.ui.player - -import android.annotation.SuppressLint -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.core.view.isGone -import androidx.core.view.isVisible -import androidx.core.widget.ContentLoadingProgressBar -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.button.MaterialButton -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.ui.result.ResultEpisode -import com.lagradost.cloudstream3.ui.result.getDisplayPosition -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings -import com.lagradost.cloudstream3.utils.AppUtils.html -import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.player_episodes_large.view.episode_holder_large -import kotlinx.android.synthetic.main.player_episodes_large.view.episode_progress -import kotlinx.android.synthetic.main.player_episodes_small.view.episode_holder -import kotlinx.android.synthetic.main.result_episode_large.view.* - - -data class PlayerEpisodeClickEvent(val action: Int, val data: Any) - -class PlayerEpisodeAdapter( - private val items: MutableList = mutableListOf(), - private val clickCallback: (PlayerEpisodeClickEvent) -> Unit, -) : RecyclerView.Adapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return PlayerEpisodeCardViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.player_episodes, parent, false), - clickCallback, - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - println("HOLDER $holder $position") - - when (holder) { - is PlayerEpisodeCardViewHolder -> { - holder.bind(items[position]) - } - } - } - - override fun getItemCount(): Int { - return items.size - } - - fun updateList(newList: List) { - println("Updated list $newList") - val diffResult = DiffUtil.calculateDiff(EpisodeDiffCallback(this.items, newList)) - items.clear() - items.addAll(newList) - - diffResult.dispatchUpdatesTo(this) - } - - class PlayerEpisodeCardViewHolder - constructor( - itemView: View, - private val clickCallback: (PlayerEpisodeClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(itemView) { - @SuppressLint("SetTextI18n") - fun bind(card: Any) { - if (card is ResultEpisode) { - val (parentView, otherView) = if (card.poster == null) { - itemView.episode_holder to itemView.episode_holder_large - } else { - itemView.episode_holder_large to itemView.episode_holder - } - - val episodeText: TextView? = parentView.episode_text - val episodeFiller: MaterialButton? = parentView.episode_filler - val episodeRating: TextView? = parentView.episode_rating - val episodeDescript: TextView? = parentView.episode_descript - val episodeProgress: ContentLoadingProgressBar? = parentView.episode_progress - val episodePoster: ImageView? = parentView.episode_poster - - parentView.isVisible = true - otherView.isVisible = false - - - episodeText?.apply { - val name = - if (card.name == null) "${context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}" - - text = name - isSelected = true - } - - episodeFiller?.isVisible = card.isFiller == true - - val displayPos = card.getDisplayPosition() - episodeProgress?.max = (card.duration / 1000).toInt() - episodeProgress?.progress = (displayPos / 1000).toInt() - episodeProgress?.isVisible = displayPos > 0L - episodePoster?.isVisible = episodePoster?.setImage(card.poster) == true - - if (card.rating != null) { - episodeRating?.text = episodeRating?.context?.getString(R.string.rated_format) - ?.format(card.rating.toFloat() / 10f) - } else { - episodeRating?.text = "" - } - - episodeRating?.isGone = episodeRating?.text.isNullOrBlank() - - episodeDescript?.apply { - text = card.description.html() - isGone = text.isNullOrBlank() - //setOnClickListener { - // clickCallback.invoke(PlayerEpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card)) - //} - } - - parentView.setOnClickListener { - clickCallback.invoke(PlayerEpisodeClickEvent(0, card)) - } - - if (isTrueTvSettings()) { - parentView.isFocusable = true - parentView.isFocusableInTouchMode = true - parentView.touchscreenBlocksFocus = false - } - } - } - } -} - -class EpisodeDiffCallback( - private val oldList: List, - private val newList: List -) : - DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val a = oldList[oldItemPosition] - val b = newList[newItemPosition] - return if (a is ResultEpisode && b is ResultEpisode) { - a.id == b.id - } else { - a == b - } - } - - override fun getOldListSize() = oldList.size - - override fun getNewListSize() = newList.size - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition] == newList[newItemPosition] -} \ No newline at end of file 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 7fdd6e1d..233614dd 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 @@ -4,16 +4,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import android.widget.ImageView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.R +import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.databinding.SearchResultGridBinding +import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding 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 */ @@ -39,10 +38,23 @@ class SearchAdapter( var hasNext: Boolean = false override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + + val layout = - if (parent.context.IsBottomLayout()) R.layout.search_result_grid_expanded else R.layout.search_result_grid + if (parent.context.IsBottomLayout()) SearchResultGridExpandedBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) else SearchResultGridBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) //R.layout.search_result_grid_expanded else R.layout.search_result_grid + + + return CardViewHolder( - LayoutInflater.from(parent.context).inflate(layout, parent, false), + layout, clickCallback, resView ) @@ -73,12 +85,11 @@ class SearchAdapter( class CardViewHolder constructor( - itemView: View, + val binding: ViewBinding, private val clickCallback: (SearchClickCallback) -> Unit, resView: AutofitRecyclerView ) : - RecyclerView.ViewHolder(itemView) { - private val cardView: ImageView = itemView.imageView + RecyclerView.ViewHolder(binding.root) { private val compactView = false//itemView.context.getGridIsCompact() private val coverHeight: Int = @@ -86,7 +97,7 @@ class SearchAdapter( fun bind(card: SearchResponse, position: Int) { if (!compactView) { - cardView.apply { + binding.root.apply { layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, coverHeight 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 e0f67d4a..a11dab25 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 @@ -104,6 +104,7 @@ class SearchFragment : Fragment() { val layout = if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search val root = inflater.inflate(layout, container, false) + // TODO TRYCATCH binding = FragmentSearchBinding.bind(root) return root diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index acf715b3..a0166409 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -15,6 +15,9 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.AccountManagmentBinding +import com.lagradost.cloudstream3.databinding.AccountSwitchBinding +import com.lagradost.cloudstream3.databinding.AddAccountInputBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi @@ -31,9 +34,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.account_managment.* -import kotlinx.android.synthetic.main.account_switch.* -import kotlinx.android.synthetic.main.add_account_input.* class SettingsAccount : PreferenceFragmentCompat() { companion object { @@ -43,15 +43,18 @@ class SettingsAccount : PreferenceFragmentCompat() { api: AccountManager, info: AuthAPI.LoginInfo ) { + if (activity == null) return + val binding: AccountManagmentBinding = + AccountManagmentBinding.inflate(activity.layoutInflater, null, false) val builder = - AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom) - .setView(R.layout.account_managment) + AlertDialog.Builder(activity, R.style.AlertDialogCustom) + .setView(binding.root) val dialog = builder.show() - dialog.account_main_profile_picture_holder?.isVisible = - dialog.account_main_profile_picture?.setImage(info.profilePicture) == true + binding.accountMainProfilePictureHolder.isVisible = + binding.accountMainProfilePicture.setImage(info.profilePicture) - dialog.account_logout?.setOnClickListener { + binding.accountLogout.setOnClickListener { api.logOut() dialog.dismissSafe(activity) } @@ -60,26 +63,28 @@ class SettingsAccount : PreferenceFragmentCompat() { dialog.findViewById(R.id.account_name)?.text = it } - dialog.account_site?.text = api.name - dialog.account_switch_account?.setOnClickListener { + binding.accountSite.text = api.name + binding.accountSwitchAccount.setOnClickListener { dialog.dismissSafe(activity) showAccountSwitch(activity, api) } if (isTvSettings()) { - dialog.account_switch_account?.requestFocus() + binding.accountSwitchAccount.requestFocus() } } - fun showAccountSwitch(activity: FragmentActivity, api: AccountManager) { + private fun showAccountSwitch(activity: FragmentActivity, api: AccountManager) { val accounts = api.getAccounts() ?: return + val binding: AccountSwitchBinding = + AccountSwitchBinding.inflate(activity.layoutInflater, null, false) val builder = AlertDialog.Builder(activity, R.style.AlertDialogCustom) - .setView(R.layout.account_switch) + .setView(binding.root) val dialog = builder.show() - dialog.account_add?.setOnClickListener { + binding.accountAdd.setOnClickListener { addAccount(activity, api) dialog?.dismissSafe(activity) } @@ -111,17 +116,21 @@ class SettingsAccount : PreferenceFragmentCompat() { is OAuth2API -> { api.authenticate(activity) } + is InAppAuthAPI -> { + if (activity == null) return + val binding: AddAccountInputBinding = + AddAccountInputBinding.inflate(activity.layoutInflater, null, false) val builder = - AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom) - .setView(R.layout.add_account_input) + AlertDialog.Builder(activity, R.style.AlertDialogCustom) + .setView(binding.root) val dialog = builder.show() - val visibilityMap = mapOf( - dialog.login_email_input to api.requiresEmail, - dialog.login_password_input to api.requiresPassword, - dialog.login_server_input to api.requiresServer, - dialog.login_username_input to api.requiresUsername + val visibilityMap = listOf( + binding.loginEmailInput to api.requiresEmail, + binding.loginPasswordInput to api.requiresPassword, + binding.loginServerInput to api.requiresServer, + binding.loginUsernameInput to api.requiresUsername ) if (isTvSettings()) { @@ -145,12 +154,12 @@ class SettingsAccount : PreferenceFragmentCompat() { } } - dialog.login_email_input?.isVisible = api.requiresEmail - dialog.login_password_input?.isVisible = api.requiresPassword - dialog.login_server_input?.isVisible = api.requiresServer - dialog.login_username_input?.isVisible = api.requiresUsername - dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank() - dialog.create_account?.setOnClickListener { + binding.loginEmailInput.isVisible = api.requiresEmail + binding.loginPasswordInput.isVisible = api.requiresPassword + binding.loginServerInput.isVisible = api.requiresServer + binding.loginUsernameInput.isVisible = api.requiresUsername + binding.createAccount.isGone = api.createAccountUrl.isNullOrBlank() + binding.createAccount.setOnClickListener { openBrowser( api.createAccountUrl ?: return@setOnClickListener, activity @@ -159,43 +168,43 @@ class SettingsAccount : PreferenceFragmentCompat() { } val displayedItems = listOf( - dialog.login_username_input, - dialog.login_email_input, - dialog.login_server_input, - dialog.login_password_input + binding.loginUsernameInput, + binding.loginEmailInput, + binding.loginServerInput, + binding.loginPasswordInput ).filter { it.isVisible } displayedItems.foldRight(displayedItems.firstOrNull()) { item, previous -> - item?.id?.let { previous?.nextFocusDownId = it } - previous?.id?.let { item?.nextFocusUpId = it } + item.id.let { previous?.nextFocusDownId = it } + previous?.id?.let { item.nextFocusUpId = it } item } displayedItems.firstOrNull()?.let { - dialog.create_account?.nextFocusDownId = it.id - it.nextFocusUpId = dialog.create_account.id + binding.createAccount.nextFocusDownId = it.id + it.nextFocusUpId = binding.createAccount.id } - dialog.apply_btt?.id?.let { + binding.applyBtt.id.let { displayedItems.lastOrNull()?.nextFocusDownId = it } - dialog.text1?.text = api.name + binding.text1.text = api.name if (api.storesPasswordInPlainText) { api.getLatestLoginData()?.let { data -> - dialog.login_email_input?.setText(data.email ?: "") - dialog.login_server_input?.setText(data.server ?: "") - dialog.login_username_input?.setText(data.username ?: "") - dialog.login_password_input?.setText(data.password ?: "") + binding.loginEmailInput.setText(data.email ?: "") + binding.loginServerInput.setText(data.server ?: "") + binding.loginUsernameInput.setText(data.username ?: "") + binding.loginPasswordInput.setText(data.password ?: "") } } - dialog.apply_btt?.setOnClickListener { + binding.applyBtt.setOnClickListener { val loginData = InAppAuthAPI.LoginData( - username = if (api.requiresUsername) dialog.login_username_input?.text?.toString() else null, - password = if (api.requiresPassword) dialog.login_password_input?.text?.toString() else null, - email = if (api.requiresEmail) dialog.login_email_input?.text?.toString() else null, - server = if (api.requiresServer) dialog.login_server_input?.text?.toString() else null, + username = if (api.requiresUsername) binding.loginUsernameInput.text?.toString() else null, + password = if (api.requiresPassword) binding.loginPasswordInput.text?.toString() else null, + email = if (api.requiresEmail) binding.loginEmailInput.text?.toString() else null, + server = if (api.requiresServer) binding.loginServerInput.text?.toString() else null, ) ioSafe { val isSuccessful = try { @@ -220,10 +229,11 @@ class SettingsAccount : PreferenceFragmentCompat() { } dialog.dismissSafe(activity) } - dialog.cancel_btt?.setOnClickListener { + binding.cancelBtt.setOnClickListener { dialog.dismissSafe(activity) } } + else -> { throw NotImplementedError("You are trying to add an account that has an unknown login method") } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 453f93be..85afc048 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -14,7 +14,9 @@ import androidx.fragment.app.Fragment import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager +import com.google.android.material.appbar.MaterialToolbar import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.ui.home.HomeFragment @@ -22,16 +24,14 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.android.synthetic.main.main_settings.* -import kotlinx.android.synthetic.main.standard_toolbar.* import java.io.File class SettingsFragment : Fragment() { companion object { var beneneCount = 0 - private var isTv : Boolean = false - private var isTrueTv : Boolean = false + private var isTv: Boolean = false + private var isTrueTv: Boolean = false fun PreferenceFragmentCompat?.getPref(id: Int): Preference? { if (this == null) return null @@ -55,26 +55,30 @@ class SettingsFragment : Fragment() { fun Fragment?.setUpToolbar(title: String) { if (this == null) return - settings_toolbar?.apply { + val settingsToolbar = view?.findViewById(R.id.settings_toolbar) ?: return + + settingsToolbar.apply { setTitle(title) setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) setNavigationOnClickListener { activity?.onBackPressed() } } - fixPaddingStatusbar(settings_toolbar) + fixPaddingStatusbar(settingsToolbar) } fun Fragment?.setUpToolbar(@StringRes title: Int) { if (this == null) return - settings_toolbar?.apply { + val settingsToolbar = view?.findViewById(R.id.settings_toolbar) ?: return + + settingsToolbar.apply { setTitle(title) setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) setNavigationOnClickListener { activity?.onBackPressed() } } - fixPaddingStatusbar(settings_toolbar) + fixPaddingStatusbar(settingsToolbar) } fun getFolderSize(dir: File): Long { @@ -139,12 +143,21 @@ class SettingsFragment : Fragment() { } } + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + + var binding: MainSettingsBinding? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { - return inflater.inflate(R.layout.main_settings, container, false) + ): View { + val localBinding = MainSettingsBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.main_settings, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -157,36 +170,34 @@ class SettingsFragment : Fragment() { for (syncApi in accountManagers) { val login = syncApi.loginInfo() val pic = login?.profilePicture ?: continue - if (settings_profile_pic?.setImage( + if (binding?.settingsProfilePic?.setImage( pic, errorImageDrawable = HomeFragment.errorProfilePic ) == true ) { - settings_profile_text?.text = login.name - settings_profile?.isVisible = true + binding?.settingsProfileText?.text = login.name + binding?.settingsProfile?.isVisible = true break } } - - listOf( - Pair(settings_general, R.id.action_navigation_settings_to_navigation_settings_general), - Pair(settings_player, R.id.action_navigation_settings_to_navigation_settings_player), - Pair(settings_credits, R.id.action_navigation_settings_to_navigation_settings_account), - Pair(settings_ui, R.id.action_navigation_settings_to_navigation_settings_ui), - Pair(settings_providers, R.id.action_navigation_settings_to_navigation_settings_providers), - Pair(settings_updates, R.id.action_navigation_settings_to_navigation_settings_updates), - Pair( - settings_extensions, - R.id.action_navigation_settings_to_navigation_settings_extensions - ), - ).forEach { (view, navigationId) -> - view?.apply { - setOnClickListener { - navigate(navigationId) - } - if (isTrueTv) { - isFocusable = true - isFocusableInTouchMode = true + binding?.apply { + listOf( + settingsGeneral to R.id.action_navigation_settings_to_navigation_settings_general, + settingsPlayer to R.id.action_navigation_settings_to_navigation_settings_player, + settingsCredits to R.id.action_navigation_settings_to_navigation_settings_account, + settingsUi to R.id.action_navigation_settings_to_navigation_settings_ui, + settingsProviders to R.id.action_navigation_settings_to_navigation_settings_providers, + settingsUpdates to R.id.action_navigation_settings_to_navigation_settings_updates, + settingsExtensions to R.id.action_navigation_settings_to_navigation_settings_extensions, + ).forEach { (view, navigationId) -> + view.apply { + setOnClickListener { + navigate(navigationId) + } + if (isTrueTv) { + isFocusable = true + isFocusableInTouchMode = true + } } } } 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 ee262eec..ef194b57 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 @@ -22,6 +22,8 @@ import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.databinding.AddRemoveSitesBinding +import com.lagradost.cloudstream3.databinding.AddSiteInputBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient @@ -38,8 +40,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath -import kotlinx.android.synthetic.main.add_remove_sites.* -import kotlinx.android.synthetic.main.add_site_input.* import java.io.File fun getCurrentLocale(context: Context): String { @@ -197,18 +197,20 @@ class SettingsGeneral : PreferenceFragmentCompat() { {}) { selection -> val provider = providers.getOrNull(selection) ?: return@showDialog + val binding : AddSiteInputBinding = AddSiteInputBinding.inflate(layoutInflater,null,false) + val builder = AlertDialog.Builder(context ?: return@showDialog, R.style.AlertDialogCustom) - .setView(R.layout.add_site_input) + .setView(binding.root) val dialog = builder.create() dialog.show() - dialog.text2?.text = provider.name - dialog.apply_btt?.setOnClickListener { - val name = dialog.site_name_input?.text?.toString() - val url = dialog.site_url_input?.text?.toString() - val lang = dialog.site_lang_input?.text?.toString() + binding.text2.text = provider.name + binding.applyBtt.setOnClickListener { + val name = binding.siteNameInput.text?.toString() + val url = binding.siteUrlInput.text?.toString() + val lang = binding.siteLangInput.text?.toString() val realLang = if (lang.isNullOrBlank()) provider.lang else lang if (url.isNullOrBlank() || name.isNullOrBlank() || realLang.length != 2) { showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT) @@ -222,7 +224,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { dialog.dismissSafe(activity) } - dialog.cancel_btt?.setOnClickListener { + binding.cancelBtt.setOnClickListener { dialog.dismissSafe(activity) } } @@ -242,18 +244,19 @@ class SettingsGeneral : PreferenceFragmentCompat() { } fun showAddOrDelete() { + val binding : AddRemoveSitesBinding = AddRemoveSitesBinding.inflate(layoutInflater,null,false) val builder = AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom) - .setView(R.layout.add_remove_sites) + .setView(binding.root) val dialog = builder.create() dialog.show() - dialog.add_site?.setOnClickListener { + binding.addSite.setOnClickListener { showAdd() dialog.dismissSafe(activity) } - dialog.remove_site?.setOnClickListener { + binding.removeSite.setOnClickListener { showDelete() dialog.dismissSafe(activity) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index f9ac3fee..4208f965 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -13,6 +13,7 @@ import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.LogcatBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom @@ -25,7 +26,6 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.VideoDownloadManager -import kotlinx.android.synthetic.main.logcat.* import okhttp3.internal.closeQuietly import java.io.BufferedReader import java.io.InputStreamReader @@ -60,7 +60,9 @@ class SettingsUpdates : PreferenceFragmentCompat() { getPref(R.string.show_logcat_key)?.setOnPreferenceClickListener { pref -> val builder = AlertDialog.Builder(pref.context, R.style.AlertDialogCustom) - .setView(R.layout.logcat) + + val binding = LogcatBinding.inflate(layoutInflater,null,false ) + builder.setView(binding.root) val dialog = builder.create() dialog.show() @@ -81,9 +83,9 @@ class SettingsUpdates : PreferenceFragmentCompat() { } val text = log.toString() - dialog.text1?.text = text + binding.text1.text = text - dialog.copy_btt?.setOnClickListener { + binding.copyBtt.setOnClickListener { // Can crash on too much text try { val serviceClipboard = @@ -96,11 +98,11 @@ class SettingsUpdates : PreferenceFragmentCompat() { showToast(activity, R.string.clipboard_too_large) } } - dialog.clear_btt?.setOnClickListener { + binding.clearBtt.setOnClickListener { Runtime.getRuntime().exec("logcat -c") dialog.dismissSafe(activity) } - dialog.save_btt?.setOnClickListener { + binding.saveBtt.setOnClickListener { var fileStream: OutputStream? = null try { fileStream = @@ -119,7 +121,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { dialog.dismissSafe(activity) } } - dialog.close_btt?.setOnClickListener { + binding.closeBtt.setOnClickListener { dialog.dismissSafe(activity) } return@setOnPreferenceClickListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 75ff8305..711026c6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -18,6 +18,8 @@ import androidx.navigation.fragment.findNavController import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.AddRepoInputBinding +import com.lagradost.cloudstream3.databinding.FragmentExtensionsBinding import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.plugins.RepositoryManager @@ -30,16 +32,22 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.widget.LinearRecycleViewLayoutManager -import kotlinx.android.synthetic.main.add_repo_input.* -import kotlinx.android.synthetic.main.fragment_extensions.* class ExtensionsFragment : Fragment() { + var binding: FragmentExtensionsBinding? = null + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { - return inflater.inflate(R.layout.fragment_extensions, container, false) + ): View { + val localBinding = FragmentExtensionsBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root//inflater.inflate(R.layout.fragment_extensions, container, false) } private fun View.setLayoutWidth(weight: Int) { @@ -74,7 +82,7 @@ class ExtensionsFragment : Fragment() { setUpToolbar(R.string.extensions) - repo_recycler_view?.adapter = RepoAdapter(false, { + binding?.repoRecyclerView?.adapter = RepoAdapter(false, { findNavController().navigate( R.id.navigation_settings_extensions_to_navigation_settings_plugins, PluginsFragment.newInstance( @@ -113,12 +121,12 @@ class ExtensionsFragment : Fragment() { }) observe(extensionViewModel.repositories) { - repo_recycler_view?.isVisible = it.isNotEmpty() - blank_repo_screen?.isVisible = it.isEmpty() - (repo_recycler_view?.adapter as? RepoAdapter)?.updateList(it) + binding?.repoRecyclerView?.isVisible = it.isNotEmpty() + binding?.blankRepoScreen?.isVisible = it.isEmpty() + (binding?.repoRecyclerView?.adapter as? RepoAdapter)?.updateList(it) } - repo_recycler_view?.apply { + binding?.repoRecyclerView?.apply { context?.let { ctx -> layoutManager = LinearRecycleViewLayoutManager(ctx, nextFocusUpId, nextFocusDownId) } @@ -140,28 +148,30 @@ class ExtensionsFragment : Fragment() { // } observeNullable(extensionViewModel.pluginStats) { value -> - if (value == null) { - plugin_storage_appbar?.isVisible = false + binding?.apply { + if (value == null) { + pluginStorageAppbar.isVisible = false - return@observeNullable - } + return@observeNullable + } - plugin_storage_appbar?.isVisible = true - if (value.total == 0) { - plugin_download?.setLayoutWidth(1) - plugin_disabled?.setLayoutWidth(0) - plugin_not_downloaded?.setLayoutWidth(0) - } else { - plugin_download?.setLayoutWidth(value.downloaded) - plugin_disabled?.setLayoutWidth(value.disabled) - plugin_not_downloaded?.setLayoutWidth(value.notDownloaded) + pluginStorageAppbar.isVisible = true + if (value.total == 0) { + pluginDownload.setLayoutWidth(1) + pluginDisabled.setLayoutWidth(0) + pluginNotDownloaded.setLayoutWidth(0) + } else { + pluginDownload.setLayoutWidth(value.downloaded) + pluginDisabled.setLayoutWidth(value.disabled) + pluginNotDownloaded.setLayoutWidth(value.notDownloaded) + } + pluginNotDownloadedTxt.setText(value.notDownloadedText) + pluginDisabledTxt.setText(value.disabledText) + pluginDownloadTxt.setText(value.downloadedText) } - plugin_not_downloaded_txt.setText(value.notDownloadedText) - plugin_disabled_txt.setText(value.disabledText) - plugin_download_txt.setText(value.downloadedText) } - plugin_storage_appbar?.setOnClickListener { + binding?.pluginStorageAppbar?.setOnClickListener { findNavController().navigate( R.id.navigation_settings_extensions_to_navigation_settings_plugins, PluginsFragment.newInstance( @@ -173,16 +183,18 @@ class ExtensionsFragment : Fragment() { } val addRepositoryClick = View.OnClickListener { + val ctx = context ?: return@OnClickListener + val binding = AddRepoInputBinding.inflate(LayoutInflater.from(ctx), null, false) val builder = - AlertDialog.Builder(context ?: return@OnClickListener, R.style.AlertDialogCustom) - .setView(R.layout.add_repo_input) + AlertDialog.Builder(ctx, R.style.AlertDialogCustom) + .setView(binding.root) val dialog = builder.create() dialog.show() (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt( 0 )?.text?.toString()?.let { copy -> - dialog.repo_url_input?.setText(copy) + binding.repoUrlInput.setText(copy) } // dialog.list_repositories?.setOnClickListener { @@ -192,10 +204,10 @@ class ExtensionsFragment : Fragment() { // } // dialog.text2?.text = provider.name - dialog.apply_btt?.setOnClickListener secondListener@{ - val name = dialog.repo_name_input?.text?.toString() + binding.applyBtt.setOnClickListener secondListener@{ + val name = binding.repoNameInput.text?.toString() ioSafe { - val url = dialog.repo_url_input?.text?.toString() + val url = binding.repoUrlInput.text?.toString() ?.let { it1 -> RepositoryManager.parseRepoUrl(it1) } if (url.isNullOrBlank()) { main { @@ -214,22 +226,23 @@ class ExtensionsFragment : Fragment() { } dialog.dismissSafe(activity) } - dialog.cancel_btt?.setOnClickListener { + binding.cancelBtt.setOnClickListener { dialog.dismissSafe(activity) } } val isTv = isTrueTvSettings() - add_repo_button?.isGone = isTv - add_repo_button_imageview_holder?.isVisible = isTv + binding?.apply { + addRepoButton.isGone = isTv + addRepoButtonImageviewHolder.isVisible = isTv - // Band-aid for Fire TV - plugin_storage_appbar?.isFocusableInTouchMode = isTv - add_repo_button_imageview?.isFocusableInTouchMode = isTv - - add_repo_button?.setOnClickListener(addRepositoryClick) - add_repo_button_imageview?.setOnClickListener(addRepositoryClick) + // Band-aid for Fire TV + pluginStorageAppbar.isFocusableInTouchMode = isTv + addRepoButtonImageview.isFocusableInTouchMode = isTv + addRepoButton.setOnClickListener(addRepositoryClick) + addRepoButtonImageview.setOnClickListener(addRepositoryClick) + } reloadRepositories() } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt index e90166a8..602b45e4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt @@ -1,14 +1,15 @@ package com.lagradost.cloudstream3.ui.settings.extensions import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.RepositoryItemBinding +import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings -import kotlinx.android.synthetic.main.repository_item.view.* class RepoAdapter( val isSetup: Boolean, @@ -20,9 +21,17 @@ class RepoAdapter( private val repositories: MutableList = mutableListOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val layout = if(isTrueTvSettings()) R.layout.repository_item_tv else R.layout.repository_item + val layout = if (isTrueTvSettings()) RepositoryItemTvBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) else RepositoryItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) //R.layout.repository_item_tv else R.layout.repository_item return RepoViewHolder( - LayoutInflater.from(parent.context).inflate(layout, parent, false) + layout ) } @@ -57,30 +66,57 @@ class RepoAdapter( diffResult.dispatchUpdatesTo(this) } - inner class RepoViewHolder(itemView: View) : - RecyclerView.ViewHolder(itemView) { + inner class RepoViewHolder( + val binding: ViewBinding + ) : + RecyclerView.ViewHolder(binding.root) { fun bind( repositoryData: RepositoryData ) { val isPrebuilt = PREBUILT_REPOSITORIES.contains(repositoryData) val drawable = if (isSetup) R.drawable.netflix_download else R.drawable.ic_baseline_delete_outline_24 + when (binding) { + is RepositoryItemTvBinding -> { + binding.apply { + // Only shows icon if on setup or if it isn't a prebuilt repo. + // No delete buttons on prebuilt repos. + if (!isPrebuilt || isSetup) { + actionButton.setImageResource(drawable) + } - // Only shows icon if on setup or if it isn't a prebuilt repo. - // No delete buttons on prebuilt repos. - if (!isPrebuilt || isSetup) { - itemView.action_button?.setImageResource(drawable) - } + actionButton.setOnClickListener { + imageClickCallback(repositoryData) + } - itemView.action_button?.setOnClickListener { - imageClickCallback(repositoryData) - } + repositoryItemRoot.setOnClickListener { + clickCallback(repositoryData) + } + mainText.text = repositoryData.name + subText.text = repositoryData.url + } + } - itemView.repository_item_root?.setOnClickListener { - clickCallback(repositoryData) + is RepositoryItemBinding -> { + binding.apply { + // Only shows icon if on setup or if it isn't a prebuilt repo. + // No delete buttons on prebuilt repos. + if (!isPrebuilt || isSetup) { + actionButton.setImageResource(drawable) + } + + actionButton.setOnClickListener { + imageClickCallback(repositoryData) + } + + repositoryItemRoot.setOnClickListener { + clickCallback(repositoryData) + } + mainText.text = repositoryData.name + subText.text = repositoryData.url + } + } } - itemView.main_text?.text = repositoryData.name - itemView.sub_text?.text = repositoryData.url } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt index 34cd67cd..59b1b856 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt @@ -1,97 +1,105 @@ package com.lagradost.cloudstream3.ui.settings.testing import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentTestingBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import kotlinx.android.synthetic.main.fragment_testing.* -import kotlinx.android.synthetic.main.view_test.* class TestFragment : Fragment() { private val testViewModel: TestViewModel by activityViewModels() + var binding: FragmentTestingBinding? = null + + override fun onDestroyView() { + binding = null + super.onDestroyView() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { setUpToolbar(R.string.category_provider_test) super.onViewCreated(view, savedInstanceState) - provider_test_recycler_view?.adapter = TestResultAdapter( - mutableListOf() - ) + binding?.apply { + providerTestRecyclerView.adapter = TestResultAdapter( + mutableListOf() + ) - testViewModel.init() - if (testViewModel.isRunningTest) { - provider_test?.setState(TestView.TestState.Running) - } - - observe(testViewModel.providerProgress) { (passed, failed, total) -> - provider_test?.setProgress(passed, failed, total) - } - - observeNullable(testViewModel.providerResults) { - normalSafeApiCall { - val newItems = it.sortedBy { api -> api.first.name } - (provider_test_recycler_view?.adapter as? TestResultAdapter)?.updateList( - newItems - ) + testViewModel.init() + if (testViewModel.isRunningTest) { + providerTest.setState(TestView.TestState.Running) } - } - provider_test?.setOnPlayButtonListener { state -> - when (state) { - TestView.TestState.Stopped -> testViewModel.stopTest() - TestView.TestState.Running -> testViewModel.startTest() - TestView.TestState.None -> testViewModel.startTest() + observe(testViewModel.providerProgress) { (passed, failed, total) -> + providerTest.setProgress(passed, failed, total) } - } - if (isTrueTvSettings()) { - tests_play_pause?.isFocusableInTouchMode = true - tests_play_pause?.requestFocus() - } - - provider_test?.playPauseButton?.setOnFocusChangeListener { _, hasFocus -> - if (hasFocus) { - provider_test_appbar?.setExpanded(true, true) + observeNullable(testViewModel.providerResults) { + normalSafeApiCall { + val newItems = it.sortedBy { api -> api.first.name } + (providerTestRecyclerView.adapter as? TestResultAdapter)?.updateList( + newItems + ) + } + } + + providerTest.setOnPlayButtonListener { state -> + when (state) { + TestView.TestState.Stopped -> testViewModel.stopTest() + TestView.TestState.Running -> testViewModel.startTest() + TestView.TestState.None -> testViewModel.startTest() + } } - } - fun focusRecyclerView() { - // Hack to make it possible to focus the recyclerview. if (isTrueTvSettings()) { - provider_test_recycler_view?.requestFocus() - provider_test_appbar?.setExpanded(false, true) + providerTest.playPauseButton?.isFocusableInTouchMode = true + providerTest.playPauseButton?.requestFocus() } - } - provider_test?.setOnMainClick { - testViewModel.setFilterMethod(TestViewModel.ProviderFilter.All) - focusRecyclerView() - } - provider_test?.setOnFailedClick { - testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Failed) - focusRecyclerView() - } - provider_test?.setOnPassedClick { - testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Passed) - focusRecyclerView() + providerTest.playPauseButton?.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + providerTestAppbar.setExpanded(true, true) + } + } + + fun focusRecyclerView() { + // Hack to make it possible to focus the recyclerview. + if (isTrueTvSettings()) { + providerTestRecyclerView.requestFocus() + providerTestAppbar.setExpanded(false, true) + } + } + + providerTest.setOnMainClick { + testViewModel.setFilterMethod(TestViewModel.ProviderFilter.All) + focusRecyclerView() + } + providerTest.setOnFailedClick { + testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Failed) + focusRecyclerView() + } + providerTest.setOnPassedClick { + testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Passed) + focusRecyclerView() + } } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_testing, container, false) + ): View { + val localBinding = FragmentTestingBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root//inflater.inflate(R.layout.fragment_testing, container, false) } } \ No newline at end of file From c3296f3210bf5268b1f098d2a7a86e2457cec6ea Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Fri, 14 Jul 2023 21:33:14 +0200 Subject: [PATCH 097/570] fixed bug with source priority --- .../com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 fd29d998..da0e43d3 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 @@ -209,7 +209,7 @@ class GeneratorPlayer : FullScreenPlayer() { closestQuality(linkData?.quality) ) val sourcePriority = - QualityDataHelper.getSourcePriority(qualityProfile, linkData?.name) + QualityDataHelper.getSourcePriority(qualityProfile, linkData?.source) // negative because we want to sort highest quality first return qualityPriority + sourcePriority @@ -1410,4 +1410,4 @@ class GeneratorPlayer : FullScreenPlayer() { } } } -} \ No newline at end of file +} From 647e91bc4b850bcae227e28e4a638b7162d8fc7b Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 14 Jul 2023 21:43:46 +0200 Subject: [PATCH 098/570] more views + MainActivity viewbindings --- app/build.gradle.kts | 4 +- .../lagradost/cloudstream3/AcraApplication.kt | 4 +- .../lagradost/cloudstream3/MainActivity.kt | 140 +++++++++++------- .../cloudstream3/ui/home/HomeFragment.kt | 22 +-- .../ui/library/LibraryScrollTransformer.kt | 4 +- .../source_priority/SourcePriorityDialog.kt | 27 ++-- .../cloudstream3/ui/search/SearchAdaptor.kt | 14 +- .../ui/settings/extensions/PluginAdapter.kt | 82 +++++----- .../lagradost/cloudstream3/utils/UIHelper.kt | 51 ++++++- app/src/main/res/layout/activity_main_tv.xml | 16 ++ app/src/main/res/values/strings.xml | 1 + 11 files changed, 237 insertions(+), 128 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 86d91147..d5364045 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -208,7 +208,7 @@ dependencies { implementation("com.github.discord:OverlappingPanels:0.1.3") // debugImplementation because LeakCanary should only run in debug builds. - // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' + // debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7") // for shimmer when loading implementation("com.facebook.shimmer:shimmer:0.5.0") @@ -228,7 +228,7 @@ dependencies { // Library/extensions searching with Levenshtein distance implementation("me.xdrop:fuzzywuzzy:1.4.0") - // color pallette for images -> colors + // color palette for images -> colors implementation("androidx.palette:palette-ktx:1.0.0") } diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 0351b1ff..76b2321f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -51,8 +51,8 @@ class CustomReportSender : ReportSender { thread { // to not run it on main thread runBlocking { suspendSafeApiCall { - val post = app.post(url, data = data) - println("Report response: $post") + app.post(url, data = data) + //println("Report response: $post") } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index f409c10f..4a7a28ad 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -32,6 +32,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.bottomnavigation.BottomNavigationView import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.snackbar.Snackbar @@ -49,6 +50,9 @@ 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.databinding.ActivityMainBinding +import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding +import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.plugins.PluginManager @@ -74,6 +78,7 @@ 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.result.txt import com.lagradost.cloudstream3.ui.search.SearchFragment import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings @@ -110,9 +115,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate 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 import java.io.File @@ -334,8 +336,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // Use both navigation views to support both layouts. // It might be better to use the QuickSearch. - nav_view?.selectedItemId = R.id.navigation_search - nav_rail_view?.selectedItemId = R.id.navigation_search + activity?.findViewById(R.id.nav_view)?.selectedItemId = + R.id.navigation_search + activity?.findViewById(R.id.nav_rail_view)?.selectedItemId = + R.id.navigation_search } else if (safeURI(str)?.scheme == appStringPlayer) { val uri = Uri.parse(str) val name = uri.getQueryParameter("name") @@ -412,7 +416,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { this.hideKeyboard() // Fucks up anime info layout since that has its own layout - cast_mini_controller_holder?.isVisible = + binding?.castMiniControllerHolder?.isVisible = !listOf( R.id.navigation_results_phone, R.id.navigation_results_tv, @@ -448,7 +452,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { R.id.navigation_player, ).contains(destination.id) - nav_host_fragment?.apply { + binding?.navHostFragment?.apply { val params = layoutParams as ConstraintLayout.LayoutParams params.setMargins( @@ -464,21 +468,24 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { Configuration.ORIENTATION_LANDSCAPE -> { true } + Configuration.ORIENTATION_PORTRAIT -> { - false + isTvSettings() } + else -> { false } } + binding?.apply { + navView.isVisible = isNavVisible && !landscape + navRailView.isVisible = isNavVisible && landscape - 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 + // Hide library on TV since it is not supported yet :( + val isTrueTv = isTrueTvSettings() + navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + } } //private var mCastSession: CastSession? = null @@ -691,28 +698,37 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } private fun hidePreviewPopupDialog() { - viewModel.clear() bottomPreviewPopup.dismissSafe(this) + bottomPreviewPopup = null + bottomPreviewBinding = null } - var bottomPreviewPopup: BottomSheetDialog? = null - private fun showPreviewPopupDialog(): BottomSheetDialog { - val ret = (bottomPreviewPopup ?: run { + private var bottomPreviewPopup: BottomSheetDialog? = null + private var bottomPreviewBinding: BottomResultviewPreviewBinding? = null + private fun showPreviewPopupDialog(): BottomResultviewPreviewBinding { + val ret = (bottomPreviewBinding ?: run { val builder = BottomSheetDialog(this) - builder.setContentView(R.layout.bottom_resultview_preview) + val binding: BottomResultviewPreviewBinding = + BottomResultviewPreviewBinding.inflate(builder.layoutInflater, null, false) + bottomPreviewBinding = binding + builder.setContentView(binding.root) builder.setOnDismissListener { bottomPreviewPopup = null + bottomPreviewBinding = null viewModel.clear() } builder.setCanceledOnTouchOutside(true) builder.show() - builder + bottomPreviewPopup = builder + binding }) - bottomPreviewPopup = ret + return ret } + var binding: ActivityMainBinding? = null + override fun onCreate(savedInstanceState: Bundle?) { app.initClient(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) @@ -744,10 +760,20 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) updateTv() - if (isTvSettings()) { - setContentView(R.layout.activity_main_tv) - } else { - setContentView(R.layout.activity_main) + // just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH + binding = try { + if (isTvSettings()) { + val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) + setContentView(newLocalBinding.root) + ActivityMainBinding.bind(newLocalBinding.root) // this may crash + } else { + val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false) + setContentView(newLocalBinding.root) + newLocalBinding + } + } catch (t: Throwable) { + showToast(this, txt(R.string.unable_to_inflate, t.message ?: ""), Toast.LENGTH_LONG) + null } changeStatusBarState(isEmulatorSettings()) @@ -832,41 +858,44 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { observeNullable(viewModel.page) { resource -> if (resource == null) { - bottomPreviewPopup.dismissSafe(this) + hidePreviewPopupDialog() return@observeNullable } when (resource) { is Resource.Failure -> { showToast(this, R.string.error) + viewModel.clear() hidePreviewPopupDialog() } + is Resource.Loading -> { showPreviewPopupDialog().apply { - resultview_preview_loading?.isVisible = true - resultview_preview_result?.isVisible = false - resultview_preview_loading_shimmer?.startShimmer() + resultviewPreviewLoading.isVisible = true + resultviewPreviewResult.isVisible = false + resultviewPreviewLoadingShimmer.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() + resultviewPreviewLoading.isVisible = false + resultviewPreviewResult.isVisible = true + resultviewPreviewLoadingShimmer.stopShimmer() - resultview_preview_title?.text = d.title + resultviewPreviewTitle.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) + resultviewPreviewMetaType.setText(d.typeText) + resultviewPreviewMetaYear.setText(d.yearText) + resultviewPreviewMetaDuration.setText(d.durationText) + resultviewPreviewMetaRating.setText(d.ratingText) - resultview_preview_description?.setText(d.plotText) - resultview_preview_poster?.setImage( + resultviewPreviewDescription.setText(d.plotText) + resultviewPreviewPoster.setImage( d.posterImage ?: d.posterBackgroundImage ) - resultview_preview_poster?.setOnClickListener { + resultviewPreviewPoster.setOnClickListener { //viewModel.updateWatchStatus(WatchType.PLANTOWATCH) val value = viewModel.watchStatus.value ?: WatchType.NONE @@ -882,7 +911,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } if (!isTvSettings()) // dont want this clickable on tv layout - resultview_preview_description?.setOnClickListener { view -> + resultviewPreviewDescription.setOnClickListener { view -> view.context?.let { ctx -> val builder: AlertDialog.Builder = AlertDialog.Builder(ctx, R.style.AlertDialogCustom) @@ -892,7 +921,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } - resultview_preview_more_info?.setOnClickListener { + resultviewPreviewMoreInfo.setOnClickListener { + viewModel.clear() hidePreviewPopupDialog() lastPopup?.let { loadSearchResult(it) @@ -964,22 +994,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { .setPopExitAnim(R.anim.nav_pop_exit) .setPopUpTo(navController.graph.startDestination, false) .build()*/ - nav_view?.setupWithNavController(navController) - val nav_rail = findViewById(R.id.nav_rail_view) - nav_rail?.setupWithNavController(navController) + binding?.navView?.setupWithNavController(navController) + val navRail = findViewById(R.id.nav_rail_view) + navRail?.setupWithNavController(navController) if (isTvSettings()) { - nav_rail?.background?.alpha = 200 + navRail?.background?.alpha = 200 } else { - nav_rail?.background?.alpha = 255 + navRail?.background?.alpha = 255 } - nav_rail?.setOnItemSelectedListener { item -> + navRail?.setOnItemSelectedListener { item -> onNavDestinationSelected( item, navController ) } - nav_view?.setOnItemSelectedListener { item -> + binding?.navView?.setOnItemSelectedListener { item -> onNavDestinationSelected( item, navController @@ -1010,16 +1040,16 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { }*/ val rippleColor = ColorStateList.valueOf(getResourceColor(R.attr.colorPrimary, 0.1f)) - nav_view?.itemRippleColor = rippleColor - nav_rail?.itemRippleColor = rippleColor - nav_rail?.itemActiveIndicatorColor = rippleColor - nav_view?.itemActiveIndicatorColor = rippleColor + binding?.navView?.itemRippleColor = rippleColor + navRail?.itemRippleColor = rippleColor + navRail?.itemActiveIndicatorColor = rippleColor + binding?.navView?.itemActiveIndicatorColor = rippleColor if (!checkWrite()) { requestRW() if (checkWrite()) return } - CastButtonFactory.setUpMediaRouteButton(this, media_route_button) + //CastButtonFactory.setUpMediaRouteButton(this, media_route_button) // THIS IS CURRENTLY REMOVED BECAUSE HIGHER VERS OF ANDROID NEEDS A NOTIFICATION //if (!VideoDownloadManager.isMyServiceRunning(this, VideoDownloadKeepAliveService::class.java)) { 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 99ce7c3b..f47432dc 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 @@ -31,6 +31,7 @@ import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.MainActivity.Companion.bookmarksUpdatedEvent import com.lagradost.cloudstream3.MainActivity.Companion.mainPluginsLoadedEvent @@ -45,6 +46,7 @@ import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment +import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.* import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings @@ -431,22 +433,20 @@ class HomeFragment : Fragment() { ): View? { //homeViewModel = // ViewModelProvider(this).get(HomeViewModel::class.java) + bottomSheetDialog?.ownShow() val layout = if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home - - /* val binding = FragmentHomeTvBinding.inflate(layout, container, false) - binding.homeLoadingError - - val binding2 = FragmentHomeBinding.inflate(layout, container, false) - binding2.homeLoadingError*/ val root = inflater.inflate(layout, container, false) - binding = FragmentHomeBinding.bind(root) - //val localBinding = FragmentHomeBinding.inflate(inflater) - //binding = localBinding - return root + binding = try { + FragmentHomeBinding.bind(root) + } catch (t : Throwable) { + showToast(activity, txt(R.string.unable_to_inflate, t.message ?: ""), Toast.LENGTH_LONG) + logError(t) + null + } - //return inflater.inflate(layout, container, false) + return root } override fun onDestroyView() { 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 index 8aafbdd6..c3cee183 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryScrollTransformer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryScrollTransformer.kt @@ -2,13 +2,13 @@ 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 com.lagradost.cloudstream3.R 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( + page.findViewById(R.id.page_recyclerview).setPadding( padding, 0, -padding, 0 ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt index efc1f1b8..1b59882e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt @@ -2,24 +2,18 @@ package com.lagradost.cloudstream3.ui.player.source_priority import android.app.Dialog import android.content.Context -import android.view.View -import android.widget.EditText -import android.widget.TextView +import android.view.LayoutInflater import androidx.annotation.StyleRes import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.FragmentActivity -import androidx.recyclerview.widget.RecyclerView -import androidx.work.impl.constraints.controllers.ConstraintController import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.PlayerSelectSourcePriorityBinding import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import kotlinx.android.synthetic.main.player_select_source_priority.* class SourcePriorityDialog( - ctx: Context, + val ctx: Context, @StyleRes themeRes: Int, val links: List, private val profile: QualityDataHelper.QualityProfile, @@ -30,13 +24,14 @@ class SourcePriorityDialog( private val updatedCallback: () -> Unit ) : Dialog(ctx, themeRes) { override fun show() { - setContentView(R.layout.player_select_source_priority) - val sourcesRecyclerView: RecyclerView = sort_sources - val qualitiesRecyclerView: RecyclerView = sort_qualities - val profileText: EditText = profile_text_editable - val saveBtt: View = save_btt - val exitBtt: View = close_btt - val helpBtt: View = help_btt + val binding = PlayerSelectSourcePriorityBinding.inflate(LayoutInflater.from(ctx), null, false) + setContentView(binding.root) + val sourcesRecyclerView = binding.sortSources + val qualitiesRecyclerView = binding.sortQualities + val profileText = binding.profileTextEditable + val saveBtt = binding.saveBtt + val exitBtt = binding.closeBtt + val helpBtt = binding.helpBtt profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context)) profileText.hint = txt(R.string.profile_number, profile.id).asString(context) 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 233614dd..b516348d 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 @@ -38,15 +38,15 @@ class SearchAdapter( var hasNext: Boolean = false override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - + val inflater = LayoutInflater.from(parent.context) val layout = if (parent.context.IsBottomLayout()) SearchResultGridExpandedBinding.inflate( - LayoutInflater.from(parent.context), + inflater, parent, false ) else SearchResultGridBinding.inflate( - LayoutInflater.from(parent.context), + inflater, parent, false ) //R.layout.search_result_grid_expanded else R.layout.search_result_grid @@ -95,9 +95,15 @@ class SearchAdapter( private val coverHeight: Int = if (compactView) 80.toPx else (resView.itemWidth / 0.68).roundToInt() + private val cardView = when(binding) { + is SearchResultGridExpandedBinding -> binding.imageView + is SearchResultGridBinding -> binding.imageView + else -> null + } + fun bind(card: SearchResponse, position: Int) { if (!compactView) { - binding.root.apply { + cardView?.apply { layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, coverHeight diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index 0c3d481b..eb0082b8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.ui.settings.extensions import android.text.format.Formatter.formatShortFileSize import android.util.Log import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone @@ -13,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.VotingApi.getVotes import com.lagradost.cloudstream3.ui.result.setText @@ -26,10 +26,11 @@ import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.android.synthetic.main.repository_item.view.* import org.junit.Assert import org.junit.Test import java.text.DecimalFormat +import kotlin.math.floor +import kotlin.math.log10 data class PluginViewData( @@ -45,8 +46,10 @@ class PluginAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val layout = if(isTrueTvSettings()) R.layout.repository_item_tv else R.layout.repository_item + val inflated = LayoutInflater.from(parent.context).inflate(layout, parent, false) + return PluginViewHolder( - LayoutInflater.from(parent.context).inflate(layout, parent, false) + RepositoryItemBinding.bind(inflated) // may crash ) } @@ -82,8 +85,10 @@ class PluginAdapter( // Clear glide image because setImageResource doesn't override override fun onViewRecycled(holder: RecyclerView.ViewHolder) { - holder.itemView.entry_icon?.let { pluginIcon -> - GlideApp.with(pluginIcon).clear(pluginIcon) + if (holder is PluginViewHolder) { + holder.binding.entryIcon.let { pluginIcon -> + GlideApp.with(pluginIcon).clear(pluginIcon) + } } super.onViewRecycled(holder) } @@ -112,7 +117,7 @@ class PluginAdapter( fun prettyCount(number: Number): String? { val suffix = charArrayOf(' ', 'k', 'M', 'B', 'T', 'P', 'E') val numValue = number.toLong() - val value = Math.floor(Math.log10(numValue.toDouble())).toInt() + val value = floor(log10(numValue.toDouble())).toInt() val base = value / 3 return if (value >= 3 && base < suffix.size) { DecimalFormat("#0.00").format( @@ -127,8 +132,8 @@ class PluginAdapter( } } - inner class PluginViewHolder(itemView: View) : - RecyclerView.ViewHolder(itemView) { + inner class PluginViewHolder(val binding: RepositoryItemBinding) : + RecyclerView.ViewHolder(binding.root) { fun bind( data: PluginViewData, @@ -138,17 +143,17 @@ class PluginAdapter( val name = metadata.name.removeSuffix("Provider") val alpha = if (disabled) 0.6f else 1f val isLocal = !data.plugin.second.url.startsWith("http") - itemView.main_text?.alpha = alpha - itemView.sub_text?.alpha = alpha + binding.mainText.alpha = alpha + binding.subText.alpha = alpha val drawableInt = if (data.isDownloaded) R.drawable.ic_baseline_delete_outline_24 else R.drawable.netflix_download - itemView.nsfw_marker?.isVisible = metadata.tvTypes?.contains("NSFW") ?: false - itemView.action_button?.setImageResource(drawableInt) + binding.nsfwMarker.isVisible = metadata.tvTypes?.contains("NSFW") ?: false + binding.actionButton.setImageResource(drawableInt) - itemView.action_button?.setOnClickListener { + binding.actionButton.setOnClickListener { iconClickCallback.invoke(data.plugin) } itemView.setOnClickListener { @@ -169,10 +174,11 @@ class PluginAdapter( if (data.isDownloaded) { // On local plugins page the filepath is provided instead of url. - val plugin = PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url] + val plugin = + PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url] if (plugin?.openSettings != null) { - itemView.action_settings?.isVisible = true - itemView.action_settings.setOnClickListener { + binding.actionSettings.isVisible = true + binding.actionSettings.setOnClickListener { try { plugin.openSettings!!.invoke(itemView.context) } catch (e: Throwable) { @@ -185,13 +191,13 @@ class PluginAdapter( } } } else { - itemView.action_settings?.isVisible = false + binding.actionSettings.isVisible = false } } else { - itemView.action_settings?.isVisible = false + binding.actionSettings.isVisible = false } - if (itemView.entry_icon?.setImage(//itemView.entry_icon?.height ?: + if (!binding.entryIcon.setImage(//itemView.entry_icon?.height ?: metadata.iconUrl?.replace( "%size%", "$iconSize" @@ -201,41 +207,47 @@ class PluginAdapter( ), null, errorImageDrawable = R.drawable.ic_baseline_extension_24 - ) != true + ) ) { - itemView.entry_icon?.setImageResource(R.drawable.ic_baseline_extension_24) + binding.entryIcon.setImageResource(R.drawable.ic_baseline_extension_24) } - itemView.ext_version?.isVisible = true - itemView.ext_version?.text = "v${metadata.version}" + binding.extVersion.isVisible = true + binding.extVersion.text = "v${metadata.version}" if (metadata.language.isNullOrBlank()) { - itemView.lang_icon?.isVisible = false + binding.langIcon.isVisible = false } else { - itemView.lang_icon?.isVisible = true - itemView.lang_icon.text = "${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}" + binding.langIcon.isVisible = true + binding.langIcon.text = + "${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}" } - itemView.ext_votes?.isVisible = false + binding.extVotes.isVisible = false if (!isLocal) { ioSafe { metadata.getVotes().main { - itemView.ext_votes?.setText(txt(R.string.extension_rating, prettyCount(it))) - itemView.ext_votes?.isVisible = true + binding.extVotes.setText(txt(R.string.extension_rating, prettyCount(it))) + binding.extVotes.isVisible = true } } } if (metadata.fileSize != null) { - itemView.ext_filesize?.isVisible = true - itemView.ext_filesize?.text = formatShortFileSize(itemView.context, metadata.fileSize) + binding.extFilesize.isVisible = true + binding.extFilesize.text = formatShortFileSize(itemView.context, metadata.fileSize) } else { - itemView.ext_filesize?.isVisible = false + binding.extFilesize.isVisible = false } - itemView.main_text.setText(if(disabled) txt(R.string.single_plugin_disabled, name) else txt(name)) - itemView.sub_text?.isGone = metadata.description.isNullOrBlank() - itemView.sub_text?.text = metadata.description.html() + binding.mainText.setText( + if (disabled) txt( + R.string.single_plugin_disabled, + name + ) else txt(name) + ) + binding.subText.isGone = metadata.description.isNullOrBlank() + binding.subText.text = metadata.description.html() } } } 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 4ed1aee6..3be4b190 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -181,6 +181,50 @@ object UIHelper { } } + /*inline fun bindViewBinding( + inflater: LayoutInflater?, + container: ViewGroup?, + layout: Int + ): Pair { + return try { + val localInflater = inflater ?: container?.context?.let { LayoutInflater.from(it) } + ?: return null to txt( + R.string.unable_to_inflate, + "Requires inflater OR container" + )//throw IllegalArgumentException("Requires inflater OR container")) + + //println("methods: ${T::class.java.methods.map { it.name }}") + val bind = T::class.java.methods.first { it.name == "bind" } + //val inflate = T::class.java.methods.first { it.name == "inflate" } + val root = localInflater.inflate(layout, container, false) + bind.invoke(null, root) as T to null + } catch (t: Throwable) { + logError(t) + val message = txt(R.string.unable_to_inflate, t.message ?: "Primary constructor") + // if the desired layout is not found then we inflate the casted layout + /*try { + val localInflater = inflater ?: container?.context?.let { LayoutInflater.from(it) } + ?: return null to txt( + R.string.unable_to_inflate, + "Requires inflater OR container" + )//throw IllegalArgumentException("Requires inflater OR container")) + + // we don't know what method to use as there are 2, but first *should* always be true + return try { + val inflate = T::class.java.methods.first { it.name == "inflate" } + inflate.invoke(null, localInflater, container, false) as T + } catch (_: Throwable) { + val inflate = T::class.java.methods.last { it.name == "inflate" } + inflate.invoke(null, localInflater, container, false) as T + } to message + } catch (t: Throwable) { + logError(t) + }*/ + + null to message + } + }*/ + fun ImageView?.setImage( url: String?, headers: Map? = null, @@ -190,7 +234,12 @@ object UIHelper { colorCallback: ((Palette) -> Unit)? = null ): Boolean { if (url.isNullOrBlank()) return false - this.setImage(UiImage.Image(url, headers, errorImageDrawable), errorImageDrawable, fadeIn, colorCallback) + this.setImage( + UiImage.Image(url, headers, errorImageDrawable), + errorImageDrawable, + fadeIn, + colorCallback + ) return true } diff --git a/app/src/main/res/layout/activity_main_tv.xml b/app/src/main/res/layout/activity_main_tv.xml index dc29dec9..4e50f464 100644 --- a/app/src/main/res/layout/activity_main_tv.xml +++ b/app/src/main/res/layout/activity_main_tv.xml @@ -41,6 +41,22 @@ + + Qualities Profile background + UI was unable to be created correctly, this is a MAJOR BUG and should be reported immediately %s From 273a947f8ef46c294ff14b4be0aeae1081868e92 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 14 Jul 2023 22:05:13 +0200 Subject: [PATCH 099/570] more views --- .../cloudstream3/ui/home/HomeScrollAdapter.kt | 6 +- .../player/source_priority/PriorityAdapter.kt | 25 +- .../player/source_priority/ProfilesAdapter.kt | 29 +- .../source_priority/QualityProfileDialog.kt | 129 ++-- .../extensions/PluginDetailsFragment.kt | 130 ++-- .../ui/settings/testing/TestResultAdapter.kt | 19 +- .../ui/subtitles/SubtitlesFragment.kt | 607 +++++++++--------- 7 files changed, 491 insertions(+), 454 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt index f296e53d..5902132e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt @@ -11,9 +11,9 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.fragment_home_head_tv.* -import kotlinx.android.synthetic.main.fragment_home_head_tv.view.* -import kotlinx.android.synthetic.main.home_scroll_view.view.* +import kotlinx.android.synthetic.main.home_scroll_view.view.home_scroll_preview +import kotlinx.android.synthetic.main.home_scroll_view.view.home_scroll_preview_tags +import kotlinx.android.synthetic.main.home_scroll_view.view.home_scroll_preview_title class HomeScrollAdapter( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt index 8e0ce67c..fb60ccce 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt @@ -1,14 +1,10 @@ package com.lagradost.cloudstream3.ui.player.source_priority import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.PlayerPrioritizeItemBinding import com.lagradost.cloudstream3.utils.AppUtils -import kotlinx.android.synthetic.main.player_prioritize_item.view.* data class SourcePriority( val data: T, @@ -20,7 +16,8 @@ class PriorityAdapter(override val items: MutableList>) : AppUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return PriorityViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false) + PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false), + //LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false) ) } @@ -31,27 +28,27 @@ class PriorityAdapter(override val items: MutableList>) : } class PriorityViewHolder( - itemView: View, - ) : RecyclerView.ViewHolder(itemView) { + val binding: PlayerPrioritizeItemBinding, + ) : RecyclerView.ViewHolder(binding.root) { fun bind(item: SourcePriority) { - val plusButton: ImageView = itemView.add_button + /* val plusButton: ImageView = itemView.add_button val subtractButton: ImageView = itemView.subtract_button val priorityText: TextView = itemView.priority_text - val priorityNumber: TextView = itemView.priority_number - priorityText.text = item.name + val priorityNumber: TextView = itemView.priority_number*/ + binding.priorityText.text = item.name fun updatePriority() { - priorityNumber.text = item.priority.toString() + binding.priorityNumber.text = item.priority.toString() } updatePriority() - plusButton.setOnClickListener { + binding.addButton.setOnClickListener { // If someone clicks til the integer limit then they deserve to crash. item.priority++ updatePriority() } - subtractButton.setOnClickListener { + binding.subtractButton.setOnClickListener { item.priority-- updatePriority() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt index ff84c1f5..8153d7a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt @@ -8,19 +8,13 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.PlayerQualityProfileItemBinding import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.player_quality_profile_item.view.card_view -import kotlinx.android.synthetic.main.player_quality_profile_item.view.outline -import kotlinx.android.synthetic.main.player_quality_profile_item.view.profile_image_background -import kotlinx.android.synthetic.main.player_quality_profile_item.view.profile_text -import kotlinx.android.synthetic.main.player_quality_profile_item.view.text_is_mobile_data -import kotlinx.android.synthetic.main.player_quality_profile_item.view.text_is_wifi class ProfilesAdapter( override val items: MutableList, @@ -34,8 +28,9 @@ class ProfilesAdapter( }) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProfilesViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.player_quality_profile_item, parent, false) + PlayerQualityProfileItemBinding.inflate(LayoutInflater.from(parent.context),parent,false) + //LayoutInflater.from(parent.context) + // .inflate(R.layout.player_quality_profile_item, parent, false) ) } @@ -52,8 +47,8 @@ class ProfilesAdapter( } inner class ProfilesViewHolder( - itemView: View, - ) : RecyclerView.ViewHolder(itemView) { + val binding: PlayerQualityProfileItemBinding, + ) : RecyclerView.ViewHolder(binding.root) { private val art = listOf( R.drawable.profile_bg_teal, R.drawable.profile_bg_blue, @@ -65,12 +60,12 @@ class ProfilesAdapter( ) fun bind(item: QualityDataHelper.QualityProfile, index: Int) { - val priorityText: TextView = itemView.profile_text - val profileBg: ImageView = itemView.profile_image_background - val wifiText: TextView = itemView.text_is_wifi - val dataText: TextView = itemView.text_is_mobile_data - val outline: View = itemView.outline - val cardView: View = itemView.card_view + val priorityText: TextView = binding.profileText + val profileBg: ImageView = binding.profileImageBackground + val wifiText: TextView = binding.textIsWifi + val dataText: TextView = binding.textIsMobileData + val outline: View = binding.outline + val cardView: View = binding.cardView priorityText.text = item.name.asString(itemView.context) dataText.isVisible = item.type == QualityDataHelper.QualityProfileType.Data diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt index 28a6365f..e3629158 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt @@ -1,20 +1,16 @@ package com.lagradost.cloudstream3.ui.player.source_priority import android.app.Dialog -import android.view.View -import android.widget.TextView import androidx.annotation.StyleRes -import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.PlayerQualityProfileDialogBinding import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfileName import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfiles import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import kotlinx.android.synthetic.main.player_quality_profile_dialog.* class QualityProfileDialog( val activity: FragmentActivity, @@ -24,83 +20,86 @@ class QualityProfileDialog( private val profileSelectionCallback: (QualityDataHelper.QualityProfile) -> Unit ) : Dialog(activity, themeRes) { override fun show() { - setContentView(R.layout.player_quality_profile_dialog) - val profilesRecyclerView: RecyclerView = profiles_recyclerview + + val binding = PlayerQualityProfileDialogBinding.inflate(this.layoutInflater, null, false) + + setContentView(binding.root)//R.layout.player_quality_profile_dialog) + /*val profilesRecyclerView: RecyclerView = profiles_recyclerview val useBtt: View = use_btt val editBtt: View = edit_btt val cancelBtt: View = cancel_btt val defaultBtt: View = set_default_btt val currentProfileText: TextView = currently_selected_profile_text - val selectedItemActionsHolder: View = selected_item_holder - - fun getCurrentProfile(): QualityDataHelper.QualityProfile? { - return (profilesRecyclerView.adapter as? ProfilesAdapter)?.getCurrentProfile() - } - - fun refreshProfiles() { - currentProfileText.text = getProfileName(usedProfile).asString(context) - (profilesRecyclerView.adapter as? ProfilesAdapter)?.updateList(getProfiles()) - } - - profilesRecyclerView.adapter = ProfilesAdapter( - mutableListOf(), - usedProfile, - ) { oldIndex: Int?, newIndex: Int -> - profilesRecyclerView.adapter?.notifyItemChanged(newIndex) - selectedItemActionsHolder.alpha = 1f - if (oldIndex != null) { - profilesRecyclerView.adapter?.notifyItemChanged(oldIndex) + val selectedItemActionsHolder: View = selected_item_holder*/ + binding.apply { + fun getCurrentProfile(): QualityDataHelper.QualityProfile? { + return (profilesRecyclerview.adapter as? ProfilesAdapter)?.getCurrentProfile() } - } - refreshProfiles() - - editBtt.setOnClickListener { - getCurrentProfile()?.let { profile -> - SourcePriorityDialog(context, themeRes, links, profile) { - refreshProfiles() - }.show() + fun refreshProfiles() { + currentlySelectedProfileText.text = getProfileName(usedProfile).asString(context) + (profilesRecyclerview.adapter as? ProfilesAdapter)?.updateList(getProfiles()) + } + + profilesRecyclerview.adapter = ProfilesAdapter( + mutableListOf(), + usedProfile, + ) { oldIndex: Int?, newIndex: Int -> + profilesRecyclerview.adapter?.notifyItemChanged(newIndex) + selectedItemHolder.alpha = 1f + if (oldIndex != null) { + profilesRecyclerview.adapter?.notifyItemChanged(oldIndex) + } + } + + refreshProfiles() + + editBtt.setOnClickListener { + getCurrentProfile()?.let { profile -> + SourcePriorityDialog(context, themeRes, links, profile) { + refreshProfiles() + }.show() + } } - } - defaultBtt.setOnClickListener { - val currentProfile = getCurrentProfile() ?: return@setOnClickListener - val choices = QualityDataHelper.QualityProfileType.values() - .filter { it != QualityDataHelper.QualityProfileType.None } - val choiceNames = choices.map { txt(it.stringRes).asString(context) } + setDefaultBtt.setOnClickListener { + val currentProfile = getCurrentProfile() ?: return@setOnClickListener + val choices = QualityDataHelper.QualityProfileType.values() + .filter { it != QualityDataHelper.QualityProfileType.None } + val choiceNames = choices.map { txt(it.stringRes).asString(context) } - activity.showBottomDialog( - choiceNames, - choices.indexOf(currentProfile.type), - txt(R.string.set_default).asString(context), - false, - {}, - { index -> - val pickedChoice = choices.getOrNull(index) ?: return@showBottomDialog - // Remove previous picks - if (pickedChoice.unique) { - getProfiles().filter { it.type == pickedChoice }.forEach { - QualityDataHelper.setQualityProfileType(it.id, null) + activity.showBottomDialog( + choiceNames, + choices.indexOf(currentProfile.type), + txt(R.string.set_default).asString(context), + false, + {}, + { index -> + val pickedChoice = choices.getOrNull(index) ?: return@showBottomDialog + // Remove previous picks + if (pickedChoice.unique) { + getProfiles().filter { it.type == pickedChoice }.forEach { + QualityDataHelper.setQualityProfileType(it.id, null) + } } - } - QualityDataHelper.setQualityProfileType(currentProfile.id, pickedChoice) - refreshProfiles() - }) - } + QualityDataHelper.setQualityProfileType(currentProfile.id, pickedChoice) + refreshProfiles() + }) + } - cancelBtt.setOnClickListener { - this.dismissSafe() - } + cancelBtt.setOnClickListener { + this@QualityProfileDialog.dismissSafe() + } - useBtt.setOnClickListener { - getCurrentProfile()?.let { - profileSelectionCallback.invoke(it) - this.dismissSafe() + useBtt.setOnClickListener { + getCurrentProfile()?.let { + profileSelectionCallback.invoke(it) + this@QualityProfileDialog.dismissSafe() + } } } - super.show() } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt index 9729b4de..00e1806d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt @@ -2,30 +2,29 @@ package com.lagradost.cloudstream3.ui.settings.extensions import android.content.res.ColorStateList import android.os.Bundle -import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import android.text.format.Formatter.formatFileSize +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.UIHelper.setImage -import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.android.synthetic.main.fragment_plugin_details.* -import android.text.format.Formatter.formatFileSize -import android.util.Log import androidx.core.view.isVisible +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentPluginDetailsBinding +import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.VotingApi +import com.lagradost.cloudstream3.plugins.VotingApi.canVote import com.lagradost.cloudstream3.plugins.VotingApi.getVoteType import com.lagradost.cloudstream3.plugins.VotingApi.getVotes import com.lagradost.cloudstream3.plugins.VotingApi.vote import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main -import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute -import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser -import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.plugins.VotingApi.canVote import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso -import kotlinx.android.synthetic.main.repository_item.view.* +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import com.lagradost.cloudstream3.utils.UIHelper.setImage +import com.lagradost.cloudstream3.utils.UIHelper.toPx class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragment() { @@ -43,18 +42,27 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen } } + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + + var binding: FragmentPluginDetailsBinding? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_plugin_details, container, false) - + ): View { + val localBinding = FragmentPluginDetailsBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.fragment_plugin_details, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val metadata = data.plugin.second - if (plugin_icon?.setImage(//plugin_icon?.height ?: + binding?.apply { + if (!pluginIcon.setImage(//plugin_icon?.height ?: metadata.iconUrl?.replace( "%size%", "$iconSize" @@ -64,23 +72,33 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen ), null, errorImageDrawable = R.drawable.ic_baseline_extension_24 - ) != true + ) ) { - plugin_icon?.setImageResource(R.drawable.ic_baseline_extension_24) + pluginIcon.setImageResource(R.drawable.ic_baseline_extension_24) } - plugin_name?.text = metadata.name.removeSuffix("Provider") - plugin_version?.text = metadata.version.toString() - plugin_description?.text = metadata.description ?: getString(R.string.no_data) - plugin_size?.text = if (metadata.fileSize == null) getString(R.string.no_data) else formatFileSize(context, metadata.fileSize) - plugin_author?.text = if (metadata.authors.isEmpty()) getString(R.string.no_data) else metadata.authors.joinToString(", ") - plugin_status?.text = resources.getStringArray(R.array.extension_statuses)[metadata.status] - plugin_types?.text = if ((metadata.tvTypes == null) || metadata.tvTypes.isEmpty()) getString(R.string.no_data) else metadata.tvTypes.joinToString(", ") - plugin_lang?.text = if (metadata.language == null) - getString(R.string.no_data) + pluginName.text = metadata.name.removeSuffix("Provider") + pluginVersion.text = metadata.version.toString() + pluginDescription.text = metadata.description ?: getString(R.string.no_data) + pluginSize.text = + if (metadata.fileSize == null) getString(R.string.no_data) else formatFileSize( + context, + metadata.fileSize + ) + pluginAuthor.text = + if (metadata.authors.isEmpty()) getString(R.string.no_data) else metadata.authors.joinToString( + ", " + ) + pluginStatus.text = resources.getStringArray(R.array.extension_statuses)[metadata.status] + pluginTypes.text = + if (metadata.tvTypes.isNullOrEmpty()) getString(R.string.no_data) else metadata.tvTypes.joinToString( + ", " + ) + pluginLang.text = if (metadata.language == null) + getString(R.string.no_data) else - "${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}" + "${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}" - github_btn.setOnClickListener { + githubBtn.setOnClickListener { if (metadata.repositoryUrl != null) { openBrowser(metadata.repositoryUrl) } @@ -93,10 +111,11 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen if (data.isDownloaded) { // On local plugins page the filepath is provided instead of url. - val plugin = PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url] + val plugin = + PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url] if (plugin?.openSettings != null && context != null) { - action_settings?.isVisible = true - action_settings.setOnClickListener { + actionSettings.isVisible = true + actionSettings.setOnClickListener { try { plugin.openSettings!!.invoke(requireContext()) } catch (e: Throwable) { @@ -109,10 +128,10 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen } } } else { - action_settings?.isVisible = false + actionSettings.isVisible = false } } else { - action_settings?.isVisible = false + actionSettings.isVisible = false } upvote.setOnClickListener { @@ -136,23 +155,40 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen updateVoting(it) } } + } } private fun updateVoting(value: Int) { val metadata = data.plugin.second - plugin_votes.text = value.toString() - when (metadata.getVoteType()) { - VotingApi.VoteType.UPVOTE -> { - upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary) - downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white) - } - VotingApi.VoteType.DOWNVOTE -> { - downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary) - upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white) - } - VotingApi.VoteType.NONE -> { - upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white) - downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white) + binding?.apply { + pluginVotes.text = value.toString() + when (metadata.getVoteType()) { + VotingApi.VoteType.UPVOTE -> { + upvote.imageTintList = ColorStateList.valueOf( + context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary + ) + downvote.imageTintList = ColorStateList.valueOf( + context?.colorFromAttribute(R.attr.white) ?: R.color.white + ) + } + + VotingApi.VoteType.DOWNVOTE -> { + downvote.imageTintList = ColorStateList.valueOf( + context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary + ) + upvote.imageTintList = ColorStateList.valueOf( + context?.colorFromAttribute(R.attr.white) ?: R.color.white + ) + } + + VotingApi.VoteType.NONE -> { + upvote.imageTintList = ColorStateList.valueOf( + context?.colorFromAttribute(R.attr.white) ?: R.color.white + ) + downvote.imageTintList = ColorStateList.valueOf( + context?.colorFromAttribute(R.attr.white) ?: R.color.white + ) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt index d04e2379..83480542 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt @@ -10,19 +10,20 @@ import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.ProviderTestItemBinding import com.lagradost.cloudstream3.mvvm.getAllMessages import com.lagradost.cloudstream3.mvvm.getStackTracePretty import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.TestingUtils -import kotlinx.android.synthetic.main.provider_test_item.view.* class TestResultAdapter(override val items: MutableList>) : AppUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProviderTestViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.provider_test_item, parent, false), + ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent,false) + //LayoutInflater.from(parent.context) + // .inflate(R.layout.provider_test_item, parent, false), ) } @@ -35,12 +36,12 @@ class TestResultAdapter(override val items: MutableList - val suffix = "dp" - val elevationTypes = listOf( - Pair(0, textView.context.getString(R.string.none)), - Pair(10, "10$suffix"), - Pair(20, "20$suffix"), - Pair(30, "30$suffix"), - Pair(40, "40$suffix"), - Pair(50, "50$suffix"), - Pair(60, "60$suffix"), - Pair(70, "70$suffix"), - Pair(80, "80$suffix"), - Pair(90, "90$suffix"), - Pair(100, "100$suffix"), - ) - - //showBottomDialog - activity?.showDialog( - elevationTypes.map { it.second }, - elevationTypes.map { it.first }.indexOf(state.elevation), - (textView as TextView).text.toString(), - false, - dismissCallback - ) { index -> - state.elevation = elevationTypes.map { it.first }[index] - textView.context.updateState() + val dismissCallback = { if (hide) activity?.hideSystemUI() } - } - subs_subtitle_elevation.setOnLongClickListener { - state.elevation = DEF_SUBS_ELEVATION - it.context.updateState() - showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) - return@setOnLongClickListener true - } + subsSubtitleElevation.setFocusableInTv() + subsSubtitleElevation.setOnClickListener { textView -> + val suffix = "dp" + val elevationTypes = listOf( + Pair(0, textView.context.getString(R.string.none)), + Pair(10, "10$suffix"), + Pair(20, "20$suffix"), + Pair(30, "30$suffix"), + Pair(40, "40$suffix"), + Pair(50, "50$suffix"), + Pair(60, "60$suffix"), + Pair(70, "70$suffix"), + Pair(80, "80$suffix"), + Pair(90, "90$suffix"), + Pair(100, "100$suffix"), + ) - subs_edge_type.setFocusableInTv() - subs_edge_type.setOnClickListener { textView -> - val edgeTypes = listOf( - Pair( - CaptionStyleCompat.EDGE_TYPE_NONE, - textView.context.getString(R.string.subtitles_none) - ), - Pair( - CaptionStyleCompat.EDGE_TYPE_OUTLINE, - textView.context.getString(R.string.subtitles_outline) - ), - Pair( - CaptionStyleCompat.EDGE_TYPE_DEPRESSED, - textView.context.getString(R.string.subtitles_depressed) - ), - Pair( - CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW, - textView.context.getString(R.string.subtitles_shadow) - ), - Pair( - CaptionStyleCompat.EDGE_TYPE_RAISED, - textView.context.getString(R.string.subtitles_raised) - ), - ) - - //showBottomDialog - activity?.showDialog( - edgeTypes.map { it.second }, - edgeTypes.map { it.first }.indexOf(state.edgeType), - (textView as TextView).text.toString(), - false, - dismissCallback - ) { index -> - state.edgeType = edgeTypes.map { it.first }[index] - textView.context.updateState() - } - } - - subs_edge_type.setOnLongClickListener { - state.edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE - it.context.updateState() - showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) - return@setOnLongClickListener true - } - - subs_font_size.setFocusableInTv() - subs_font_size.setOnClickListener { textView -> - val suffix = "sp" - val fontSizes = listOf( - Pair(null, textView.context.getString(R.string.normal)), - Pair(6f, "6$suffix"), - Pair(7f, "7$suffix"), - Pair(8f, "8$suffix"), - Pair(9f, "9$suffix"), - Pair(10f, "10$suffix"), - Pair(11f, "11$suffix"), - Pair(12f, "12$suffix"), - Pair(13f, "13$suffix"), - Pair(14f, "14$suffix"), - Pair(15f, "15$suffix"), - Pair(16f, "16$suffix"), - Pair(17f, "17$suffix"), - Pair(18f, "18$suffix"), - Pair(19f, "19$suffix"), - Pair(20f, "20$suffix"), - Pair(21f, "21$suffix"), - Pair(22f, "22$suffix"), - Pair(23f, "23$suffix"), - Pair(24f, "24$suffix"), - Pair(25f, "25$suffix"), - Pair(26f, "26$suffix"), - Pair(28f, "28$suffix"), - Pair(30f, "30$suffix"), - Pair(32f, "32$suffix"), - Pair(34f, "34$suffix"), - Pair(36f, "36$suffix"), - Pair(38f, "38$suffix"), - Pair(40f, "40$suffix"), - Pair(42f, "42$suffix"), - Pair(44f, "44$suffix"), - Pair(48f, "48$suffix"), - Pair(60f, "60$suffix"), - ) - - //showBottomDialog - activity?.showDialog( - fontSizes.map { it.second }, - fontSizes.map { it.first }.indexOf(state.fixedTextSize), - (textView as TextView).text.toString(), - false, - dismissCallback - ) { index -> - state.fixedTextSize = fontSizes.map { it.first }[index] - //textView.context.updateState() // font size not changed - } - } - - subtitles_remove_bloat?.isChecked = state.removeBloat - subtitles_remove_bloat?.setOnCheckedChangeListener { _, b -> - state.removeBloat = b - } - subtitles_uppercase?.isChecked = state.upperCase - subtitles_uppercase?.setOnCheckedChangeListener { _, b -> - state.upperCase = b - context?.updateState() - } - - subtitles_remove_captions?.isChecked = state.removeCaptions - subtitles_remove_captions?.setOnCheckedChangeListener { _, b -> - state.removeCaptions = b - } - - subs_font_size.setOnLongClickListener { _ -> - state.fixedTextSize = null - //textView.context.updateState() // font size not changed - showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) - return@setOnLongClickListener true - } - - //Fetch current value from preference - context?.let { ctx -> - subtitles_filter_sub_lang?.isChecked = - PreferenceManager.getDefaultSharedPreferences(ctx) - .getBoolean(getString(R.string.filter_sub_lang_key), false) - } - - subtitles_filter_sub_lang?.setOnCheckedChangeListener { _, b -> - context?.let { ctx -> - PreferenceManager.getDefaultSharedPreferences(ctx) - .edit() - .putBoolean(getString(R.string.filter_sub_lang_key), b) - .apply() - } - } - - subs_font.setFocusableInTv() - subs_font.setOnClickListener { textView -> - val fontTypes = listOf( - Pair(null, textView.context.getString(R.string.normal)), - Pair(R.font.trebuchet_ms, "Trebuchet MS"), - Pair(R.font.netflix_sans, "Netflix Sans"), - Pair(R.font.google_sans, "Google Sans"), - Pair(R.font.open_sans, "Open Sans"), - Pair(R.font.futura, "Futura"), - Pair(R.font.consola, "Consola"), - Pair(R.font.gotham, "Gotham"), - Pair(R.font.lucida_grande, "Lucida Grande"), - Pair(R.font.stix_general, "STIX General"), - Pair(R.font.times_new_roman, "Times New Roman"), - Pair(R.font.verdana, "Verdana"), - Pair(R.font.ubuntu_regular, "Ubuntu"), - Pair(R.font.comic_sans, "Comic Sans"), - Pair(R.font.poppins_regular, "Poppins"), - ) - val savedFontTypes = textView.context.getSavedFonts() - - val currentIndex = - savedFontTypes.indexOfFirst { it.absolutePath == state.typefaceFilePath } - .let { index -> - if (index == -1) - fontTypes.indexOfFirst { it.first == state.typeface } - else index + fontTypes.size - } - - //showBottomDialog - activity?.showDialog( - fontTypes.map { it.second } + savedFontTypes.map { it.name }, - currentIndex, - (textView as TextView).text.toString(), - false, - dismissCallback - ) { index -> - if (index < fontTypes.size) { - state.typeface = fontTypes[index].first - state.typefaceFilePath = null - } else { - state.typefaceFilePath = savedFontTypes[index - fontTypes.size].absolutePath - state.typeface = null + //showBottomDialog + activity?.showDialog( + elevationTypes.map { it.second }, + elevationTypes.map { it.first }.indexOf(state.elevation), + (textView as TextView).text.toString(), + false, + dismissCallback + ) { index -> + state.elevation = elevationTypes.map { it.first }[index] + textView.context.updateState() + if (hide) + activity?.hideSystemUI() } + } + + subsSubtitleElevation.setOnLongClickListener { + state.elevation = DEF_SUBS_ELEVATION + it.context.updateState() + showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) + return@setOnLongClickListener true + } + + subsEdgeType.setFocusableInTv() + subsEdgeType.setOnClickListener { textView -> + val edgeTypes = listOf( + Pair( + CaptionStyleCompat.EDGE_TYPE_NONE, + textView.context.getString(R.string.subtitles_none) + ), + Pair( + CaptionStyleCompat.EDGE_TYPE_OUTLINE, + textView.context.getString(R.string.subtitles_outline) + ), + Pair( + CaptionStyleCompat.EDGE_TYPE_DEPRESSED, + textView.context.getString(R.string.subtitles_depressed) + ), + Pair( + CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW, + textView.context.getString(R.string.subtitles_shadow) + ), + Pair( + CaptionStyleCompat.EDGE_TYPE_RAISED, + textView.context.getString(R.string.subtitles_raised) + ), + ) + + //showBottomDialog + activity?.showDialog( + edgeTypes.map { it.second }, + edgeTypes.map { it.first }.indexOf(state.edgeType), + (textView as TextView).text.toString(), + false, + dismissCallback + ) { index -> + state.edgeType = edgeTypes.map { it.first }[index] + textView.context.updateState() + } + } + + subsEdgeType.setOnLongClickListener { + state.edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE + it.context.updateState() + showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) + return@setOnLongClickListener true + } + + subsFontSize.setFocusableInTv() + subsFontSize.setOnClickListener { textView -> + val suffix = "sp" + val fontSizes = listOf( + Pair(null, textView.context.getString(R.string.normal)), + Pair(6f, "6$suffix"), + Pair(7f, "7$suffix"), + Pair(8f, "8$suffix"), + Pair(9f, "9$suffix"), + Pair(10f, "10$suffix"), + Pair(11f, "11$suffix"), + Pair(12f, "12$suffix"), + Pair(13f, "13$suffix"), + Pair(14f, "14$suffix"), + Pair(15f, "15$suffix"), + Pair(16f, "16$suffix"), + Pair(17f, "17$suffix"), + Pair(18f, "18$suffix"), + Pair(19f, "19$suffix"), + Pair(20f, "20$suffix"), + Pair(21f, "21$suffix"), + Pair(22f, "22$suffix"), + Pair(23f, "23$suffix"), + Pair(24f, "24$suffix"), + Pair(25f, "25$suffix"), + Pair(26f, "26$suffix"), + Pair(28f, "28$suffix"), + Pair(30f, "30$suffix"), + Pair(32f, "32$suffix"), + Pair(34f, "34$suffix"), + Pair(36f, "36$suffix"), + Pair(38f, "38$suffix"), + Pair(40f, "40$suffix"), + Pair(42f, "42$suffix"), + Pair(44f, "44$suffix"), + Pair(48f, "48$suffix"), + Pair(60f, "60$suffix"), + ) + + //showBottomDialog + activity?.showDialog( + fontSizes.map { it.second }, + fontSizes.map { it.first }.indexOf(state.fixedTextSize), + (textView as TextView).text.toString(), + false, + dismissCallback + ) { index -> + state.fixedTextSize = fontSizes.map { it.first }[index] + //textView.context.updateState() // font size not changed + } + } + + subtitlesRemoveBloat.isChecked = state.removeBloat + subtitlesRemoveBloat.setOnCheckedChangeListener { _, b -> + state.removeBloat = b + } + subtitlesUppercase.isChecked = state.upperCase + subtitlesUppercase.setOnCheckedChangeListener { _, b -> + state.upperCase = b + context?.updateState() + } + + subtitlesRemoveCaptions.isChecked = state.removeCaptions + subtitlesRemoveCaptions.setOnCheckedChangeListener { _, b -> + state.removeCaptions = b + } + + subsFontSize.setOnLongClickListener { _ -> + state.fixedTextSize = null + //textView.context.updateState() // font size not changed + showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) + return@setOnLongClickListener true + } + + //Fetch current value from preference + context?.let { ctx -> + subtitlesFilterSubLang.isChecked = + PreferenceManager.getDefaultSharedPreferences(ctx) + .getBoolean(getString(R.string.filter_sub_lang_key), false) + } + + subtitlesFilterSubLang.setOnCheckedChangeListener { _, b -> + context?.let { ctx -> + PreferenceManager.getDefaultSharedPreferences(ctx) + .edit() + .putBoolean(getString(R.string.filter_sub_lang_key), b) + .apply() + } + } + + subsFont.setFocusableInTv() + subsFont.setOnClickListener { textView -> + val fontTypes = listOf( + Pair(null, textView.context.getString(R.string.normal)), + Pair(R.font.trebuchet_ms, "Trebuchet MS"), + Pair(R.font.netflix_sans, "Netflix Sans"), + Pair(R.font.google_sans, "Google Sans"), + Pair(R.font.open_sans, "Open Sans"), + Pair(R.font.futura, "Futura"), + Pair(R.font.consola, "Consola"), + Pair(R.font.gotham, "Gotham"), + Pair(R.font.lucida_grande, "Lucida Grande"), + Pair(R.font.stix_general, "STIX General"), + Pair(R.font.times_new_roman, "Times New Roman"), + Pair(R.font.verdana, "Verdana"), + Pair(R.font.ubuntu_regular, "Ubuntu"), + Pair(R.font.comic_sans, "Comic Sans"), + Pair(R.font.poppins_regular, "Poppins"), + ) + val savedFontTypes = textView.context.getSavedFonts() + + val currentIndex = + savedFontTypes.indexOfFirst { it.absolutePath == state.typefaceFilePath } + .let { index -> + if (index == -1) + fontTypes.indexOfFirst { it.first == state.typeface } + else index + fontTypes.size + } + + //showBottomDialog + activity?.showDialog( + fontTypes.map { it.second } + savedFontTypes.map { it.name }, + currentIndex, + (textView as TextView).text.toString(), + false, + dismissCallback + ) { index -> + if (index < fontTypes.size) { + state.typeface = fontTypes[index].first + state.typefaceFilePath = null + } else { + state.typefaceFilePath = savedFontTypes[index - fontTypes.size].absolutePath + state.typeface = null + } + textView.context.updateState() + } + } + + subsFont.setOnLongClickListener { textView -> + state.typeface = null + state.typefaceFilePath = null textView.context.updateState() + showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) + return@setOnLongClickListener true } - } - subs_font.setOnLongClickListener { textView -> - state.typeface = null - state.typefaceFilePath = null - textView.context.updateState() - showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) - return@setOnLongClickListener true - } + subsAutoSelectLanguage.setFocusableInTv() + subsAutoSelectLanguage.setOnClickListener { textView -> + val langMap = arrayListOf( + SubtitleHelper.Language639( + textView.context.getString(R.string.none), + textView.context.getString(R.string.none), + "", + "", + "", + "", + "" + ), + ) + langMap.addAll(SubtitleHelper.languages) - subs_auto_select_language.setFocusableInTv() - subs_auto_select_language.setOnClickListener { textView -> - val langMap = arrayListOf( - SubtitleHelper.Language639( - textView.context.getString(R.string.none), - textView.context.getString(R.string.none), - "", - "", - "", - "", - "" - ), - ) - langMap.addAll(SubtitleHelper.languages) - - val lang639_1 = langMap.map { it.ISO_639_1 } - activity?.showDialog( - langMap.map { it.languageName }, - lang639_1.indexOf(getAutoSelectLanguageISO639_1()), - (textView as TextView).text.toString(), - true, - dismissCallback - ) { index -> - setKey(SUBTITLE_AUTO_SELECT_KEY, lang639_1[index]) + val lang639_1 = langMap.map { it.ISO_639_1 } + activity?.showDialog( + langMap.map { it.languageName }, + lang639_1.indexOf(getAutoSelectLanguageISO639_1()), + (textView as TextView).text.toString(), + true, + dismissCallback + ) { index -> + setKey(SUBTITLE_AUTO_SELECT_KEY, lang639_1[index]) + } } - } - subs_auto_select_language.setOnLongClickListener { - setKey(SUBTITLE_AUTO_SELECT_KEY, "en") - showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) - return@setOnLongClickListener true - } - - subs_download_languages.setFocusableInTv() - subs_download_languages.setOnClickListener { textView -> - val langMap = SubtitleHelper.languages - val lang639_1 = langMap.map { it.ISO_639_1 } - val keys = getDownloadSubsLanguageISO639_1() - val keyMap = keys.map { lang639_1.indexOf(it) }.filter { it >= 0 } - - activity?.showMultiDialog( - langMap.map { it.languageName }, - keyMap, - (textView as TextView).text.toString(), - dismissCallback - ) { indexList -> - setKey(SUBTITLE_DOWNLOAD_KEY, indexList.map { lang639_1[it] }.toList()) + subsAutoSelectLanguage.setOnLongClickListener { + setKey(SUBTITLE_AUTO_SELECT_KEY, "en") + showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) + return@setOnLongClickListener true } - } - subs_download_languages.setOnLongClickListener { - setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en")) + subsDownloadLanguages.setFocusableInTv() + subsDownloadLanguages.setOnClickListener { textView -> + val langMap = SubtitleHelper.languages + val lang639_1 = langMap.map { it.ISO_639_1 } + val keys = getDownloadSubsLanguageISO639_1() + val keyMap = keys.map { lang639_1.indexOf(it) }.filter { it >= 0 } - showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) - return@setOnLongClickListener true - } + activity?.showMultiDialog( + langMap.map { it.languageName }, + keyMap, + (textView as TextView).text.toString(), + dismissCallback + ) { indexList -> + setKey(SUBTITLE_DOWNLOAD_KEY, indexList.map { lang639_1[it] }.toList()) + } + } - cancel_btt.setOnClickListener { - activity?.popCurrentPage() - } + subsDownloadLanguages.setOnLongClickListener { + setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en")) - apply_btt.setOnClickListener { - it.context.saveStyle(state) - applyStyleEvent.invoke(state) - it.context.fromSaveToStyle(state) - activity?.popCurrentPage() + showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) + return@setOnLongClickListener true + } + + cancelBtt.setOnClickListener { + activity?.popCurrentPage() + } + + applyBtt.setOnClickListener { + it.context.saveStyle(state) + applyStyleEvent.invoke(state) + it.context.fromSaveToStyle(state) + activity?.popCurrentPage() + } } } } From 04f52f4a6d3eab60b714479e3bdc647d54439209 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 15 Jul 2023 03:25:32 +0200 Subject: [PATCH 100/570] added tests for layout --- app/build.gradle.kts | 1 + .../cloudstream3/ExampleInstrumentedTest.kt | 63 +++++++++++++++++++ .../lagradost/cloudstream3/MainActivity.kt | 17 ++++- .../ui/home/HomeChildItemAdapter.kt | 2 +- .../ui/home/HomeParentItemAdapterPreview.kt | 19 +++++- .../res/layout/home_result_grid_expanded.xml | 13 +++- 6 files changed, 107 insertions(+), 8 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d5364045..288add26 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -142,6 +142,7 @@ dependencies { testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.3") androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") + androidTestImplementation("androidx.test:core") //implementation("io.karn:khttp-android:0.1.2") //okhttp instead // implementation("org.jsoup:jsoup:1.13.1") diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index 92042d60..f28018d1 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -1,6 +1,19 @@ package com.lagradost.cloudstream3 +import android.app.Activity +import android.os.Bundle +import android.os.PersistableBundle +import android.view.LayoutInflater +import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.viewbinding.ViewBinding +import com.lagradost.cloudstream3.databinding.FragmentHomeBinding +import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding +import com.lagradost.cloudstream3.databinding.FragmentSearchBinding +import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding +import com.lagradost.cloudstream3.databinding.HomeResultGridBinding +import com.lagradost.cloudstream3.databinding.RepositoryItemBinding +import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.TestingUtils import kotlinx.coroutines.runBlocking @@ -8,11 +21,18 @@ import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith + /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ +class TestApplication : Activity() { + override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { + super.onCreate(savedInstanceState, persistentState) + } +} + @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { private fun getAllProviders(): List { @@ -26,6 +46,49 @@ class ExampleInstrumentedTest { println("Done providersExist") } + @Throws + private inline fun testAllLayouts( + activity: Activity, + vararg layouts: Int + ) { + + val bind = T::class.java.methods.first { it.name == "bind" } + val inflater = LayoutInflater.from(activity) + for (layout in layouts) { + val root = inflater.inflate(layout, null, false) + bind.invoke(null, root) + } + } + + @Test + @Throws + fun layoutTest() { + ActivityScenario.launch(MainActivity::class.java).use { scenario -> + scenario.onActivity { activity: MainActivity -> + // FragmentHomeHeadBinding and FragmentHomeHeadTvBinding CANT be the same + //testAllLayouts(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv) + //testAllLayouts(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv) + + // main cant be tested + // testAllLayouts(activity,R.layout.activity_main, R.layout.activity_main_tv) + // testAllLayouts(activity,R.layout.activity_main, R.layout.activity_main_tv) + //testAllLayouts(activity, R.layout.activity_main_tv) + + testAllLayouts(activity, R.layout.repository_item_tv, R.layout.repository_item) + testAllLayouts(activity, R.layout.repository_item_tv, R.layout.repository_item) + + testAllLayouts(activity, R.layout.fragment_home_tv, R.layout.fragment_home) + testAllLayouts(activity, R.layout.fragment_home_tv, R.layout.fragment_home) + + testAllLayouts(activity, R.layout.fragment_search_tv, R.layout.fragment_search) + testAllLayouts(activity, R.layout.fragment_search_tv, R.layout.fragment_search) + + testAllLayouts(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) + //testAllLayouts(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) ??? fails ??? + } + } + } + @Test @Throws(AssertionError::class) fun providerCorrectData() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 4a7a28ad..c1223415 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -97,6 +97,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadRepository import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -753,13 +754,25 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (isCastApiAvailable()) { mSessionManager = CastContext.getSharedInstance(this).sessionManager } - } catch (e: Exception) { - logError(e) + } catch (t: Throwable) { + logError(t) } window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) updateTv() + // backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting? + try { + val appVer = BuildConfig.VERSION_NAME + val lastAppAutoBackup = getKey("VERSION_NAME") ?: 0 + if (appVer != lastAppAutoBackup) { + setKey("VERSION_NAME", BuildConfig.VERSION_NAME) + backup() + } + } catch (t : Throwable) { + logError(t) + } + // just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH binding = try { if (isTvSettings()) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index b90a4e43..1e04acf0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -87,7 +87,7 @@ class HomeChildItemAdapter( else -> null } - (itemView.image_holder ?: itemView.background_card)?.apply { + (itemView.background_card)?.apply { val min = 114.toPx val max = 180.toPx 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 715f1867..9bbfbb37 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 @@ -32,15 +32,28 @@ 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.view.* -import kotlinx.android.synthetic.main.fragment_home_head.view.* +import kotlinx.android.synthetic.main.activity_main.view.nav_rail_view +import kotlinx.android.synthetic.main.fragment_home_head.view.home_bookmark_parent_item_title import kotlinx.android.synthetic.main.fragment_home_head.view.home_bookmarked_child_recyclerview +import kotlinx.android.synthetic.main.fragment_home_head.view.home_preview_bookmark +import kotlinx.android.synthetic.main.fragment_home_head.view.home_preview_image +import kotlinx.android.synthetic.main.fragment_home_head.view.home_preview_info +import kotlinx.android.synthetic.main.fragment_home_head.view.home_preview_play +import kotlinx.android.synthetic.main.fragment_home_head.view.home_search 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 import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_plan_to_watch_btt import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_change_api +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_change_api2 +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_description +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_hidden_next_focus +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_hidden_prev_focus +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_info_btt +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_play_btt +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_tags +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_text import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_viewpager import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_completed_btt import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_dropped_btt diff --git a/app/src/main/res/layout/home_result_grid_expanded.xml b/app/src/main/res/layout/home_result_grid_expanded.xml index b697c1de..3c3804a5 100644 --- a/app/src/main/res/layout/home_result_grid_expanded.xml +++ b/app/src/main/res/layout/home_result_grid_expanded.xml @@ -7,7 +7,7 @@ + Date: Sat, 15 Jul 2023 03:27:25 +0200 Subject: [PATCH 101/570] mini fix --- app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index c1223415..a2a24243 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -764,7 +764,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting? try { val appVer = BuildConfig.VERSION_NAME - val lastAppAutoBackup = getKey("VERSION_NAME") ?: 0 + val lastAppAutoBackup : String = getKey("VERSION_NAME") ?: "" if (appVer != lastAppAutoBackup) { setKey("VERSION_NAME", BuildConfig.VERSION_NAME) backup() From f209c7286e34e2640238c5d240170c8569b506da Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 15 Jul 2023 20:00:09 +0200 Subject: [PATCH 102/570] more viewbindings + result fix + more tests --- .../cloudstream3/ExampleInstrumentedTest.kt | 17 ++++ .../ui/home/HomeChildItemAdapter.kt | 22 ++--- .../ui/home/HomeParentItemAdapter.kt | 31 +++--- .../ui/home/HomeParentItemAdapterPreview.kt | 5 +- .../cloudstream3/ui/home/HomeScrollAdapter.kt | 59 +++++++----- .../cloudstream3/ui/library/PageAdapter.kt | 12 +-- .../ui/library/ViewpagerAdapter.kt | 2 +- .../ui/search/SearchResultBuilder.kt | 46 +++++---- .../utils/SingleSelectionHelper.kt | 95 +++++++++++++------ .../main/res/layout/search_result_grid.xml | 22 ++++- .../layout/search_result_grid_expanded.xml | 36 +++---- app/src/main/res/values/styles.xml | 7 +- 12 files changed, 214 insertions(+), 140 deletions(-) diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index f28018d1..68418704 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -12,8 +12,14 @@ import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding import com.lagradost.cloudstream3.databinding.FragmentSearchBinding import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding import com.lagradost.cloudstream3.databinding.HomeResultGridBinding +import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding +import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding +import com.lagradost.cloudstream3.databinding.HomepageParentBinding +import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding +import com.lagradost.cloudstream3.databinding.SearchResultGridBinding +import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.TestingUtils import kotlinx.coroutines.runBlocking @@ -85,6 +91,17 @@ class ExampleInstrumentedTest { testAllLayouts(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) //testAllLayouts(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) ??? fails ??? + + testAllLayouts(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded) + testAllLayouts(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded) + + + // testAllLayouts(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv) + // testAllLayouts(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv) + + testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent) + testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent) + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index 1e04acf0..92bc242d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -1,22 +1,20 @@ package com.lagradost.cloudstream3.ui.home import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.databinding.HomeResultGridBinding import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.android.synthetic.main.home_result_grid.view.background_card -import kotlinx.android.synthetic.main.home_result_grid_expanded.view.* class HomeChildItemAdapter( val cardList: MutableList, - private val overrideLayout: Int? = null, + private val nextFocusUp: Int? = null, private val nextFocusDown: Int? = null, private val clickCallback: (SearchClickCallback) -> Unit, @@ -26,11 +24,13 @@ class HomeChildItemAdapter( var hasNext: Boolean = false override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val layout = overrideLayout - ?: if (parent.context.IsBottomLayout()) R.layout.home_result_grid_expanded else R.layout.home_result_grid + val layout = if (parent.context.IsBottomLayout()) R.layout.home_result_grid_expanded else R.layout.home_result_grid + + val root = LayoutInflater.from(parent.context).inflate(layout, parent, false) + val binding = HomeResultGridBinding.bind(root) return CardViewHolder( - LayoutInflater.from(parent.context).inflate(layout, parent, false), + binding, clickCallback, itemCount, nextFocusUp, @@ -69,14 +69,14 @@ class HomeChildItemAdapter( class CardViewHolder constructor( - itemView: View, + val binding: HomeResultGridBinding, private val clickCallback: (SearchClickCallback) -> Unit, var itemCount: Int, private val nextFocusUp: Int? = null, private val nextFocusDown: Int? = null, private val isHorizontal: Boolean = false ) : - RecyclerView.ViewHolder(itemView) { + RecyclerView.ViewHolder(binding.root) { fun bind(card: SearchResponse, position: Int) { @@ -87,7 +87,7 @@ class HomeChildItemAdapter( else -> null } - (itemView.background_card)?.apply { + binding.backgroundCard.apply { val min = 114.toPx val max = 180.toPx @@ -119,7 +119,7 @@ class HomeChildItemAdapter( itemView.tag = position if (position == 0) { // to fix tv - itemView.background_card?.nextFocusLeftId = R.id.nav_rail_view + binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view } //val ani = ScaleAnimation(0.9f, 1.0f, 0.9f, 1f) //ani.fillAfter = true 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 7ce9e67d..d05b4cab 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 @@ -10,18 +10,12 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.HomepageParentBinding import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable -import kotlinx.android.synthetic.main.activity_main_tv.* -import kotlinx.android.synthetic.main.activity_main_tv.view.* -import kotlinx.android.synthetic.main.fragment_home.* -import kotlinx.android.synthetic.main.fragment_home.view.* -import kotlinx.android.synthetic.main.fragment_home_head_tv.* -import kotlinx.android.synthetic.main.fragment_home_head_tv.view.* -import kotlinx.android.synthetic.main.homepage_parent.view.* class LoadClickCallback( val action: Int = 0, @@ -37,12 +31,17 @@ open class ParentItemAdapter( private val expandCallback: ((String) -> Unit)? = null, ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + + val root = LayoutInflater.from(parent.context).inflate( + if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent, + parent, + false + ) + + val binding = HomepageParentBinding.bind(root) + return ParentViewHolder( - LayoutInflater.from(parent.context).inflate( - if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent, - parent, - false - ), + binding, clickCallback, moreInfoClickCallback, expandCallback @@ -153,14 +152,14 @@ open class ParentItemAdapter( class ParentViewHolder constructor( - itemView: View, + val binding: HomepageParentBinding, private val clickCallback: (SearchClickCallback) -> Unit, private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, private val expandCallback: ((String) -> Unit)? = null, ) : - RecyclerView.ViewHolder(itemView) { - val title: TextView = itemView.home_child_more_info - private val recyclerView: RecyclerView = itemView.home_child_recyclerview + RecyclerView.ViewHolder(binding.root) { + val title: TextView = binding.homeChildMoreInfo + private val recyclerView: RecyclerView = binding.homeChildRecyclerview fun update(expand: HomeViewModel.ExpandableHomepageList) { val info = expand.list 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 9bbfbb37..fffe590e 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 @@ -409,10 +409,7 @@ class HomeParentItemAdapterPreview( // setPageTransformer(null) if (adapter == null) - adapter = HomeScrollAdapter( - if (isTvSettings()) R.layout.home_scroll_view_tv else R.layout.home_scroll_view, - if (isTvSettings()) true else null - ) + adapter = HomeScrollAdapter() } previewAdapter = previewViewpager?.adapter as? HomeScrollAdapter? // previewViewpager?.registerOnPageChangeCallback(previewCallback) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt index 5902132e..c54996c2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt @@ -2,24 +2,18 @@ package com.lagradost.cloudstream3.ui.home import android.content.res.Configuration import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import androidx.annotation.LayoutRes import androidx.core.view.isGone import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.LoadResponse -import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding +import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.home_scroll_view.view.home_scroll_preview -import kotlinx.android.synthetic.main.home_scroll_view.view.home_scroll_preview_tags -import kotlinx.android.synthetic.main.home_scroll_view.view.home_scroll_preview_title - -class HomeScrollAdapter( - @LayoutRes val layout: Int = R.layout.home_scroll_view, - private val forceHorizontalPosters: Boolean? = null -) : RecyclerView.Adapter() { +class HomeScrollAdapter : RecyclerView.Adapter() { private var items: MutableList = mutableListOf() var hasMoreItems: Boolean = false @@ -45,9 +39,16 @@ class HomeScrollAdapter( } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = if(isTvSettings()) { + HomeScrollViewBinding.inflate(inflater,parent,false) + } else { + HomeScrollViewTvBinding.inflate(inflater,parent,false) + } + return CardViewHolder( - LayoutInflater.from(parent.context).inflate(layout, parent, false), - forceHorizontalPosters + binding, + //forceHorizontalPosters ) } @@ -61,22 +62,30 @@ class HomeScrollAdapter( class CardViewHolder constructor( - itemView: View, - private val forceHorizontalPosters: Boolean? = null + val binding: ViewBinding, + //private val forceHorizontalPosters: Boolean? = null ) : - RecyclerView.ViewHolder(itemView) { + RecyclerView.ViewHolder(binding.root) { fun bind(card: LoadResponse) { - card.apply { - val isHorizontal = - (forceHorizontalPosters == true) || ((forceHorizontalPosters != false) && itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) + val isHorizontal = + binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - val posterUrl = if (isHorizontal) backgroundPosterUrl ?: posterUrl else posterUrl - ?: backgroundPosterUrl - itemView.home_scroll_preview_tags?.text = tags?.joinToString(" • ") ?: "" - itemView.home_scroll_preview_tags?.isGone = tags.isNullOrEmpty() - itemView.home_scroll_preview?.setImage(posterUrl, posterHeaders) - itemView.home_scroll_preview_title?.text = name + val posterUrl = if (isHorizontal) card.backgroundPosterUrl ?: card.posterUrl else card.posterUrl + ?: card.backgroundPosterUrl + + when(binding) { + is HomeScrollViewBinding -> { + binding.homeScrollPreview.setImage(posterUrl) + binding.homeScrollPreviewTags.apply { + text = card.tags?.joinToString(" • ") ?: "" + isGone = card.tags.isNullOrEmpty() + } + binding.homeScrollPreviewTitle.text = card.name + } + is HomeScrollViewTvBinding -> { + binding.homeScrollPreview.setImage(posterUrl) + } } } } 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 index 05b05f44..d558d6a5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt @@ -84,7 +84,7 @@ class PageAdapter( binding.textRating.apply { setTextColor(ColorStateList.valueOf(fg)) } - binding.textRatingHolder.backgroundTintList = ColorStateList.valueOf(bg) + binding.textRating.backgroundTintList = ColorStateList.valueOf(bg) binding.watchProgress.apply { progressTintList = ColorStateList.valueOf(fg) progressBackgroundTintList = ColorStateList.valueOf(bg) @@ -111,16 +111,6 @@ class PageAdapter( } binding.imageText.text = item.name - - val showRating = (item.personalRating ?: 0) != 0 - binding.textRatingHolder.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", "") - - binding.textRating.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 index 441d6adc..95fefcbe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -64,7 +64,7 @@ class ViewpagerAdapter( } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY -> + setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> val diff = scrollY - oldScrollY if (diff == 0) return@setOnScrollChangeListener 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 69812f22..2b2269ff 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 @@ -16,22 +16,13 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.isMovieType +import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.home_result_grid.view.background_card -import kotlinx.android.synthetic.main.home_result_grid.view.imageText -import kotlinx.android.synthetic.main.home_result_grid.view.imageView -import kotlinx.android.synthetic.main.home_result_grid.view.search_item_download_play -import kotlinx.android.synthetic.main.home_result_grid.view.text_flag -import kotlinx.android.synthetic.main.home_result_grid.view.text_is_dub -import kotlinx.android.synthetic.main.home_result_grid.view.text_is_sub -import kotlinx.android.synthetic.main.home_result_grid.view.text_quality -import kotlinx.android.synthetic.main.home_result_grid.view.title_shadow -import kotlinx.android.synthetic.main.home_result_grid.view.watchProgress object SearchResultBuilder { private val showCache: MutableMap = mutableMapOf() @@ -59,19 +50,21 @@ object SearchResultBuilder { nextFocusDown: Int? = null, colorCallback : ((Palette) -> Unit)? = null ) { - val cardView: ImageView = itemView.imageView - val cardText: TextView? = itemView.imageText + val cardView: ImageView = itemView.findViewById(R.id.imageView) + val cardText: TextView? = itemView.findViewById(R.id.imageText) - val textIsDub: TextView? = itemView.text_is_dub - val textIsSub: TextView? = itemView.text_is_sub - val textFlag: TextView? = itemView.text_flag - val textQuality: TextView? = itemView.text_quality - val shadow: View? = itemView.title_shadow + val textIsDub: TextView? = itemView.findViewById(R.id.text_is_dub) + val textIsSub: TextView? = itemView.findViewById(R.id.text_is_sub) + val textFlag: TextView? = itemView.findViewById(R.id.text_flag) + val rating: TextView? = itemView.findViewById(R.id.text_rating) - val bg: CardView = itemView.background_card + val textQuality: TextView? = itemView.findViewById(R.id.text_quality) + val shadow: View? = itemView.findViewById(R.id.title_shadow) - val bar: ProgressBar? = itemView.watchProgress - val playImg: ImageView? = itemView.search_item_download_play + val bg: CardView = itemView.findViewById(R.id.background_card) + + val bar: ProgressBar? = itemView.findViewById(R.id.watchProgress) + val playImg: ImageView? = itemView.findViewById(R.id.search_item_download_play) // Do logic @@ -80,12 +73,25 @@ object SearchResultBuilder { textIsDub?.isVisible = false textIsSub?.isVisible = false textFlag?.isVisible = false + rating?.isVisible = false val showSub = showCache[textIsDub?.context?.getString(R.string.show_sub_key)] ?: false val showDub = showCache[textIsDub?.context?.getString(R.string.show_dub_key)] ?: false val showTitle = showCache[cardText?.context?.getString(R.string.show_title_key)] ?: false val showHd = showCache[textQuality?.context?.getString(R.string.show_hd_key)] ?: false + if(card is SyncAPI.LibraryItem) { + val showRating = (card.personalRating ?: 0) != 0 + rating?.isVisible = showRating + if (showRating) { + // We want to show 8.5 but not 8.0 hence the replace + val ratingText = ((card.personalRating ?: 0).toDouble() / 10).toString() + .replace(".0", "") + + rating?.text = ratingText + } + } + shadow?.isVisible = showTitle when (card.quality) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt index 1f6d726d..8285b8ab 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt @@ -2,19 +2,28 @@ package com.lagradost.cloudstream3.utils import android.app.Activity import android.app.Dialog +import android.view.LayoutInflater import android.view.View -import android.widget.* +import android.widget.AbsListView +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ListView +import android.widget.TextView import androidx.appcompat.app.AlertDialog -import androidx.core.view.* +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.core.view.marginLeft +import androidx.core.view.marginRight +import androidx.core.view.marginTop import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.add_account_input.* -import kotlinx.android.synthetic.main.add_account_input.text1 -import kotlinx.android.synthetic.main.bottom_selection_dialog_direct.* object SingleSelectionHelper { fun Activity?.showOptionSelectStringRes( @@ -82,6 +91,7 @@ object SingleSelectionHelper { } fun Activity?.showDialog( + binding: BottomSelectionDialogBinding, dialog: Dialog, items: List, selectedIndex: List, @@ -95,39 +105,39 @@ object SingleSelectionHelper { if (this == null) return val realShowApply = showApply || isMultiSelect - val listView = dialog.listview1//.findViewById(R.id.listview1)!! - val textView = dialog.text1//.findViewById(R.id.text1)!! - val applyButton = dialog.apply_btt//.findViewById(R.id.apply_btt) - val cancelButton = dialog.cancel_btt//findViewById(R.id.cancel_btt) + val listView = binding.listview1//.findViewById(R.id.listview1)!! + val textView = binding.text1//.findViewById(R.id.text1)!! + val applyButton = binding.applyBtt//.findViewById(R.id.apply_btt) + val cancelButton = binding.cancelBtt//findViewById(R.id.cancel_btt) val applyHolder = - dialog.apply_btt_holder//.findViewById(R.id.apply_btt_holder) + binding.applyBttHolder//.findViewById(R.id.apply_btt_holder) - applyHolder?.isVisible = realShowApply + applyHolder.isVisible = realShowApply if (!realShowApply) { val params = listView.layoutParams as LinearLayout.LayoutParams params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0) listView.layoutParams = params } - textView?.text = name - textView?.isGone = name.isBlank() + textView.text = name + textView.isGone = name.isBlank() val arrayAdapter = ArrayAdapter(this, itemLayout) arrayAdapter.addAll(items) - listView?.adapter = arrayAdapter + listView.adapter = arrayAdapter if (isMultiSelect) { - listView?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE + listView.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE } else { - listView?.choiceMode = AbsListView.CHOICE_MODE_SINGLE + listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE } for (select in selectedIndex) { - listView?.setItemChecked(select, true) + listView.setItemChecked(select, true) } selectedIndex.minOrNull()?.let { - listView?.setSelection(it) + listView.setSelection(it) } // var lastSelectedIndex = if(selectedIndex.isNotEmpty()) selectedIndex.first() else -1 @@ -136,7 +146,7 @@ object SingleSelectionHelper { dismissCallback.invoke() } - listView?.setOnItemClickListener { _, _, which, _ -> + listView.setOnItemClickListener { _, _, which, _ -> // lastSelectedIndex = which if (realShowApply) { if (!isMultiSelect) { @@ -148,7 +158,7 @@ object SingleSelectionHelper { } } if (realShowApply) { - applyButton?.setOnClickListener { + applyButton.setOnClickListener { val list = ArrayList() for (index in 0 until listView.count) { if (listView.checkedItemPositions[index]) @@ -157,7 +167,7 @@ object SingleSelectionHelper { callback.invoke(list) dialog.dismissSafe(this) } - cancelButton?.setOnClickListener { + cancelButton.setOnClickListener { dialog.dismissSafe(this) } } @@ -213,13 +223,26 @@ object SingleSelectionHelper { ) { if (this == null) return + val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate( + LayoutInflater.from(this) + ) val builder = AlertDialog.Builder(this, R.style.AlertDialogCustom) - .setView(R.layout.bottom_selection_dialog) + .setView(binding.root) val dialog = builder.create() dialog.show() - showDialog(dialog, items, selectedIndex, name, true, true, callback, dismissCallback) + showDialog( + binding, + dialog, + items, + selectedIndex, + name, + showApply = true, + isMultiSelect = true, + callback, + dismissCallback + ) } fun Activity?.showDialog( @@ -232,13 +255,19 @@ object SingleSelectionHelper { ) { if (this == null) return + val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate( + LayoutInflater.from(this) + ) val builder = AlertDialog.Builder(this, R.style.AlertDialogCustom) - .setView(R.layout.bottom_selection_dialog) + .setView(binding.root) val dialog = builder.create() dialog.show() + + showDialog( + binding, dialog, items, listOf(selectedIndex), @@ -260,12 +289,18 @@ object SingleSelectionHelper { callback: (Int) -> Unit, ) { if (this == null) return + + val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate( + LayoutInflater.from(this) + ) + val builder = BottomSheetDialog(this) - builder.setContentView(R.layout.bottom_selection_dialog) + builder.setContentView(binding.root) builder.show() showDialog( + binding, builder, items, listOf(selectedIndex), @@ -285,13 +320,19 @@ object SingleSelectionHelper { ): BottomSheetDialog { val builder = BottomSheetDialog(this) - builder.setContentView(R.layout.bottom_selection_dialog_direct) + val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate( + LayoutInflater.from(this) + ) + + //builder.setContentView(R.layout.bottom_selection_dialog_direct) + builder.setContentView(binding.root) builder.show() showDialog( + binding, builder, items, - listOf(), + emptyList(), name, showApply = false, isMultiSelect = false, diff --git a/app/src/main/res/layout/search_result_grid.xml b/app/src/main/res/layout/search_result_grid.xml index f3c35ca4..cec7d4ce 100644 --- a/app/src/main/res/layout/search_result_grid.xml +++ b/app/src/main/res/layout/search_result_grid.xml @@ -1,5 +1,5 @@ - + + - \ No newline at end of file + \ No newline at end of file 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 cf6ab3b2..c64486b4 100644 --- a/app/src/main/res/layout/search_result_grid_expanded.xml +++ b/app/src/main/res/layout/search_result_grid_expanded.xml @@ -58,29 +58,12 @@ style="@style/SubButton" android:layout_gravity="end" /> - - - - - + tools:text="7.7" /> + @color/subColorText + + + + + + + + @@ -547,6 +548,7 @@ 0dp 0dp @drawable/outline_drawable_less + @string/tv_no_focus_tag From c987f7581e590a8f69f525fd8246f5a15224577a Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 28 Jul 2023 04:18:28 +0200 Subject: [PATCH 127/570] major focus fixes --- .../lagradost/cloudstream3/CommonActivity.kt | 79 ++++++++++++------- .../cloudstream3/ui/AutofitRecyclerView.kt | 25 ++++-- .../ui/home/HomeChildItemAdapter.kt | 42 +++++++--- .../ui/home/HomeParentItemAdapter.kt | 4 +- .../ui/home/HomeParentItemAdapterPreview.kt | 4 + .../cloudstream3/ui/result/ActorAdaptor.kt | 12 ++- .../ui/result/LinearListLayout.kt | 24 +++--- .../ui/result/ResultFragmentTv.kt | 34 ++++---- .../ui/search/SearchResultBuilder.kt | 57 ++++++++----- .../drawable/ic_baseline_arrow_back_24.xml | 1 + .../ic_baseline_arrow_back_ios_24.xml | 14 +++- .../drawable/ic_baseline_arrow_forward_24.xml | 1 + .../ic_baseline_keyboard_arrow_left_24.xml | 14 +++- .../res/drawable/ic_baseline_language_24.xml | 13 ++- .../res/drawable/ic_baseline_more_vert_24.xml | 13 ++- .../ic_baseline_notifications_active_24.xml | 13 ++- .../main/res/drawable/netflix_skip_back.xml | 32 ++++---- .../res/drawable/outline_drawable_forced.xml | 5 ++ app/src/main/res/layout/activity_main_tv.xml | 1 - .../main/res/layout/fragment_home_head_tv.xml | 4 +- .../main/res/layout/fragment_result_tv.xml | 3 +- ...sort_bottom_single_choice_no_checkmark.xml | 12 +-- 22 files changed, 268 insertions(+), 139 deletions(-) create mode 100644 app/src/main/res/drawable/outline_drawable_forced.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 7eb1bf6d..684e2269 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.ui.player.PlayerEventType import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv +import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.UIHelper @@ -299,22 +300,29 @@ object CommonActivity { view: View?, direction: FocusDirection, depth: Int = 0 - ): Int? { + ): View? { + // if input is invalid let android decide + depth test to not crash if loop is found if (view == null || depth >= 10 || act == null) { return null } val nextId = when (direction) { - FocusDirection.Left -> { - view.nextFocusLeftId + FocusDirection.Start -> { + if (view.isRtl()) + view.nextFocusRightId + else + view.nextFocusLeftId } FocusDirection.Up -> { view.nextFocusUpId } - FocusDirection.Right -> { - view.nextFocusRightId + FocusDirection.End -> { + if (view.isRtl()) + view.nextFocusLeftId + else + view.nextFocusRightId } FocusDirection.Down -> { @@ -322,27 +330,41 @@ object CommonActivity { } } - return if (nextId != -1) { - val next = act.findViewById(nextId) - //println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" ) + // if view not found then return + if (nextId == -1) return null + var next = act.findViewById(nextId) ?: return null - if (next?.isShown == false) { - getNextFocus(act, next, direction, depth + 1) - } else { - if (depth == 0) { - null - } else { - nextId - } + // because we want closes find, aka when multiple have the same id, we go to parent + // until the correct one is found + /*var currentLook: View = view + while (true) { + val tmpNext = currentLook.findViewById(nextId) + if (tmpNext != null) { + next = tmpNext + break } - } else { - null + + currentLook = currentLook.parent as? View ?: break + }*/ + + var currentLook: View = view + while (currentLook.findViewById(nextId)?.also { next = it } == null) { + currentLook = (currentLook.parent as? View) ?: break } + + // if cant focus but visible then break and let android decide + if (!next.isFocusable && next.isShown) return null + + // if not shown then continue because we will "skip" over views to get to a replacement + if (!next.isShown) return getNextFocus(act, next, direction, depth + 1) + + // nothing wrong with the view found, return it + return next } enum class FocusDirection { - Left, - Right, + Start, + End, Up, Down, } @@ -447,17 +469,17 @@ object CommonActivity { event?.keyCode?.let { keyCode -> if (currentFocus == null || event.action != KeyEvent.ACTION_DOWN) return@let - val next = when (keyCode) { + val nextView = when (keyCode) { KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus( act, currentFocus, - FocusDirection.Left + FocusDirection.Start ) KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus( act, currentFocus, - FocusDirection.Right + FocusDirection.End ) KeyEvent.KEYCODE_DPAD_UP -> getNextFocus( @@ -475,13 +497,10 @@ object CommonActivity { else -> null } - if (next != null && next != -1) { - val nextView = act.findViewById(next) - if (nextView != null) { - nextView.requestFocus() - keyEventListener?.invoke(Pair(event, true)) - return true - } + if (nextView != null) { + nextView.requestFocus() + keyEventListener?.invoke(Pair(event, true)) + return true } if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)) { 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 b4c07792..28ced48c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt @@ -24,7 +24,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : } } - override fun onRequestChildFocus( + /*override fun onRequestChildFocus( parent: RecyclerView, state: RecyclerView.State, child: View, @@ -32,13 +32,17 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : ): Boolean { // android.widget.FrameLayout$LayoutParams cannot be cast to androidx.recyclerview.widget.RecyclerView$LayoutParams return try { - val pos = maxOf(0, getPosition(focused!!) - 2) - parent.scrollToPosition(pos) + if(focused != null) { + // val pos = maxOf(0, getPosition(focused) - 2) // IDK WHY + val pos = getPosition(focused) + if(pos >= 0) parent.scrollToPosition(pos) + } + super.onRequestChildFocus(parent, state, child, focused) } catch (e: Exception) { false } - } + }*/ // Allows moving right and left with focus https://gist.github.com/vganin/8930b41f55820ec49e4d override fun onInterceptFocusSearch(focused: View, direction: Int): View? { @@ -65,8 +69,17 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : val spanCount = this.spanCount val orientation = this.orientation + // fixes arabic by inverting left and right layout focus + val correctDirection = if(this.isLayoutRTL) { + when(direction) { + View.FOCUS_RIGHT -> View.FOCUS_LEFT + View.FOCUS_LEFT -> View.FOCUS_RIGHT + else -> direction + } + } else direction + if (orientation == VERTICAL) { - when (direction) { + when (correctDirection) { View.FOCUS_DOWN -> { return spanCount } @@ -81,7 +94,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : } } } else if (orientation == HORIZONTAL) { - when (direction) { + when (correctDirection) { View.FOCUS_DOWN -> { return 1 } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index 8b0f9003..607cda01 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -11,6 +11,7 @@ import com.lagradost.cloudstream3.databinding.HomeResultGridBinding import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder +import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx @@ -27,13 +28,17 @@ class HomeChildItemAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val expanded = parent.context.IsBottomLayout() - /* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid + /* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid - val root = LayoutInflater.from(parent.context).inflate(layout, parent, false) - val binding = HomeResultGridBinding.bind(root)*/ + val root = LayoutInflater.from(parent.context).inflate(layout, parent, false) + val binding = HomeResultGridBinding.bind(root)*/ val inflater = LayoutInflater.from(parent.context) - val binding = if(expanded) HomeResultGridExpandedBinding.inflate(inflater,parent,false) else HomeResultGridBinding.inflate(inflater,parent,false) + val binding = if (expanded) HomeResultGridExpandedBinding.inflate( + inflater, + parent, + false + ) else HomeResultGridBinding.inflate(inflater, parent, false) return CardViewHolder( @@ -42,7 +47,8 @@ class HomeChildItemAdapter( itemCount, nextFocusUp, nextFocusDown, - isHorizontal + isHorizontal, + parent.isRtl() ) } @@ -81,7 +87,8 @@ class HomeChildItemAdapter( var itemCount: Int, private val nextFocusUp: Int? = null, private val nextFocusDown: Int? = null, - private val isHorizontal: Boolean = false + private val isHorizontal: Boolean = false, + private val isRtl : Boolean ) : RecyclerView.ViewHolder(binding.root) { @@ -93,7 +100,23 @@ class HomeChildItemAdapter( itemCount - 1 -> false else -> null } - when(binding) { + + if (position == 0) { // to fix tv + if (isRtl) { + itemView.nextFocusRightId = R.id.nav_rail_view + itemView.nextFocusLeftId = -1 + } + else { + itemView.nextFocusLeftId = R.id.nav_rail_view + itemView.nextFocusRightId = -1 + } + } else { + itemView.nextFocusRightId = -1 + itemView.nextFocusLeftId = -1 + } + + + when (binding) { is HomeResultGridBinding -> { binding.backgroundCard.apply { val min = 114.toPx @@ -114,10 +137,9 @@ class HomeChildItemAdapter( } } - if (position == 0) { // to fix tv - binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view - } + } + is HomeResultGridExpandedBinding -> { binding.backgroundCard.apply { val min = 114.toPx 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 d4c0bd62..f6c3fead 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 @@ -177,7 +177,7 @@ open class ParentItemAdapter( ).apply { isHorizontal = info.isHorizontalImages } - //recyclerView.setLinearListLayout() + recyclerView.setLinearListLayout() } } @@ -192,7 +192,7 @@ open class ParentItemAdapter( isHorizontal = info.isHorizontalImages hasNext = expand.hasNext } - // recyclerView.setLinearListLayout() + recyclerView.setLinearListLayout() title.text = info.name recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { 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 a304b43f..ce7b8447 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 @@ -30,6 +30,7 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST +import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA import com.lagradost.cloudstream3.ui.search.SearchClickCallback @@ -427,6 +428,9 @@ class HomeParentItemAdapterPreview( resumeRecyclerView.adapter = resumeAdapter bookmarkRecyclerView.adapter = bookmarkAdapter + resumeRecyclerView.setLinearListLayout() + bookmarkRecyclerView.setLinearListLayout() + for ((chip, watch) in toggleList) { chip.isChecked = false chip.setOnCheckedChangeListener { _, isChecked -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 7b415d78..531cb5d2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil @@ -11,7 +12,7 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.CastItemBinding import com.lagradost.cloudstream3.utils.UIHelper.setImage -class ActorAdaptor : RecyclerView.Adapter() { +class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerView.Adapter() { data class ActorMetaData( var isInverted: Boolean, val actor: ActorData, @@ -21,7 +22,7 @@ class ActorAdaptor : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return CardViewHolder( - CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), focusCallback ) } @@ -66,6 +67,7 @@ class ActorAdaptor : RecyclerView.Adapter() { private class CardViewHolder constructor( val binding: CastItemBinding, + private val focusCallback : (View?) -> Unit = {} ) : RecyclerView.ViewHolder(binding.root) { @@ -76,6 +78,12 @@ class ActorAdaptor : RecyclerView.Adapter() { Pair(actor.voiceActor?.image, actor.actor.image) } + itemView.setOnFocusChangeListener { v, hasFocus -> + if(hasFocus) { + focusCallback(v) + } + } + itemView.setOnClickListener { callback(position) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt index 434355a2..26cb7900 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt @@ -2,19 +2,16 @@ package com.lagradost.cloudstream3.ui.result import android.content.Context import android.view.View -import android.view.View.LAYOUT_DIRECTION_LTR import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppUtils.isLtr -import com.lagradost.cloudstream3.utils.AppUtils.isRtl fun RecyclerView?.setLinearListLayout(isHorizontal: Boolean = true) { if (this == null) return this.layoutManager = this.context?.let { LinearListLayout(it).apply { if (isHorizontal) setHorizontal() else setVertical() } } - // ?: this.layoutManager + // ?: this.layoutManager } open class LinearListLayout(context: Context?) : @@ -60,7 +57,7 @@ open class LinearListLayout(context: Context?) : startSmoothScroll(linearSmoothScroller) }*/ override fun onInterceptFocusSearch(focused: View, direction: Int): View? { - var dir = if (orientation == HORIZONTAL) { + val dir = if (orientation == HORIZONTAL) { if (direction == View.FOCUS_DOWN || direction == View.FOCUS_UP) { // This scrolls the recyclerview before doing focus search, which // allows the focus search to work better. @@ -70,19 +67,28 @@ open class LinearListLayout(context: Context?) : (focused.parent as? RecyclerView)?.focusSearch(direction) return null } - if (direction == View.FOCUS_RIGHT) 1 else -1 + var ret = if (direction == View.FOCUS_RIGHT) 1 else -1 + // only flip on horizontal layout + if (this.isLayoutRTL) { + ret = -ret + } + ret } else { if (direction == View.FOCUS_RIGHT || direction == View.FOCUS_LEFT) return null if (direction == View.FOCUS_DOWN) 1 else -1 } - if(this.isLayoutRTL) { - dir = -dir - } return try { getPosition(getCorrectParent(focused))?.let { position -> val lookfor = dir + position //clamp(dir + position, 0, recyclerView.adapter?.itemCount ?: return null) + + // refocus on the same view if going out of bounds, note that we only do it + // for out of bounds one way as we may override the start where item == -1 + if (lookfor >= itemCount) { + return getViewFromPos(itemCount - 1) ?: focused + } + getViewFromPos(lookfor) ?: run { scrollToPosition(lookfor) null 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 69127e86..f62d7e73 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 @@ -1,15 +1,12 @@ package com.lagradost.cloudstream3.ui.result import android.animation.Animator -import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.app.Dialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.animation.AlphaAnimation -import android.view.animation.Animation import android.view.animation.DecelerateInterpolator import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone @@ -31,20 +28,17 @@ import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup -import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent +import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.html -import com.lagradost.cloudstream3.utils.AppUtils.isLtr import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.AppUtils.loadCache -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant import com.lagradost.cloudstream3.utils.UIHelper @@ -52,10 +46,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.setImage -import com.lagradost.cloudstream3.utils.UIHelper.setImageBlur -import kotlinx.coroutines.delay class ResultFragmentTv : Fragment() { protected lateinit var viewModel: ResultViewModel2 @@ -204,7 +195,7 @@ class ResultFragmentTv : Fragment() { } }) } - this.animate().translationX(if (turnVisible) 0f else 100f).apply { + this.animate().translationX(if (turnVisible) 0f else if(isRtl()) -100.0f else 100f).apply { duration = 200 interpolator = DecelerateInterpolator() } @@ -214,6 +205,13 @@ class ResultFragmentTv : Fragment() { binding?.apply { episodesShadow.fade(show) episodeHolderTv.fade(show) + if(episodesShadow.isRtl()) { + episodesShadow.scaleX = -1.0f + episodesShadow.scaleY = -1.0f + } else { + episodesShadow.scaleX = 1.0f + episodesShadow.scaleY = 1.0f + } } } @@ -238,6 +236,7 @@ class ResultFragmentTv : Fragment() { // ===== ===== ===== binding?.apply { + //episodesShadow.rotationX = 180.0f//if(episodesShadow.isRtl()) 180.0f else 0.0f val leftListener: View.OnFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> @@ -264,6 +263,8 @@ class ResultFragmentTv : Fragment() { toggleEpisodes(!episodeHolderTv.isVisible) } + // resultEpisodes.onFocusChangeListener = leftListener + redirectToPlay.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) return@setOnFocusChangeListener toggleEpisodes(false) @@ -306,7 +307,7 @@ class ResultFragmentTv : Fragment() { } } - resultEpisodes.setLinearListLayout(false)/*.layoutManager = + resultEpisodes.setLinearListLayout(isHorizontal = false)/*.layoutManager = LinearListLayout(resultEpisodes.context, resultEpisodes.isRtl()).apply { setVertical() }*/ @@ -348,7 +349,10 @@ class ResultFragmentTv : Fragment() { ArrayList(), resultRecommendationsList, ) { callback -> - SearchHelper.handleSearchClickCallback(callback) + if(callback.action == SEARCH_ACTION_FOCUSED) + toggleEpisodes(false) + else + SearchHelper.handleSearchClickCallback(callback) } resultEpisodes.adapter = @@ -381,7 +385,9 @@ class ResultFragmentTv : Fragment() { }.apply { this.orientation = RecyclerView.HORIZONTAL } - resultCastItems.adapter = ActorAdaptor() + resultCastItems.adapter = ActorAdaptor { + toggleEpisodes(false) + } } observeNullable(viewModel.resumeWatching) { resume -> 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 2b2269ff..e1b72b30 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 @@ -162,15 +162,42 @@ object SearchResultBuilder { } } - bg.setOnClickListener { - click(it) + bg.isFocusable = false + bg.isFocusableInTouchMode = false + if(!isTrueTvSettings()) { + bg.setOnClickListener { + click(it) + } + bg.setOnLongClickListener { + longClick(it) + return@setOnLongClickListener true + } } + // + // + // itemView.setOnClickListener { click(it) } - if (nextFocusUp != null) { + itemView.nextFocusUpId = nextFocusUp + } + + if (nextFocusDown != null) { + itemView.nextFocusDownId = nextFocusDown + } + + /*when (nextFocusBehavior) { + true -> itemView.nextFocusLeftId = bg.id + false -> itemView.nextFocusRightId = bg.id + null -> { + bg.nextFocusRightId = -1 + bg.nextFocusLeftId = -1 + } + }*/ + + /*if (nextFocusUp != null) { bg.nextFocusUpId = nextFocusUp } @@ -178,36 +205,26 @@ object SearchResultBuilder { bg.nextFocusDownId = nextFocusDown } - when (nextFocusBehavior) { - true -> bg.nextFocusLeftId = bg.id - false -> bg.nextFocusRightId = bg.id - null -> { - bg.nextFocusRightId = -1 - bg.nextFocusLeftId = -1 - } - } + */ if (isTrueTvSettings()) { - bg.isFocusable = true - bg.isFocusableInTouchMode = true - bg.touchscreenBlocksFocus = false + // bg.isFocusable = true + // bg.isFocusableInTouchMode = true + // bg.touchscreenBlocksFocus = false itemView.isFocusableInTouchMode = true itemView.isFocusable = true } - bg.setOnLongClickListener { - longClick(it) - return@setOnLongClickListener true - } + /**/ itemView.setOnLongClickListener { longClick(it) return@setOnLongClickListener true } - bg.setOnFocusChangeListener { view, b -> + /*bg.setOnFocusChangeListener { view, b -> focus(view, b) - } + }*/ itemView.setOnFocusChangeListener { view, b -> focus(view, b) diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml index ebe459b2..dbda1cc0 100644 --- a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml +++ b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml @@ -3,6 +3,7 @@ android:viewportWidth="48" android:viewportHeight="48" android:tint="?attr/white" + android:autoMirrored="true" xmlns:android="http://schemas.android.com/apk/res/android"> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml index 6c3197a6..516df382 100644 --- a/app/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml +++ b/app/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml @@ -1,5 +1,11 @@ - - + + diff --git a/app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml index 2ec8c110..48ac45e7 100644 --- a/app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml +++ b/app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml @@ -3,6 +3,7 @@ android:viewportWidth="48" android:viewportHeight="48" android:tint="?attr/white" + android:autoMirrored="true" xmlns:android="http://schemas.android.com/apk/res/android"> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_keyboard_arrow_left_24.xml b/app/src/main/res/drawable/ic_baseline_keyboard_arrow_left_24.xml index 916c761c..b67188db 100644 --- a/app/src/main/res/drawable/ic_baseline_keyboard_arrow_left_24.xml +++ b/app/src/main/res/drawable/ic_baseline_keyboard_arrow_left_24.xml @@ -1,5 +1,11 @@ - - + + diff --git a/app/src/main/res/drawable/ic_baseline_language_24.xml b/app/src/main/res/drawable/ic_baseline_language_24.xml index 1749952e..89b47937 100644 --- a/app/src/main/res/drawable/ic_baseline_language_24.xml +++ b/app/src/main/res/drawable/ic_baseline_language_24.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_baseline_more_vert_24.xml b/app/src/main/res/drawable/ic_baseline_more_vert_24.xml index 249fe2a2..b6908e96 100644 --- a/app/src/main/res/drawable/ic_baseline_more_vert_24.xml +++ b/app/src/main/res/drawable/ic_baseline_more_vert_24.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml b/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml index 2003bfe7..5d6045e7 100644 --- a/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml +++ b/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/netflix_skip_back.xml b/app/src/main/res/drawable/netflix_skip_back.xml index bb63e948..5ad9c1a1 100644 --- a/app/src/main/res/drawable/netflix_skip_back.xml +++ b/app/src/main/res/drawable/netflix_skip_back.xml @@ -1,23 +1,23 @@ + android:width="850.39dp" + android:height="850.39dp" + android:viewportWidth="850.39" + android:viewportHeight="850.39"> + android:fillColor="#00000000" + android:pathData="M143.05,279.28A317.41,317.41 0,0 0,106.3 428c0,176.13 142.77,318.9 318.9,318.9S744.09,604.16 744.09,428 601.32,109.14 425.2,109.14q-14.15,0 -28,1.2" + android:strokeWidth="45" + android:strokeColor="#fff" /> + android:fillColor="#fff" + android:pathData="M483.083,223.108l-111.666,-111.666l25.442,-25.442l111.666,111.666z" /> + android:fillColor="#fff" + android:pathData="M371.421,111.662l111.666,-111.666l25.442,25.442l-111.666,111.666z" /> + android:fillColor="#fff" + android:pathData="M398.087,223.272l-111.666,-111.666l25.442,-25.442l111.666,111.666z" /> + android:fillColor="#fff" + android:pathData="M286.427,111.826l111.666,-111.666l25.442,25.442l-111.666,111.666z" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_drawable_forced.xml b/app/src/main/res/drawable/outline_drawable_forced.xml new file mode 100644 index 00000000..16eba83c --- /dev/null +++ b/app/src/main/res/drawable/outline_drawable_forced.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main_tv.xml b/app/src/main/res/layout/activity_main_tv.xml index 77baf1d3..a70a40cd 100644 --- a/app/src/main/res/layout/activity_main_tv.xml +++ b/app/src/main/res/layout/activity_main_tv.xml @@ -83,7 +83,6 @@ android:layout_height="match_parent"> @@ -111,7 +111,7 @@ android:nextFocusLeft="@id/home_preview_play_btt" android:nextFocusRight="@id/home_preview_hidden_next_focus" android:nextFocusUp="@id/home_preview_change_api" - android:nextFocusDown="@id/home_watch_parent_item_title" + android:nextFocusDown="@id/home_watch_child_recyclerview" android:text="@string/home_info" app:icon="@drawable/ic_outline_info_24" /> diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 949ef2ef..324935e5 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -537,6 +537,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit + xmlns:tools="http://schemas.android.com/tools" + android:id="@android:id/text1" + style="@style/NoCheckLabel" + android:textColor="?attr/textColor" + android:textStyle="normal" + tools:text="hello" /> From 3bdbb35754a45472aad6383df51511f45c4c0dd5 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 30 Jul 2023 05:05:13 +0200 Subject: [PATCH 128/570] alert fix + synchronized + bump + homepage load fix + small focus change --- app/build.gradle.kts | 2 +- .../cloudstream3/ExampleInstrumentedTest.kt | 6 +- .../lagradost/cloudstream3/CommonActivity.kt | 58 +++++++++----- .../com/lagradost/cloudstream3/MainAPI.kt | 36 ++++++--- .../lagradost/cloudstream3/MainActivity.kt | 76 ++++++++++--------- .../metaproviders/CrossTmdbProvider.kt | 13 ++-- .../metaproviders/MultiAnimeProvider.kt | 17 +++-- .../lagradost/cloudstream3/plugins/Plugin.kt | 16 ++-- .../cloudstream3/plugins/PluginManager.kt | 13 +++- .../cloudstream3/ui/home/HomeFragment.kt | 3 +- .../ui/home/HomeParentItemAdapterPreview.kt | 4 +- .../cloudstream3/ui/home/HomeViewModel.kt | 76 +++++++++++++------ .../ui/library/LibraryFragment.kt | 14 ++-- .../ui/result/ResultViewModel2.kt | 22 +++--- .../cloudstream3/ui/search/SearchViewModel.kt | 4 +- .../ui/settings/SettingsFragment.kt | 3 + .../ui/settings/SettingsGeneral.kt | 5 +- .../ui/settings/SettingsProviders.kt | 6 +- .../ui/settings/testing/TestViewModel.kt | 6 +- .../ui/setup/SetupFragmentExtensions.kt | 2 +- .../ui/setup/SetupFragmentProviderLanguage.kt | 4 +- .../lagradost/cloudstream3/utils/DataStore.kt | 2 + .../lagradost/cloudstream3/utils/SyncUtil.kt | 6 +- .../cloudstream3/utils/TestingUtils.kt | 2 +- .../main/res/layout/fragment_home_head_tv.xml | 22 +++--- app/src/main/res/values/styles.xml | 3 +- 26 files changed, 265 insertions(+), 156 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f4886258..27bd1e48 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,7 +52,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.0.1" + versionName = "4.1.1" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index 509ea4b9..df41ef91 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -46,9 +46,9 @@ class TestApplication : Activity() { @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { - private fun getAllProviders(): List { + private fun getAllProviders(): Array { println("Providers: ${APIHolder.allProviders.size}") - return APIHolder.allProviders //.filter { !it.usesWebView } + return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView } } @Test @@ -147,7 +147,7 @@ class ExampleInstrumentedTest { @Test fun providerCorrectHomepage() { runBlocking { - getAllProviders().amap { api -> + getAllProviders().toList().amap { api -> TestingUtils.testHomepage(api, ::println) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 684e2269..9c7c319e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -9,6 +9,7 @@ import android.content.res.Resources import android.os.Build import android.util.Log import android.view.* +import android.view.View.NO_ID import android.widget.TextView import android.widget.Toast import androidx.activity.ComponentActivity @@ -295,6 +296,30 @@ object CommonActivity { ) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW } + /** because we want closes find, aka when multiple have the same id, we go to parent + until the correct one is found */ + private fun localLook(from: View, id: Int): View? { + if (id == NO_ID) return null + var currentLook: View = from + while (true) { + currentLook.findViewById(id)?.let { return it } + currentLook = (currentLook.parent as? View) ?: break + } + return null + } + /*var currentLook: View = view + while (true) { + val tmpNext = currentLook.findViewById(nextId) + if (tmpNext != null) { + next = tmpNext + break + } + currentLook = currentLook.parent as? View ?: break + }*/ + + /** recursively looks for a next focus up to a depth of 10, + * this is used to override the normal shit focus system + * because this application has a lot of invisible views that messes with some tv devices*/ private fun getNextFocus( act: Activity?, view: View?, @@ -306,7 +331,7 @@ object CommonActivity { return null } - val nextId = when (direction) { + var nextId = when (direction) { FocusDirection.Start -> { if (view.isRtl()) view.nextFocusRightId @@ -330,22 +355,16 @@ object CommonActivity { } } - // if view not found then return - if (nextId == -1) return null + if (nextId == NO_ID) { + // if not specified then use forward id + nextId = view.nextFocusForwardId + // if view is still not found to next focus then return and let android decide + if (nextId == NO_ID) return null + } + var next = act.findViewById(nextId) ?: return null - // because we want closes find, aka when multiple have the same id, we go to parent - // until the correct one is found - /*var currentLook: View = view - while (true) { - val tmpNext = currentLook.findViewById(nextId) - if (tmpNext != null) { - next = tmpNext - break - } - - currentLook = currentLook.parent as? View ?: break - }*/ + next = localLook(view, nextId) ?: next var currentLook: View = view while (currentLook.findViewById(nextId)?.also { next = it } == null) { @@ -362,7 +381,7 @@ object CommonActivity { return next } - enum class FocusDirection { + private enum class FocusDirection { Start, End, Up, @@ -463,6 +482,7 @@ object CommonActivity { //} } + /** overrides focus and custom key events */ fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? { if (act == null) return null val currentFocus = act.currentFocus @@ -503,7 +523,9 @@ object CommonActivity { return true } - if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)) { + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && + (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) + ) { UIHelper.showInputMethod(act.currentFocus?.findFocus()) } @@ -516,6 +538,8 @@ object CommonActivity { } + // if someone else want to override the focus then don't handle the event as it is already + // consumed. used in video player if (keyEventListener?.invoke(Pair(event, false)) == true) { return true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 86252b40..51d218bf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -50,8 +50,10 @@ object APIHolder { val allProviders = threadSafeListOf() fun initAll() { - for (api in allProviders) { - api.init() + synchronized(allProviders) { + for (api in allProviders) { + api.init() + } } apiMap = null } @@ -64,27 +66,35 @@ object APIHolder { var apiMap: Map? = null fun addPluginMapping(plugin: MainAPI) { - apis = apis + plugin + synchronized(apis) { + apis = apis + plugin + } initMap(true) } fun removePluginMapping(plugin: MainAPI) { - apis = apis.filter { it != plugin } + synchronized(apis) { + apis = apis.filter { it != plugin } + } initMap(true) } private fun initMap(forcedUpdate: Boolean = false) { - if (apiMap == null || forcedUpdate) - apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap() + synchronized(apis) { + if (apiMap == null || forcedUpdate) + apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap() + } } fun getApiFromNameNull(apiName: String?): MainAPI? { if (apiName == null) return null synchronized(allProviders) { initMap() - return apiMap?.get(apiName)?.let { apis.getOrNull(it) } - // Leave the ?. null check, it can crash regardless - ?: allProviders.firstOrNull { it.name == apiName } + synchronized(apis) { + return apiMap?.get(apiName)?.let { apis.getOrNull(it) } + // Leave the ?. null check, it can crash regardless + ?: allProviders.firstOrNull { it.name == apiName } + } } } @@ -215,7 +225,7 @@ object APIHolder { val hashSet = HashSet() val activeLangs = getApiProviderLangSettings() val hasUniversal = activeLangs.contains(AllLanguagesName) - hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) } + hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } .map { it.name }) /*val set = settingsManager.getStringSet( @@ -314,8 +324,9 @@ object APIHolder { } ?: default val langs = this.getApiProviderLangSettings() val hasUniversal = langs.contains(AllLanguagesName) - val allApis = apis.filter { hasUniversal || langs.contains(it.lang) } - .filter { api -> api.hasMainPage || !hasHomePageIsRequired } + val allApis = synchronized(apis) { + apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } + } return if (currentPrefMedia.isEmpty()) { allApis } else { @@ -736,6 +747,7 @@ fun fixTitle(str: String): String { .replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it } } } + /** * Get rhino context in a safe way as it needs to be initialized on the main thread. * Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects() diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 3dd5a495..1083ad49 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -382,10 +382,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { this.navigate(R.id.navigation_downloads) return true } else { - for (api in apis) { - if (str.startsWith(api.mainUrl)) { - loadResult(str, api.name) - return true + synchronized(apis) { + for (api in apis) { + if (str.startsWith(api.mainUrl)) { + loadResult(str, api.name) + return true + } } } } @@ -464,9 +466,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { binding?.navHostFragment?.apply { val params = layoutParams as ConstraintLayout.LayoutParams - val push = if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0 + val push = + if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0 - if(!this.isLtr()) { + if (!this.isLtr()) { params.setMargins( params.leftMargin, params.topMargin, @@ -695,27 +698,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { private fun onAllPluginsLoaded(success: Boolean = false) { ioSafe { pluginsLock.withLock { - // Load cloned sites after plugins have been loaded since clones depend on plugins. - try { - getKey>(USER_PROVIDER_API)?.let { list -> - list.forEach { custom -> - allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } - ?.let { - allProviders.add(it.javaClass.newInstance().apply { - name = custom.name - lang = custom.lang - mainUrl = custom.url.trimEnd('/') - canBeOverridden = false - }) - } + synchronized(allProviders) { + // Load cloned sites after plugins have been loaded since clones depend on plugins. + try { + getKey>(USER_PROVIDER_API)?.let { list -> + list.forEach { custom -> + allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } + ?.let { + allProviders.add(it.javaClass.newInstance().apply { + name = custom.name + lang = custom.lang + mainUrl = custom.url.trimEnd('/') + canBeOverridden = false + }) + } + } } + // it.hashCode() is not enough to make sure they are distinct + apis = + allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name } + APIHolder.apiMap = null + } catch (e: Exception) { + logError(e) } - // it.hashCode() is not enough to make sure they are distinct - apis = - allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name } - APIHolder.apiMap = null - } catch (e: Exception) { - logError(e) } } } @@ -814,6 +819,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { translationX = target.x translationY = target.y + bringToFront() } } @@ -839,10 +845,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val out = IntArray(2) newFocus.getLocationInWindow(out) val (screenX, screenY) = out - var (x,y) = screenX.toFloat() to screenY.toFloat() + var (x, y) = screenX.toFloat() to screenY.toFloat() val (currentX, currentY) = focusOutline.translationX to focusOutline.translationY - // println(">><<< $x $y $currentX $currentY") - if(!newFocus.isLtr()) { + // println(">><<< $x $y $currentX $currentY") + if (!newFocus.isLtr()) { x = x - focusOutline.rootView.width + newFocus.measuredWidth } @@ -1195,7 +1201,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { ioSafe { initAll() // No duplicates (which can happen by registerMainAPI) - apis = allProviders.distinctBy { it } + apis = synchronized(allProviders) { + allProviders.distinctBy { it } + } } // val navView: BottomNavigationView = findViewById(R.id.nav_view) @@ -1347,8 +1355,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { }*/ if (BuildConfig.DEBUG) { - try { - var providersAndroidManifestString = "Current androidmanifest should be:\n" + var providersAndroidManifestString = "Current androidmanifest should be:\n" + synchronized(allProviders) { for (api in allProviders) { providersAndroidManifestString += "\n" } - println(providersAndroidManifestString) - - } catch (t: Throwable) { - logError(t) } - + println(providersAndroidManifestString) } handleAppIntent(intent) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt index 07aa904e..5bbb4538 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt @@ -21,10 +21,11 @@ class CrossTmdbProvider : TmdbProvider() { return Regex("""[^a-zA-Z0-9-]""").replace(name, "") } - private val validApis by lazy { - apis.filter { it.lang == this.lang && it::class.java != this::class.java } - //.distinctBy { it.uniqueId } - } + private val validApis + get() = + synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } } + //.distinctBy { it.uniqueId } + data class CrossMetaData( @JsonProperty("isSuccess") val isSuccess: Boolean, @@ -60,7 +61,8 @@ class CrossTmdbProvider : TmdbProvider() { override suspend fun load(url: String): LoadResponse? { val base = super.load(url)?.apply { - this.recommendations = this.recommendations?.filterIsInstance() // TODO REMOVE + this.recommendations = + this.recommendations?.filterIsInstance() // TODO REMOVE val matchName = filterName(this.name) when (this) { is MovieLoadResponse -> { @@ -98,6 +100,7 @@ class CrossTmdbProvider : TmdbProvider() { this.dataUrl = CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson() } + else -> { throw ErrorLoadingException("Nothing besides movies are implemented for this provider") } diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt index e8ac1876..8cfe1e9a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt @@ -25,13 +25,16 @@ class MultiAnimeProvider : MainAPI() { } } - private val validApis by lazy { - APIHolder.apis.filter { - it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains( - TvType.Anime - ) - } - } + private val validApis + get() = + synchronized(APIHolder.apis) { + APIHolder.apis.filter { + it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains( + TvType.Anime + ) + } + } + private fun filterName(name: String): String { return Regex("""[^a-zA-Z0-9-]""").replace(name, "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index 242baf59..6b7dc90b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -36,7 +36,9 @@ abstract class Plugin { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI") element.sourcePlugin = this.__filename // Race condition causing which would case duplicates if not for distinctBy - APIHolder.allProviders.add(element) + synchronized(APIHolder.allProviders) { + APIHolder.allProviders.add(element) + } APIHolder.addPluginMapping(element) } @@ -51,10 +53,14 @@ abstract class Plugin { } class Manifest { - @JsonProperty("name") var name: String? = null - @JsonProperty("pluginClassName") var pluginClassName: String? = null - @JsonProperty("version") var version: Int? = null - @JsonProperty("requiresResources") var requiresResources: Boolean = false + @JsonProperty("name") + var name: String? = null + @JsonProperty("pluginClassName") + var pluginClassName: String? = null + @JsonProperty("version") + var version: Int? = null + @JsonProperty("requiresResources") + var requiresResources: Boolean = false } /** 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 0dee57eb..49b5a752 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -163,7 +163,8 @@ object PluginManager { private val classLoaders: MutableMap = HashMap() - private var loadedLocalPlugins = false + var loadedLocalPlugins = false + private set private val gson = Gson() private suspend fun maybeLoadPlugin(context: Context, file: File) { @@ -531,10 +532,14 @@ object PluginManager { } // remove all registered apis - APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach { - removePluginMapping(it) + synchronized(APIHolder.apis) { + APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach { + removePluginMapping(it) + } + } + synchronized(APIHolder.allProviders) { + APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename } } - APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename } extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename } classLoaders.values.removeIf { v -> v == plugin } 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 eb12c411..a6e1b5e6 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 @@ -462,7 +462,7 @@ class HomeFragment : Fragment() { private val apiChangeClickListener = View.OnClickListener { view -> view.context.selectHomepage(currentApiName) { api -> - homeViewModel.loadAndCancel(api) + homeViewModel.loadAndCancel(api, forceReload = true,fromUI = true) } /*val validAPIs = view.context?.filterProviderByPreferredMedia()?.toMutableList() ?: mutableListOf() @@ -652,6 +652,7 @@ class HomeFragment : Fragment() { } homeViewModel.reloadStored() + homeViewModel.loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false) //loadHomePage(false) // nice profile pic on homepage 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 ce7b8447..fd2412da 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 @@ -447,12 +447,12 @@ class HomeParentItemAdapterPreview( (binding as? FragmentHomeHeadTvBinding)?.apply { homePreviewChangeApi.setOnClickListener { view -> view.context.selectHomepage(viewModel.repo?.name) { api -> - viewModel.loadAndCancel(api) + viewModel.loadAndCancel(api, forceReload = true, fromUI = true) } } homePreviewChangeApi2.setOnClickListener { view -> view.context.selectHomepage(viewModel.repo?.name) { api -> - viewModel.loadAndCancel(api) + viewModel.loadAndCancel(api, forceReload = true, fromUI = true) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 563a326e..a2dc9821 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.LiveData 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.filterHomePageListByFilmQuality import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia @@ -15,12 +14,22 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.mvvm.* +import com.lagradost.cloudstream3.HomePageList +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MainActivity +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.mvvm.Resource +import com.lagradost.cloudstream3.mvvm.debugAssert +import com.lagradost.cloudstream3.mvvm.debugWarning +import com.lagradost.cloudstream3.mvvm.launchSafe +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi import com.lagradost.cloudstream3.ui.WatchType -import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchClickCallback @@ -30,8 +39,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE -import com.lagradost.cloudstream3.utils.DataStore.getKey -import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds @@ -44,7 +51,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.withContext -import java.util.* +import java.util.EnumSet import kotlin.collections.set class HomeViewModel : ViewModel() { @@ -95,7 +102,7 @@ class HomeViewModel : ViewModel() { private var currentShuffledList: List = listOf() private fun autoloadRepo(): APIRepository { - return APIRepository(apis.first { it.hasMainPage }) + return APIRepository(synchronized(apis) { apis.first { it.hasMainPage }}) } private val _availableWatchStatusTypes = @@ -177,8 +184,10 @@ class HomeViewModel : ViewModel() { } private var onGoingLoad: Job? = null - private fun loadAndCancel(api: MainAPI?) { + private var isCurrentlyLoadingName : String? = null + private fun loadAndCancel(api: MainAPI) { onGoingLoad?.cancel() + isCurrentlyLoadingName = api.name onGoingLoad = load(api) } @@ -280,12 +289,12 @@ class HomeViewModel : ViewModel() { } } - private fun load(api: MainAPI?) = ioSafe { - repo = if (api != null) { + private fun load(api: MainAPI) : Job = ioSafe { + repo = //if (api != null) { APIRepository(api) - } else { - autoloadRepo() - } + //} else { + // autoloadRepo() + //} _apiName.postValue(repo?.name) _randomItems.postValue(listOf()) @@ -299,6 +308,7 @@ class HomeViewModel : ViewModel() { _page.postValue(Resource.Loading()) _preview.postValue(Resource.Loading()) + // cancel the current preview expand as that is no longer relevant addJob?.cancel() when (val data = repo?.getMainPage(1, null)) { @@ -370,7 +380,7 @@ class HomeViewModel : ViewModel() { else -> Unit } - onGoingLoad = null + isCurrentlyLoadingName = null } fun click(callback: SearchClickCallback) { @@ -437,33 +447,51 @@ class HomeViewModel : ViewModel() { loadResult(load.response.url, load.response.apiName, load.action) } - fun loadAndCancel(preferredApiName: String?, forceReload: Boolean = true) = - viewModelScope.launchSafe { + // only save the key if it is from UI, as we don't want internal functions changing the setting + fun loadAndCancel( + preferredApiName: String?, + forceReload: Boolean = true, + fromUI: Boolean = false + ) = + ioSafe { // Since plugins are loaded in stages this function can get called multiple times. // The issue with this is that the homepage may be fetched multiple times while the first request is loading val api = getApiFromNameNull(preferredApiName) - if (!forceReload && api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true) { - return@launchSafe + // api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true + val currentPage = page.value + + // if we don't need to reload and we have a valid homepage or currently loading the same thing then return + val currentLoading = isCurrentlyLoadingName + if (!forceReload && (currentPage is Resource.Success && currentPage.value.isNotEmpty() || (currentLoading != null && currentLoading == preferredApiName))) { + return@ioSafe } if (preferredApiName == noneApi.name) { - setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) + // just set to random + if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) loadAndCancel(noneApi) } else if (preferredApiName == randomApi.name) { + // randomize the api, if none exist like if not loaded or not installed + // then use nothing val validAPIs = context?.filterProviderByPreferredMedia() if (validAPIs.isNullOrEmpty()) { - // Do not set USER_SELECTED_HOMEPAGE_API when there is no plugins loaded loadAndCancel(noneApi) } else { val apiRandom = validAPIs.random() loadAndCancel(apiRandom) - setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) + if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) } - // If the plugin isn't loaded yet. (Does not set the key) } else if (api == null) { - loadAndCancel(noneApi) + // API is not found aka not loaded or removed, post the loading + // progress if waiting for plugins, otherwise nothing + if(PluginManager.loadedLocalPlugins) { + loadAndCancel(noneApi) + } else { + _page.postValue(Resource.Loading()) + } } else { - setKey(USER_SELECTED_HOMEPAGE_API, api.name) + // if the api is found, then set it to it and save key + if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, api.name) loadAndCancel(api) } } 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 index a20cd5c6..04ef3d96 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -163,12 +163,14 @@ class LibraryFragment : Fragment() { 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 availableProviders = synchronized(allProviders) { + 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, 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 88f55444..011d133d 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 @@ -321,7 +321,7 @@ data class ExtractedTrailerData( class ResultViewModel2 : ViewModel() { private var currentResponse: LoadResponse? = null - var EPISODE_RANGE_SIZE : Int = 20 + var EPISODE_RANGE_SIZE: Int = 20 fun clear() { currentResponse = null _page.postValue(null) @@ -456,7 +456,7 @@ class ResultViewModel2 : ViewModel() { currentResponse.year ) ) - if(currentWatchType != status) { + if (currentWatchType != status) { MainActivity.bookmarksUpdatedEvent(true) } } @@ -477,7 +477,10 @@ class ResultViewModel2 : ViewModel() { ) ) - private fun getRanges(allEpisodes: Map>, EPISODE_RANGE_SIZE : Int): Map> { + private fun getRanges( + allEpisodes: Map>, + EPISODE_RANGE_SIZE: Int + ): Map> { return allEpisodes.keys.mapNotNull { index -> val episodes = allEpisodes[index] ?: return@mapNotNull null // this should never happened @@ -1505,13 +1508,14 @@ class ResultViewModel2 : ViewModel() { } val realRecommendations = ArrayList() - val apiNames = apis.filter { - it.name.contains("gogoanime", true) || - it.name.contains("9anime", true) - }.map { - it.name + val apiNames = synchronized(apis) { + 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)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index aceda644..320687f8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -37,7 +37,7 @@ class SearchViewModel : ViewModel() { private val _currentHistory: MutableLiveData> = MutableLiveData() val currentHistory: LiveData> get() = _currentHistory - private var repos = apis.map { APIRepository(it) } + private var repos = synchronized(apis) { apis.map { APIRepository(it) } } fun clearSearch() { _searchResponse.postValue(Resource.Success(ArrayList())) @@ -48,7 +48,7 @@ class SearchViewModel : ViewModel() { private var onGoingSearch: Job? = null fun reloadRepos() { - repos = apis.map { APIRepository(it) } + repos = synchronized(apis) { apis.map { APIRepository(it) } } } fun searchAndCancel( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 070389b0..e53fa91a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -8,7 +8,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView import androidx.annotation.StringRes +import androidx.core.view.children import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.preference.Preference @@ -74,6 +76,7 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) setNavigationOnClickListener { activity?.onBackPressed() } 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 b308efc7..85dd9540 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 @@ -20,6 +20,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.databinding.AddRemoveSitesBinding @@ -188,7 +189,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { fun showAdd() { - val providers = allProviders.distinctBy { it.javaClass }.sortedBy { it.name } + val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } } activity?.showDialog( providers.map { "${it.name} (${it.mainUrl})" }, -1, @@ -221,6 +222,8 @@ class SettingsGeneral : PreferenceFragmentCompat() { val newSite = CustomSite(provider.javaClass.simpleName, name, url, realLang) current.add(newSite) setKey(USER_PROVIDER_API, current.toTypedArray()) + // reload apis + MainActivity.afterPluginsLoadedEvent.invoke(false) dialog.dismissSafe(activity) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 42a864a6..0bef5e9a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -105,8 +105,10 @@ class SettingsProviders : PreferenceFragmentCompat() { getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener { activity?.getApiProviderLangSettings()?.let { current -> - val languages = APIHolder.apis.map { it.lang }.toSet() - .sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName + val languages = synchronized(APIHolder.apis) { + APIHolder.apis.map { it.lang }.toSet() + .sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName + } val currentList = current.map { languages.indexOf(it) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt index 2e05baff..4fd24afe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt @@ -10,6 +10,7 @@ import com.lagradost.cloudstream3.utils.TestingUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel +import okhttp3.internal.toImmutableList class TestViewModel : ViewModel() { data class TestProgress( @@ -81,15 +82,14 @@ class TestViewModel : ViewModel() { } fun init() { - val apis = APIHolder.allProviders - total = apis.size + total = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size } updateProgress() } fun startTest() { scope = CoroutineScope(Dispatchers.Default) - val apis = APIHolder.allProviders + val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() } total = apis.size failed = 0 passed = 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt index 138a31a3..4369b22f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt @@ -107,7 +107,7 @@ class SetupFragmentExtensions : Fragment() { if (isSetup) if ( // If any available languages - apis.distinctBy { it.lang }.size > 1 + synchronized(apis) { apis.distinctBy { it.lang }.size > 1 } ) { findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index 8637fc99..59dcc402 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -51,8 +51,8 @@ class SetupFragmentProviderLanguage : Fragment() { ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) val current = ctx.getApiProviderLangSettings() - val langs = APIHolder.apis.map { it.lang }.toSet() - .sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName + val langs = synchronized(APIHolder.apis) { APIHolder.apis.map { it.lang }.toSet() + .sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName} val currentList = current.map { langs.indexOf(it) }.filter { it != -1 } // TODO LOOK INTO diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index e1cedd39..dd2b40a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -18,6 +18,8 @@ const val USER_PROVIDER_API = "user_custom_sites" const val PREFERENCES_NAME = "rebuild_preference" +// TODO degelgate by value for get & set + object DataStore { val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() 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 817e9235..71d3a1ef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -96,8 +96,10 @@ object SyncUtil { .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") + synchronized(apis) { + apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { + current.add("${it.mainUrl}/anime/$id") + } } } return current diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index 66e1e504..dd973538 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -211,7 +211,7 @@ object TestingUtils { fun getDeferredProviderTests( scope: CoroutineScope, - providers: List, + providers: Array, logger: (String) -> Unit, callback: (MainAPI, TestResultProvider) -> Unit ) { diff --git a/app/src/main/res/layout/fragment_home_head_tv.xml b/app/src/main/res/layout/fragment_home_head_tv.xml index 0a2c52b2..8592daea 100644 --- a/app/src/main/res/layout/fragment_home_head_tv.xml +++ b/app/src/main/res/layout/fragment_home_head_tv.xml @@ -1,13 +1,13 @@ - + android:orientation="vertical"> @@ -39,7 +39,9 @@ android:layout_width="wrap_content" android:layout_gravity="top|start" android:layout_marginStart="@dimen/navbar_width" - android:minWidth="150dp" /> + android:minWidth="150dp" + android:nextFocusLeft="@id/nav_rail_view" + android:nextFocusDown="@id/home_preview_play_btt" /> + android:minWidth="150dp" + android:nextFocusLeft="@id/nav_rail_view" + android:nextFocusDown="@id/home_watch_child_recyclerview" /> @null - @drawable/outline_drawable_less + @drawable/outline_drawable_forced false @@ -572,6 +572,7 @@ @drawable/ic_baseline_check_24_listview + + + - \ No newline at end of file + From 3ac462ae9625f8a546e0c95f947da00c6283eba3 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 12 Aug 2023 21:20:51 +0200 Subject: [PATCH 160/570] changed UI a bit for flashbang + fixed crash inf loading bug --- app/build.gradle.kts | 2 +- .../lagradost/cloudstream3/MainActivity.kt | 30 ++-- .../cloudstream3/ui/home/HomeFragment.kt | 23 ++- .../ui/home/HomeParentItemAdapterPreview.kt | 55 +++---- .../cloudstream3/ui/home/HomeViewModel.kt | 3 +- .../ui/settings/SettingsUpdates.kt | 4 +- .../lagradost/cloudstream3/utils/UIHelper.kt | 7 +- .../res/color/player_on_button_tv_attr.xml | 5 + app/src/main/res/color/white_attr_20.xml | 4 + .../res/drawable/player_button_tv_attr.xml | 15 ++ .../drawable/player_button_tv_attr_no_bg.xml | 9 ++ app/src/main/res/layout/fragment_home.xml | 47 ++++-- .../main/res/layout/fragment_home_head.xml | 24 ++-- .../main/res/layout/fragment_home_head_tv.xml | 135 +++++++++++------- app/src/main/res/layout/fragment_home_tv.xml | 52 ++++--- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/colors.xml | 1 - app/src/main/res/values/strings.xml | 1 - app/src/main/res/values/styles.xml | 19 ++- 19 files changed, 252 insertions(+), 185 deletions(-) create mode 100644 app/src/main/res/color/player_on_button_tv_attr.xml create mode 100644 app/src/main/res/color/white_attr_20.xml create mode 100644 app/src/main/res/drawable/player_button_tv_attr.xml create mode 100644 app/src/main/res/drawable/player_button_tv_attr_no_bg.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9300775c..cfe89c05 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,7 +51,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.2" + versionName = "4.1.3" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index d6e275ed..a8160d33 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -278,7 +278,7 @@ var app = Requests(responseParser = object : ResponseParser { class MainActivity : AppCompatActivity(), ColorPickerDialogListener { companion object { const val TAG = "MAINACT" - + var lastError: String? = null /** * Setting this will automatically enter the query in the search * next time the search fragment is opened. @@ -599,22 +599,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } override fun dispatchKeyEvent(event: KeyEvent?): Boolean { - val start = System.currentTimeMillis() - try { - val response = CommonActivity.dispatchKeyEvent(this, event) - - if (response != null) - return response - } finally { - debugAssert({ - val end = System.currentTimeMillis() - val delta = end - start - delta > 100 - }) { - "Took over 100ms to navigate, smth is VERY wrong" - } - } - + val response = CommonActivity.dispatchKeyEvent(this, event) + if (response != null) + return response return super.dispatchKeyEvent(event) } @@ -1054,10 +1041,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val errorFile = filesDir.resolve("last_error") - var lastError: String? = null if (errorFile.exists() && errorFile.isFile) { lastError = errorFile.readText(Charset.defaultCharset()) errorFile.delete() + } else { + lastError = null } val settingsForProvider = SettingsJson() @@ -1167,16 +1155,16 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } //Automatically download not existing plugins, using mode specified. - val auto_download_plugin = AutoDownloadMode.getEnum( + val autoDownloadPlugin = AutoDownloadMode.getEnum( settingsManager.getInt( getString(R.string.auto_download_plugins_key), 0 ) ) ?: AutoDownloadMode.Disable - if (auto_download_plugin != AutoDownloadMode.Disable) { + if (autoDownloadPlugin != AutoDownloadMode.Disable) { PluginManager.downloadNotExistingPluginsAndLoad( this@MainActivity, - auto_download_plugin + autoDownloadPlugin ) } } 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 6f9a1654..fa0b6dfb 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 @@ -513,9 +513,13 @@ class HomeFragment : Fragment() { fixGrid() binding?.apply { - homeChangeApiLoading.setOnClickListener(apiChangeClickListener) + //homeChangeApiLoading.setOnClickListener(apiChangeClickListener) //homeChangeApiLoading.setOnClickListener(apiChangeClickListener) homeApiFab.setOnClickListener(apiChangeClickListener) + homeChangeApi.setOnClickListener(apiChangeClickListener) + homeSwitchAccount.setOnClickListener { v -> + DataStoreHelper.showWhoIsWatching(v?.context ?: return@setOnClickListener) + } homeRandom.setOnClickListener { if (listHomepageItems.isNotEmpty()) { activity.loadSearchResult(listHomepageItems.random()) @@ -527,21 +531,9 @@ class HomeFragment : Fragment() { mutableListOf(), homeViewModel ) - fixPaddingStatusbar(homeLoadingStatusbar) + //fixPaddingStatusbar(homeLoadingStatusbar) - if (isTvSettings()) { - homeApiFab.isVisible = false - if (isTrueTvSettings()) { - homeChangeApiLoading.isVisible = true - homeChangeApiLoading.isFocusable = true - homeChangeApiLoading.isFocusableInTouchMode = true - } - // home_bookmark_select?.isFocusable = true - // home_bookmark_select?.isFocusableInTouchMode = true - } else { - homeApiFab.isVisible = true - homeChangeApiLoading.isVisible = false - } + homeApiFab.isVisible = !isTvSettings() homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -574,6 +566,7 @@ class HomeFragment : Fragment() { observe(homeViewModel.apiName) { apiName -> currentApiName = apiName binding?.homeApiFab?.text = apiName + binding?.homeChangeApi?.text = apiName } observe(homeViewModel.page) { data -> 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 1684dfe5..943f784a 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 @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.ui.home import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.FrameLayout import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat import androidx.core.view.isGone @@ -13,7 +12,6 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity @@ -41,10 +39,9 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes -import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarMargin import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView -import com.lagradost.cloudstream3.utils.UIHelper.setImage +import com.lagradost.cloudstream3.utils.UIHelper.populateChips class HomeParentItemAdapterPreview( items: MutableList, @@ -245,7 +242,11 @@ class HomeParentItemAdapterPreview( private val previewViewpager: ViewPager2 = itemView.findViewById(R.id.home_preview_viewpager) - private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview) + + private val previewViewpagerText: ViewGroup = + itemView.findViewById(R.id.home_preview_viewpager_text) + + // private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview) private var resumeHolder: View = itemView.findViewById(R.id.home_watch_holder) private var resumeRecyclerView: RecyclerView = itemView.findViewById(R.id.home_watch_child_recyclerview) @@ -254,7 +255,7 @@ class HomeParentItemAdapterPreview( itemView.findViewById(R.id.home_bookmarked_child_recyclerview) private var homeAccount: View? = - itemView.findViewById(R.id.home_switch_account) + itemView.findViewById(R.id.home_preview_switch_account) private var topPadding : View? = itemView.findViewById(R.id.home_padding) @@ -282,26 +283,8 @@ class HomeParentItemAdapterPreview( item.plot ?: "" homePreviewText.text = item.name - homePreviewTags.apply { - removeAllViews() - item.tags?.forEach { tag -> - val chip = Chip(context) - val chipDrawable = - ChipDrawable.createFromAttributes( - context, - null, - 0, - R.style.ChipFilledSemiTransparent - ) - chip.setChipDrawable(chipDrawable) - chip.text = tag - chip.isChecked = false - chip.isCheckable = false - chip.isFocusable = false - chip.isClickable = false - addView(chip) - } - } + populateChips(homePreviewTags,item.tags ?: emptyList(), R.style.ChipFilledSemiTransparent) + homePreviewTags.isGone = item.tags.isNullOrEmpty() @@ -324,7 +307,7 @@ class HomeParentItemAdapterPreview( } (binding as? FragmentHomeHeadBinding)?.apply { - homePreviewImage.setImage(item.posterUrl, item.posterHeaders) + //homePreviewImage.setImage(item.posterUrl, item.posterHeaders) homePreviewPlay.setOnClickListener { view -> viewModel.click( @@ -402,7 +385,6 @@ class HomeParentItemAdapterPreview( if (binding is FragmentHomeHeadTvBinding) { observe(viewModel.apiName) { name -> binding.homePreviewChangeApi.text = name - binding.homePreviewChangeApi2.text = name } } observe(viewModel.resumeWatching) { @@ -468,11 +450,6 @@ class HomeParentItemAdapterPreview( viewModel.loadAndCancel(api, forceReload = true, fromUI = true) } } - homePreviewChangeApi2.setOnClickListener { view -> - view.context.selectHomepage(viewModel.repo?.name) { api -> - viewModel.loadAndCancel(api, forceReload = true, fromUI = true) - } - } // This makes the hidden next buttons only available when on the info button // Otherwise you might be able to go to the next item without being at the info button @@ -517,10 +494,6 @@ class HomeParentItemAdapterPreview( } private fun updatePreview(preview: Resource>>) { - if (binding is FragmentHomeHeadTvBinding) { - binding.homePreviewChangeApi2.isGone = preview is Resource.Success - } - if (preview is Resource.Success) { homeNonePadding.apply { val params = layoutParams @@ -545,14 +518,18 @@ class HomeParentItemAdapterPreview( previewViewpager.fakeDragBy(1f) previewViewpager.endFakeDrag() previewCallback.onPageSelected(0) - previewHeader.isVisible = true + previewViewpager.isVisible = true + previewViewpagerText.isVisible = true + //previewHeader.isVisible = true } } else -> { previewAdapter.setItems(listOf(), false) previewViewpager.setCurrentItem(0, false) - previewHeader.isVisible = false + previewViewpager.isVisible = false + previewViewpagerText.isVisible = false + //previewHeader.isVisible = false } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index a2dc9821..b1ced59e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -18,6 +18,7 @@ import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.MainActivity +import com.lagradost.cloudstream3.MainActivity.Companion.lastError import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.mvvm.Resource @@ -484,7 +485,7 @@ class HomeViewModel : ViewModel() { } else if (api == null) { // API is not found aka not loaded or removed, post the loading // progress if waiting for plugins, otherwise nothing - if(PluginManager.loadedLocalPlugins) { + if(PluginManager.loadedLocalPlugins || PluginManager.checkSafeModeFile() || lastError != null) { loadAndCancel(noneApi) } else { _page.postValue(Resource.Loading()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index 9227409d..c304629a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -177,7 +177,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { val prefNames = resources.getStringArray(R.array.auto_download_plugin) val prefValues = enumValues().sortedBy { x -> x.value }.map { x -> x.value } - val current = settingsManager.getInt(getString(R.string.auto_download_plugins_pref), 0) + val current = settingsManager.getInt(getString(R.string.auto_download_plugins_key), 0) activity?.showBottomDialog( prefNames.toList(), @@ -185,7 +185,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { getString(R.string.automatic_plugin_download_mode_title), true, {}) { - settingsManager.edit().putInt(getString(R.string.auto_download_plugins_pref), prefValues[it]).apply() + settingsManager.edit().putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } return@setOnPreferenceClickListener true 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 5a393ed5..038a2f11 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -24,6 +24,7 @@ import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.annotation.IdRes +import androidx.annotation.StyleRes import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.widget.PopupMenu @@ -81,7 +82,7 @@ object UIHelper { || Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) } - fun populateChips(view: ChipGroup?, tags: List) { + fun populateChips(view: ChipGroup?, tags: List, @StyleRes style : Int = R.style.ChipFilled) { if (view == null) return view.removeAllViews() val context = view.context ?: return @@ -92,7 +93,7 @@ object UIHelper { context, null, 0, - R.style.ChipFilled + style ) chip.setChipDrawable(chipDrawable) chip.text = tag @@ -100,7 +101,7 @@ object UIHelper { chip.isCheckable = false chip.isFocusable = false chip.isClickable = false - chip.setTextColor(context.colorFromAttribute(R.attr.textColor)) + chip.setTextColor(context.colorFromAttribute(R.attr.white)) view.addView(chip) } } diff --git a/app/src/main/res/color/player_on_button_tv_attr.xml b/app/src/main/res/color/player_on_button_tv_attr.xml new file mode 100644 index 00000000..feb1eeb0 --- /dev/null +++ b/app/src/main/res/color/player_on_button_tv_attr.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/white_attr_20.xml b/app/src/main/res/color/white_attr_20.xml new file mode 100644 index 00000000..e0237df0 --- /dev/null +++ b/app/src/main/res/color/white_attr_20.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/player_button_tv_attr.xml b/app/src/main/res/drawable/player_button_tv_attr.xml new file mode 100644 index 00000000..4c90a64e --- /dev/null +++ b/app/src/main/res/drawable/player_button_tv_attr.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml b/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml new file mode 100644 index 00000000..b9b927da --- /dev/null +++ b/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index eb38e262..672a6d21 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -13,7 +13,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone" - tools:visibility="gone"> + tools:visibility="visible"> - + + + - + android:contentDescription="@string/account" + android:nextFocusLeft="@id/home_search" + android:padding="10dp" + android:src="@drawable/ic_outline_account_circle_24" /> + + tools:listitem="@layout/homepage_parent" + tools:visibility="gone" /> @@ -26,15 +26,6 @@ - - @@ -150,6 +141,7 @@ app:tint="?attr/white" /> + diff --git a/app/src/main/res/layout/fragment_home_head_tv.xml b/app/src/main/res/layout/fragment_home_head_tv.xml index d2c20bc4..03766d79 100644 --- a/app/src/main/res/layout/fragment_home_head_tv.xml +++ b/app/src/main/res/layout/fragment_home_head_tv.xml @@ -15,7 +15,6 @@ android:layout_height="0dp" /> @@ -28,7 +27,44 @@ - + + + + + + + + + - - - + + + tools:visibility="visible"> - + + + + - + android:padding="10dp" + android:src="@drawable/ic_outline_account_circle_24" + android:tag="@string/tv_no_focus_tag" + app:tint="@color/player_on_button_tv_attr" /> + - + tools:listitem="@layout/homepage_parent_tv" + tools:visibility="gone" /> + \ 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 7dd4c989..d9258c40 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -13,7 +13,6 @@ #161616 #e9eaee - #1AFFFFFF #9ba0a4 #DCDCDC diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 74515fbf..c80e0e76 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,7 +7,6 @@ auto_update auto_update_plugins auto_download_plugins_key - auto_download_plugins_pref skip_update_key prerelease_update manual_check_update diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 3ef56c22..e2f11221 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -62,7 +62,8 @@ @color/iconGrayBackground @color/boxItemBackground @color/iconColor - #FFF + @color/white + @color/black @style/CustomPreferenceThemeOverlay @@ -99,7 +100,7 @@ @@ -117,6 +118,7 @@ @color/textColor @color/grayTextColor @color/white + @color/black @color/whiteText @@ -158,7 +160,9 @@ @color/lightItemBackground @color/lightTextColor @color/lightGrayTextColor - #000 + @color/black + @color/white + @color/blackText @@ -170,6 +174,7 @@ @color/material_dynamic_neutral90 @color/material_dynamic_neutral60 @color/material_dynamic_neutral90 + @color/material_dynamic_neutral10 @color/material_on_primary_emphasis_medium @@ -747,13 +752,13 @@ @null @color/transparent @null - @drawable/player_button_tv + @drawable/player_button_tv_attr @color/white @color/transparent - @color/player_on_button_tv - @color/player_on_button_tv - @color/player_on_button_tv + @color/player_on_button_tv_attr + @color/player_on_button_tv_attr + @color/player_on_button_tv_attr wrap_content 40dp 16dp From e43b4808d1be1dfad1b308f971e54c835ee074b8 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 12 Aug 2023 21:23:43 +0200 Subject: [PATCH 161/570] phone fix --- app/src/main/res/layout/fragment_home.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 672a6d21..ac660ccd 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -93,6 +93,7 @@ +/> Date: Sat, 12 Aug 2023 21:52:37 +0200 Subject: [PATCH 162/570] should fix an issue with auto_download_plugins_key --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c80e0e76..ded7366b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,7 +6,7 @@ search_type_list auto_update auto_update_plugins - auto_download_plugins_key + auto_download_plugins_key2 skip_update_key prerelease_update manual_check_update From d2d2e41fb31a7da70adf6ef080540833a7070657 Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Sat, 12 Aug 2023 20:25:30 +0000 Subject: [PATCH 163/570] Added Simkl (#548) --- .github/workflows/build_to_archive.yml | 2 + .github/workflows/prerelease.yml | 2 + app/build.gradle.kts | 25 +- .../com/lagradost/cloudstream3/MainAPI.kt | 29 +- .../syncproviders/AccountManager.kt | 7 +- .../cloudstream3/syncproviders/SyncAPI.kt | 31 +- .../cloudstream3/syncproviders/SyncRepo.kt | 4 +- .../syncproviders/providers/AniListApi.kt | 4 +- .../syncproviders/providers/LocalList.kt | 4 +- .../syncproviders/providers/MALApi.kt | 2 +- .../syncproviders/providers/SimklApi.kt | 848 ++++++++++++++++++ .../cloudstream3/ui/result/SyncViewModel.kt | 38 +- .../ui/settings/SettingsAccount.kt | 2 + app/src/main/res/drawable/simkl_logo.xml | 9 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/settings_account.xml | 43 +- 16 files changed, 988 insertions(+), 63 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt create mode 100644 app/src/main/res/drawable/simkl_logo.xml diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml index 9cd2c523..3b7aa9ae 100644 --- a/.github/workflows/build_to_archive.yml +++ b/.github/workflows/build_to_archive.yml @@ -56,6 +56,8 @@ jobs: SIGNING_KEY_ALIAS: "key0" SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} + SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} + SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} - uses: actions/checkout@v3 with: repository: "recloudstream/cloudstream-archive" diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 856d267c..58009a7a 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -48,6 +48,8 @@ jobs: SIGNING_KEY_ALIAS: "key0" SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} + SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} + SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} - name: Create pre-release uses: "marvinpinto/action-automatic-releases@latest" with: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cfe89c05..3c12652a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,4 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import org.jetbrains.dokka.gradle.DokkaTask import java.io.ByteArrayOutputStream import java.net.URL @@ -54,17 +55,27 @@ android { versionName = "4.1.3" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") - resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") - resValue("bool", "is_prerelease", "false") + // Reads local.properties + val localProperties = gradleLocalProperties(rootDir) + buildConfigField( "String", "BUILDDATE", "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));" ) - + buildConfigField( + "String", + "SIMKL_CLIENT_ID", + "\"" + (System.getenv("SIMKL_CLIENT_ID") ?: localProperties["simkl.id"]) + "\"" + ) + buildConfigField( + "String", + "SIMKL_CLIENT_SECRET", + "\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\"" + ) testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" kapt { @@ -108,9 +119,9 @@ android { } } //toolchain { - // languageVersion.set(JavaLanguageVersion.of(17)) - // } - // jvmToolchain(17) + // languageVersion.set(JavaLanguageVersion.of(17)) + // } + // jvmToolchain(17) compileOptions { isCoreLibraryDesugaringEnabled = true @@ -211,7 +222,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.2") + implementation("com.github.Blatzar:NiceHttp:0.4.3") // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 76abda97..7790f047 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -11,9 +11,12 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.syncproviders.providers.SimklApi import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson @@ -821,7 +824,8 @@ public enum class AutoDownloadMode(val value: Int) { ; companion object { - infix fun getEnum(value: Int): AutoDownloadMode? = AutoDownloadMode.values().firstOrNull { it.value == value } + infix fun getEnum(value: Int): AutoDownloadMode? = + AutoDownloadMode.values().firstOrNull { it.value == value } } } @@ -1143,6 +1147,7 @@ interface LoadResponse { companion object { private val malIdPrefix = malApi.idPrefix private val aniListIdPrefix = aniListApi.idPrefix + private val simklIdPrefix = simklApi.idPrefix var isTrailersEnabled = true fun LoadResponse.isMovie(): Boolean { @@ -1164,6 +1169,20 @@ interface LoadResponse { this.actors = actors?.map { (actor, role) -> ActorData(actor, role = role) } } + /** + * Internal helper function to add simkl ids from other databases. + */ + private fun LoadResponse.addSimklId( + database: SimklApi.Companion.SyncServices, + id: String? + ) { + normalSafeApiCall { + this.syncData[simklIdPrefix] = + SimklApi.addIdToString(this.syncData[simklIdPrefix], database, id.toString()) + ?: return@normalSafeApiCall + } + } + @JvmName("addActorsOnly") fun LoadResponse.addActors(actors: List?) { this.actors = actors?.map { actor -> ActorData(actor) } @@ -1179,10 +1198,16 @@ interface LoadResponse { fun LoadResponse.addMalId(id: Int?) { this.syncData[malIdPrefix] = (id ?: return).toString() + this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString()) } fun LoadResponse.addAniListId(id: Int?) { this.syncData[aniListIdPrefix] = (id ?: return).toString() + this.addSimklId(SimklApi.Companion.SyncServices.AniList, id.toString()) + } + + fun LoadResponse.addSimklId(id: Int?) { + this.addSimklId(SimklApi.Companion.SyncServices.Simkl, id.toString()) } fun LoadResponse.addImdbUrl(url: String?) { @@ -1264,6 +1289,7 @@ interface LoadResponse { fun LoadResponse.addImdbId(id: String?) { // TODO add imdb sync + this.addSimklId(SimklApi.Companion.SyncServices.Imdb, id) } fun LoadResponse.addTrackId(id: String?) { @@ -1276,6 +1302,7 @@ interface LoadResponse { fun LoadResponse.addTMDbId(id: String?) { // TODO add TMDb sync + this.addSimklId(SimklApi.Companion.SyncServices.Tmdb, id) } fun LoadResponse.addRating(text: String?) { 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 8ce6bae2..8bf8dffa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -11,6 +11,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val malApi = MALApi(0) val aniListApi = AniListApi(0) val openSubtitlesApi = OpenSubtitlesApi(0) + val simklApi = SimklApi(0) val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() val localListApi = LocalList() @@ -18,19 +19,19 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { // used to login via app intent val OAuth2Apis get() = listOf( - malApi, aniListApi + malApi, aniListApi, simklApi ) // this needs init with context and can be accessed in settings val accountManagers get() = listOf( - malApi, aniListApi, openSubtitlesApi, //nginxApi + malApi, aniListApi, openSubtitlesApi, simklApi //nginxApi ) // used for active syncing val SyncApis get() = listOf( - SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi) + SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi), SyncRepo(simklApi) ) 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 8c76c5bf..ed496326 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -10,7 +10,8 @@ enum class SyncIdName { MyAnimeList, Trakt, Imdb, - LocalList + Simkl, + LocalList, } interface SyncAPI : OAuth2API { @@ -35,9 +36,9 @@ interface SyncAPI : OAuth2API { 4 -> PlanToWatch 5 -> ReWatching */ - suspend fun score(id: String, status: SyncStatus): Boolean + suspend fun score(id: String, status: AbstractSyncStatus): Boolean - suspend fun getStatus(id: String): SyncStatus? + suspend fun getStatus(id: String): AbstractSyncStatus? suspend fun getResult(id: String): SyncResult? @@ -59,14 +60,24 @@ interface SyncAPI : OAuth2API { override var id: Int? = null, ) : SearchResponse - data class SyncStatus( - val status: Int, + abstract class AbstractSyncStatus { + abstract var status: Int + /** 1-10 */ - val score: Int?, - val watchedEpisodes: Int?, - var isFavorite: Boolean? = null, - var maxEpisodes: Int? = null, - ) + abstract var score: Int? + abstract var watchedEpisodes: Int? + abstract var isFavorite: Boolean? + abstract var maxEpisodes: Int? + } + + data class SyncStatus( + override var status: Int, + /** 1-10 */ + override var score: Int?, + override var watchedEpisodes: Int?, + override var isFavorite: Boolean? = null, + override var maxEpisodes: Int? = null, + ) : AbstractSyncStatus() data class SyncResult( /**Used to verify*/ 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 85b877e0..9363cb6f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt @@ -18,11 +18,11 @@ class SyncRepo(private val repo: SyncAPI) { repo.requireLibraryRefresh = value } - suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource { + suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): 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") } } 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 0010ce25..d0c88901 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 @@ -158,7 +158,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) } - override suspend fun getStatus(id: String): SyncAPI.SyncStatus? { + override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { val internalId = id.toIntOrNull() ?: return null val data = getDataAboutId(internalId) ?: return null @@ -171,7 +171,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) } - override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { + override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { return postDataAboutId( id.toIntOrNull() ?: return false, fromIntToAnimeStatus(status.status), 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 index 7dd43fe7..e6ca9711 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -45,11 +45,11 @@ class LocalList : SyncAPI { override val mainUrl = "" override val syncIdName = SyncIdName.LocalList - override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { + override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { return true } - override suspend fun getStatus(id: String): SyncAPI.SyncStatus? { + override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { return null } 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 5164b606..02826401 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 @@ -91,7 +91,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { return Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first() } - override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { + override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { return setScoreRequest( id.toIntOrNull() ?: return false, fromIntToAnimeStatus(status.status), diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt new file mode 100644 index 00000000..64afb0e2 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -0,0 +1,848 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import androidx.annotation.StringRes +import androidx.core.net.toUri +import androidx.fragment.app.FragmentActivity +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.debugAssert +import com.lagradost.cloudstream3.mvvm.debugPrint +import com.lagradost.cloudstream3.mvvm.logError +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.toJson +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import okhttp3.Interceptor +import okhttp3.Response +import java.math.BigInteger +import java.security.SecureRandom +import java.text.SimpleDateFormat +import java.time.Instant +import java.util.Date +import java.util.TimeZone + +class SimklApi(index: Int) : AccountManager(index), SyncAPI { + override var name = "Simkl" + override val key = "simkl-key" + override val redirectUrl = "simkl" + override val idPrefix = "simkl" + override var requireLibraryRefresh = true + override var mainUrl = "https://api.simkl.com" + override val icon = R.drawable.simkl_logo + override val requiresLogin = false + override val createAccountUrl = "$mainUrl/signup" + override val syncIdName = SyncIdName.Simkl + private val token: String? + get() = getKey(accountId, SIMKL_TOKEN_KEY).also { + debugAssert({ it == null }) { "No ${this.name} token!" } + } + + /** Automatically adds simkl auth headers */ + private val interceptor = HeaderInterceptor() + + /** + * This is required to override the reported last activity as simkl activites + * may not always update based on testing. + */ + private var lastScoreTime = -1L + + companion object { + private const val clientId = BuildConfig.SIMKL_CLIENT_ID + private const val clientSecret = BuildConfig.SIMKL_CLIENT_SECRET + private var lastLoginState = "" + + const val SIMKL_TOKEN_KEY: String = "simkl_token" + const val SIMKL_USER_KEY: String = "simkl_user" + const val SIMKL_CACHED_LIST: String = "simkl_cached_list" + const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time" + + /** 2014-09-01T09:10:11Z -> 1409562611 */ + private const val simklDateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + fun getUnixTime(string: String?): Long? { + return try { + SimpleDateFormat(simklDateFormat).apply { + this.timeZone = TimeZone.getTimeZone("UTC") + }.parse( + string ?: return null + )?.toInstant()?.epochSecond + } catch (e: Exception) { + logError(e) + return null + } + } + + /** 1409562611 -> 2014-09-01T09:10:11Z */ + fun getDateTime(unixTime: Long?): String? { + return try { + SimpleDateFormat(simklDateFormat).apply { + this.timeZone = TimeZone.getTimeZone("UTC") + }.format( + Date.from( + Instant.ofEpochSecond( + unixTime ?: return null + ) + ) + ) + } catch (e: Exception) { + null + } + } + + /** + * Set of sync services simkl is compatible with. + * Add more as required: https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id + */ + enum class SyncServices(val originalName: String) { + Simkl("simkl"), + Imdb("imdb"), + Tmdb("tmdb"), + AniList("anilist"), + Mal("mal"), + } + + /** + * The ID string is a way to keep a collection of services in one single ID using a map + * This adds a database service (like imdb) to the string and returns the new string. + */ + fun addIdToString(idString: String?, database: SyncServices, id: String?): String? { + if (id == null) return idString + return (readIdFromString(idString) + mapOf(database to id)).toJson() + } + + /** Read the id string to get all other ids */ + private fun readIdFromString(idString: String?): Map { + return tryParseJson(idString) ?: return emptyMap() + } + + fun getPosterUrl(poster: String): String { + return "https://wsrv.nl/?url=https://simkl.in/posters/${poster}_m.webp" + } + + private fun getUrlFromId(id: Int): String { + return "https://simkl.com/shows/$id" + } + + enum class SimklListStatusType( + var value: Int, + @StringRes val stringRes: Int, + val originalName: String? + ) { + Watching(0, R.string.type_watching, "watching"), + Completed(1, R.string.type_completed, "completed"), + Paused(2, R.string.type_on_hold, "hold"), + Dropped(3, R.string.type_dropped, "dropped"), + Planning(4, R.string.type_plan_to_watch, "plantowatch"), + ReWatching(5, R.string.type_re_watching, "watching"), + None(-1, R.string.none, null); + + companion object { + fun fromString(string: String): SimklListStatusType? { + return SimklListStatusType.values().firstOrNull { + it.originalName == string + } + } + } + } + + // ------------------- + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class TokenRequest( + @JsonProperty("code") val code: String, + @JsonProperty("client_id") val client_id: String = clientId, + @JsonProperty("client_secret") val client_secret: String = clientSecret, + @JsonProperty("redirect_uri") val redirect_uri: String = "$appString://simkl", + @JsonProperty("grant_type") val grant_type: String = "authorization_code" + ) + + data class TokenResponse( + /** No expiration date */ + val access_token: String, + val token_type: String, + val scope: String + ) + // ------------------- + + /** https://simkl.docs.apiary.io/#reference/users/settings/receive-settings */ + data class SettingsResponse( + val user: User + ) { + data class User( + val name: String, + /** Url */ + val avatar: String + ) + } + + // ------------------- + data class ActivitiesResponse( + val all: String?, + val tv_shows: UpdatedAt, + val anime: UpdatedAt, + val movies: UpdatedAt, + ) { + data class UpdatedAt( + val all: String?, + val removed_from_list: String?, + val rated_at: String?, + ) + } + + /** https://simkl.docs.apiary.io/#reference/tv/episodes/get-tv-show-episodes */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class EpisodeMetadata( + @JsonProperty("title") val title: String?, + @JsonProperty("description") val description: String?, + @JsonProperty("season") val season: Int?, + @JsonProperty("episode") val episode: Int, + @JsonProperty("img") val img: String? + ) { + companion object { + fun convertToEpisodes(list: List?): List { + return list?.map { + MediaObject.Season.Episode(it.episode) + } ?: emptyList() + } + + fun convertToSeasons(list: List?): List { + return list?.filter { it.season != null }?.groupBy { + it.season + }?.map { (season, episodes) -> + MediaObject.Season(season!!, convertToEpisodes(episodes)) + } ?: emptyList() + } + } + } + + /** + * https://simkl.docs.apiary.io/#introduction/about-simkl-api/standard-media-objects + * Useful for finding shows from metadata + */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + open class MediaObject( + @JsonProperty("title") val title: String?, + @JsonProperty("year") val year: Int?, + @JsonProperty("ids") val ids: Ids?, + @JsonProperty("poster") val poster: String? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("seasons") val seasons: List? = null, + @JsonProperty("episodes") val episodes: List? = null + ) { + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class Season( + @JsonProperty("number") val number: Int, + @JsonProperty("episodes") val episodes: List + ) { + data class Episode(@JsonProperty("number") val number: Int) + } + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class Ids( + @JsonProperty("simkl") val simkl: Int?, + @JsonProperty("imdb") val imdb: String? = null, + @JsonProperty("tmdb") val tmdb: String? = null, + @JsonProperty("mal") val mal: String? = null, + @JsonProperty("anilist") val anilist: String? = null, + ) { + companion object { + fun fromMap(map: Map): Ids { + return Ids( + simkl = map[SyncServices.Simkl]?.toIntOrNull(), + imdb = map[SyncServices.Imdb], + tmdb = map[SyncServices.Tmdb], + mal = map[SyncServices.Mal], + anilist = map[SyncServices.AniList] + ) + } + } + } + + fun toSyncSearchResult(): SyncAPI.SyncSearchResult? { + return SyncAPI.SyncSearchResult( + this.title ?: return null, + "Simkl", + this.ids?.simkl?.toString() ?: return null, + getUrlFromId(this.ids.simkl), + this.poster?.let { getPosterUrl(it) }, + if (this.type == "movie") TvType.Movie else TvType.TvSeries + ) + } + } + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + class RatingMediaObject( + @JsonProperty("title") title: String?, + @JsonProperty("year") year: Int?, + @JsonProperty("ids") ids: Ids?, + @JsonProperty("rating") val rating: Int, + @JsonProperty("rated_at") val rated_at: String? = getDateTime(unixTime) + ) : MediaObject(title, year, ids) + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + class StatusMediaObject( + @JsonProperty("title") title: String?, + @JsonProperty("year") year: Int?, + @JsonProperty("ids") ids: Ids?, + @JsonProperty("to") val to: String, + @JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime) + ) : MediaObject(title, year, ids) + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + class HistoryMediaObject( + @JsonProperty("title") title: String?, + @JsonProperty("year") year: Int?, + @JsonProperty("ids") ids: Ids?, + @JsonProperty("seasons") seasons: List?, + @JsonProperty("episodes") episodes: List?, + ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class StatusRequest( + @JsonProperty("movies") val movies: List, + @JsonProperty("shows") val shows: List + ) + + /** https://simkl.docs.apiary.io/#reference/sync/get-all-items/get-all-items-in-the-user's-watchlist */ + data class AllItemsResponse( + val shows: List, + val anime: List, + val movies: List, + ) { + companion object { + fun merge(first: AllItemsResponse?, second: AllItemsResponse?): AllItemsResponse { + + // Replace the first item with the same id, or add the new item + fun MutableList.replaceOrAddItem(newItem: T, predicate: (T) -> Boolean) { + for (i in this.indices) { + if (predicate(this[i])) { + this[i] = newItem + return + } + } + this.add(newItem) + } + + // + fun merge( + first: List?, + second: List? + ): List { + return (first?.toMutableList() ?: mutableListOf()).apply { + second?.forEach { secondShow -> + this.replaceOrAddItem(secondShow) { + it.getIds().simkl == secondShow.getIds().simkl + } + } + } + } + + return AllItemsResponse( + merge(first?.shows, second?.shows), + merge(first?.anime, second?.anime), + merge(first?.movies, second?.movies), + ) + } + } + + interface Metadata { + val last_watched_at: String? + val status: String? + val user_rating: Int? + val last_watched: String? + val watched_episodes_count: Int? + val total_episodes_count: Int? + + fun getIds(): ShowMetadata.Show.Ids + fun toLibraryItem(): SyncAPI.LibraryItem + } + + data class MovieMetadata( + override val last_watched_at: String?, + override val status: String, + override val user_rating: Int?, + override val last_watched: String?, + override val watched_episodes_count: Int?, + override val total_episodes_count: Int?, + val movie: ShowMetadata.Show + ) : Metadata { + override fun getIds(): ShowMetadata.Show.Ids { + return this.movie.ids + } + + override fun toLibraryItem(): SyncAPI.LibraryItem { + return SyncAPI.LibraryItem( + this.movie.title, + "https://simkl.com/tv/${movie.ids.simkl}", + movie.ids.simkl.toString(), + this.watched_episodes_count, + this.total_episodes_count, + this.user_rating?.times(10), + getUnixTime(last_watched_at) ?: 0, + "Simkl", + TvType.Movie, + this.movie.poster?.let { getPosterUrl(it) }, + null, + null, + movie.ids.simkl + ) + } + } + + data class ShowMetadata( + override val last_watched_at: String?, + override val status: String, + override val user_rating: Int?, + override val last_watched: String?, + override val watched_episodes_count: Int?, + override val total_episodes_count: Int?, + val show: Show + ) : Metadata { + override fun getIds(): Show.Ids { + return this.show.ids + } + + override fun toLibraryItem(): SyncAPI.LibraryItem { + return SyncAPI.LibraryItem( + this.show.title, + "https://simkl.com/tv/${show.ids.simkl}", + show.ids.simkl.toString(), + this.watched_episodes_count, + this.total_episodes_count, + this.user_rating?.times(10), + getUnixTime(last_watched_at) ?: 0, + "Simkl", + TvType.Anime, + this.show.poster?.let { getPosterUrl(it) }, + null, + null, + show.ids.simkl + ) + } + + data class Show( + val title: String, + val poster: String?, + val year: Int?, + val ids: Ids, + ) { + data class Ids( + val simkl: Int, + val slug: String?, + val imdb: String?, + val zap2it: String?, + val tmdb: String?, + val offen: String?, + val tvdb: String?, + val mal: String?, + val anidb: String?, + val anilist: String?, + val traktslug: String? + ) { + fun matchesId(database: SyncServices, id: String): Boolean { + return when (database) { + SyncServices.Simkl -> this.simkl == id.toIntOrNull() + SyncServices.AniList -> this.anilist == id + SyncServices.Mal -> this.mal == id + SyncServices.Tmdb -> this.tmdb == id + SyncServices.Imdb -> this.imdb == id + } + } + } + } + } + } + } + + /** + * Appends api keys to the requests + **/ + private inner class HeaderInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + debugPrint { "${this@SimklApi.name} made request to ${chain.request().url}" } + return chain.proceed( + chain.request() + .newBuilder() + .addHeader("Authorization", "Bearer $token") + .addHeader("simkl-api-key", clientId) + .build() + ) + } + } + + private suspend fun getUser(): SettingsResponse.User? { + return suspendSafeApiCall { + app.post("$mainUrl/users/settings", interceptor = interceptor) + .parsedSafe()?.user + } + } + + class SimklSyncStatus( + override var status: Int, + override var score: Int?, + override var watchedEpisodes: Int?, + val episodes: Array?, + override var isFavorite: Boolean? = null, + override var maxEpisodes: Int? = null, + ) : SyncAPI.AbstractSyncStatus() + + override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { + val realIds = readIdFromString(id) + val foundItem = getSyncListSmart()?.let { list -> + listOf(list.shows, list.anime, list.movies).flatten().firstOrNull { show -> + realIds.any { (database, id) -> + show.getIds().matchesId(database, id) + } + } + } + + // Search to get episodes + val searchResult = searchByIds(realIds)?.firstOrNull() + val episodes = getEpisodes(searchResult?.ids?.simkl, searchResult?.type) + + if (foundItem != null) { + return SimklSyncStatus( + status = foundItem.status?.let { SimklListStatusType.fromString(it)?.value } + ?: return null, + score = foundItem.user_rating, + watchedEpisodes = foundItem.watched_episodes_count, + maxEpisodes = foundItem.total_episodes_count, + episodes = episodes + ) + } else { + return if (searchResult != null) { + SimklSyncStatus( + status = SimklListStatusType.None.value, + score = 0, + watchedEpisodes = 0, + maxEpisodes = if (searchResult.type == "movie") 0 else null, + episodes = episodes + ) + } else { + null + } + } + } + + override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { + val parsedId = readIdFromString(id) + lastScoreTime = unixTime + + if (status.status == SimklListStatusType.None.value) { + return app.post( + "$mainUrl/sync/history/remove", + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + emptyList(), + emptyList() + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } + + val realScore = status.score + val ratingResponseSuccess = if (realScore != null) { + // Remove rating if score is 0 + val ratingsSuffix = if (realScore == 0) "/remove" else "" + debugPrint { "Rate ${this.name} item: rating=$realScore" } + app.post( + "$mainUrl/sync/ratings$ratingsSuffix", + json = StatusRequest( + // Not possible to know if TV or Movie + shows = listOf( + RatingMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + realScore + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } else { + true + } + + val simklStatus = status as? SimklSyncStatus + // All episodes if marked as completed + val watchedEpisodes = if (status.status == SimklListStatusType.Completed.value) { + simklStatus?.episodes?.size + } else { + status.watchedEpisodes + } + + // Only post episodes if available episodes and the status is correct + val episodeResponseSuccess = + if (simklStatus != null && watchedEpisodes != null && !simklStatus.episodes.isNullOrEmpty() && listOf( + SimklListStatusType.Paused.value, + SimklListStatusType.Dropped.value, + SimklListStatusType.Watching.value, + SimklListStatusType.Completed.value, + SimklListStatusType.ReWatching.value + ).contains(status.status) + ) { + val cutEpisodes = simklStatus.episodes.take(watchedEpisodes) + + val (seasons, episodes) = if (cutEpisodes.any { it.season != null }) { + EpisodeMetadata.convertToSeasons(cutEpisodes) to null + } else { + null to EpisodeMetadata.convertToEpisodes(cutEpisodes) + } + + debugPrint { "Synced history for ${status.watchedEpisodes} given size of ${simklStatus.episodes.size}: seasons=${seasons?.toList()}, episodes=${episodes?.toList()}" } + val episodeResponse = app.post( + "$mainUrl/sync/history", + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + seasons, + episodes + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ) + episodeResponse.isSuccessful + } else true + + val newStatus = + SimklListStatusType.values().firstOrNull { it.value == status.status }?.originalName + ?: SimklListStatusType.Watching.originalName + + val statusResponseSuccess = if (newStatus != null) { + debugPrint { "Add to ${this.name} list: status=$newStatus" } + app.post( + "$mainUrl/sync/add-to-list", + json = StatusRequest( + shows = listOf( + StatusMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + newStatus + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } else { + true + } + + debugPrint { "All scoring complete: rating=$ratingResponseSuccess, status=$statusResponseSuccess, episode=$episodeResponseSuccess" } + requireLibraryRefresh = true + return ratingResponseSuccess && statusResponseSuccess && episodeResponseSuccess + } + + + /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ + suspend fun searchByIds(serviceMap: Map): Array? { + if (serviceMap.isEmpty()) return emptyArray() + + return app.get( + "$mainUrl/search/id", + params = mapOf("client_id" to clientId) + serviceMap.map { (service, id) -> + service.originalName to id + } + ).parsedSafe() + } + + suspend fun getEpisodes(simklId: Int?, type: String?): Array? { + if (simklId == null) return null + val url = when (type) { + "anime" -> "https://api.simkl.com/anime/episodes/$simklId" + "tv" -> "https://api.simkl.com/tv/episodes/$simklId" + "movie" -> return null + else -> return null + } + return app.get(url, params = mapOf("client_id" to clientId)).parsedSafe() + } + + override suspend fun search(name: String): List? { + return app.get( + "$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name) + ).parsedSafe>()?.mapNotNull { it.toSyncSearchResult() } + } + + override fun authenticate(activity: FragmentActivity?) { + lastLoginState = BigInteger(130, SecureRandom()).toString(32) + val url = + "https://simkl.com/oauth/authorize?response_type=code&client_id=$clientId&redirect_uri=$appString://${redirectUrl}&state=$lastLoginState" + openBrowser(url, activity) + } + + override fun loginInfo(): AuthAPI.LoginInfo? { + return getKey(accountId, SIMKL_USER_KEY)?.let { user -> + AuthAPI.LoginInfo( + name = user.name, + profilePicture = user.avatar, + accountIndex = accountIndex + ) + } + } + + override fun logOut() { + requireLibraryRefresh = true + removeAccountKeys() + } + + override suspend fun getResult(id: String): SyncAPI.SyncResult? { + return null + } + + private suspend fun getSyncListSince(since: Long?): AllItemsResponse { + val params = getDateTime(since)?.let { + mapOf("date_from" to it) + } ?: emptyMap() + + return app.get( + "$mainUrl/sync/all-items/", + params = params, + interceptor = interceptor + ).parsed() + } + + private suspend fun getActivities(): ActivitiesResponse? { + return app.post("$mainUrl/sync/activities", interceptor = interceptor).parsedSafe() + } + + private fun getSyncListCached(): AllItemsResponse? { + return getKey(accountId, SIMKL_CACHED_LIST) + } + + private suspend fun getSyncListSmart(): AllItemsResponse? { + if (token == null) return null + + val activities = getActivities() + val lastCacheUpdate = getKey(accountId, SIMKL_CACHED_LIST_TIME) + val lastRemoval = listOf( + activities?.tv_shows?.removed_from_list, + activities?.anime?.removed_from_list, + activities?.movies?.removed_from_list + ).maxOf { + getUnixTime(it) ?: -1 + } + val lastRealUpdate = + listOf( + activities?.tv_shows?.all, + activities?.anime?.all, + activities?.movies?.all, + ).maxOf { + getUnixTime(it) ?: -1 + } + + debugPrint { "Cache times: lastCacheUpdate=$lastCacheUpdate, lastRemoval=$lastRemoval, lastRealUpdate=$lastRealUpdate" } + val list = if (lastCacheUpdate == null || lastCacheUpdate < lastRemoval) { + debugPrint { "Full list update in ${this.name}." } + setKey(accountId, SIMKL_CACHED_LIST_TIME, lastRemoval) + getSyncListSince(null) + } else if (lastCacheUpdate < lastRealUpdate || lastCacheUpdate < lastScoreTime) { + debugPrint { "Partial list update in ${this.name}." } + setKey(accountId, SIMKL_CACHED_LIST_TIME, lastCacheUpdate) + AllItemsResponse.merge(getSyncListCached(), getSyncListSince(lastCacheUpdate)) + } else { + debugPrint { "Cached list update in ${this.name}." } + getSyncListCached() + } + debugPrint { "List sizes: movies=${list?.movies?.size}, shows=${list?.shows?.size}, anime=${list?.anime?.size}" } + + setKey(accountId, SIMKL_CACHED_LIST, list) + + return list + } + + + override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata? { + val list = getSyncListSmart() ?: return null + + val baseMap = + SimklListStatusType.values() + .filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value } + .associate { + it.stringRes to emptyList() + } + + val syncMap = listOf(list.anime, list.movies, list.shows) + .flatten() + .groupBy { + it.status + } + .mapNotNull { (status, list) -> + val stringRes = + status?.let { SimklListStatusType.fromString(it)?.stringRes } + ?: return@mapNotNull null + val libraryList = list.map { it.toLibraryItem() } + stringRes to libraryList + }.toMap() + + return SyncAPI.LibraryMetadata( + (baseMap + syncMap).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 { + val simklUrlRegex = Regex("""https://simkl\.com/[^/]*/(\d+).*""") + return simklUrlRegex.find(url)?.groupValues?.get(1) ?: "" + } + + override suspend fun handleRedirect(url: String): Boolean { + val uri = url.toUri() + val state = uri.getQueryParameter("state") + // Ensure consistent state + if (state != lastLoginState) return false + lastLoginState = "" + + val code = uri.getQueryParameter("code") ?: return false + val token = app.post( + "$mainUrl/oauth/token", json = TokenRequest(code) + ).parsedSafe() ?: return false + + switchToNewAccount() + setKey(accountId, SIMKL_TOKEN_KEY, token.access_token) + + val user = getUser() + if (user == null) { + removeKey(accountId, SIMKL_TOKEN_KEY) + switchToOldAccount() + return false + } + + setKey(accountId, SIMKL_USER_KEY, user) + registerAccount() + requireLibraryRefresh = true + + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt index 91415d26..a3e2ed87 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt @@ -36,18 +36,18 @@ class SyncViewModel : ViewModel() { val metadata: LiveData> get() = _metaResponse - private val _userDataResponse: MutableLiveData?> = + private val _userDataResponse: MutableLiveData?> = MutableLiveData(null) - val userData: LiveData?> get() = _userDataResponse + val userData: LiveData?> get() = _userDataResponse // prefix, id - private var syncs = mutableMapOf() + private val syncs = mutableMapOf() //private val _syncIds: MutableLiveData> = // MutableLiveData(mutableMapOf()) //val syncIds: LiveData> get() = _syncIds - fun getSyncs() : Map { + fun getSyncs(): Map { return syncs } @@ -106,7 +106,7 @@ class SyncViewModel : ViewModel() { Log.i(TAG, "addFromUrl = $url") if (url == null || hasAddedFromUrl.contains(url)) return@ioSafe - if(!url.startsWith("http")) return@ioSafe + if (!url.startsWith("http")) return@ioSafe SyncUtil.getIdsFromUrl(url)?.let { (malId, aniListId) -> hasAddedFromUrl.add(url) @@ -150,7 +150,8 @@ class SyncViewModel : ViewModel() { val user = userData.value if (user is Resource.Success) { - _userDataResponse.postValue(Resource.Success(user.value.copy(watchedEpisodes = episodes))) + user.value.watchedEpisodes = episodes + _userDataResponse.postValue(Resource.Success(user.value)) } } @@ -158,7 +159,8 @@ class SyncViewModel : ViewModel() { Log.i(TAG, "setScore = $score") val user = userData.value if (user is Resource.Success) { - _userDataResponse.postValue(Resource.Success(user.value.copy(score = score))) + user.value.score = score + _userDataResponse.postValue(Resource.Success(user.value)) } } @@ -167,7 +169,8 @@ class SyncViewModel : ViewModel() { if (which < -1 || which > 5) return // validate input val user = userData.value if (user is Resource.Success) { - _userDataResponse.postValue(Resource.Success(user.value.copy(status = which))) + user.value.status = which + _userDataResponse.postValue(Resource.Success(user.value)) } } @@ -185,17 +188,16 @@ class SyncViewModel : ViewModel() { fun modifyMaxEpisode(episodeNum: Int) { Log.i(TAG, "modifyMaxEpisode = $episodeNum") modifyData { status -> - status.copy( - watchedEpisodes = maxOf( - episodeNum, - status.watchedEpisodes ?: return@modifyData null - ) + status.watchedEpisodes = maxOf( + episodeNum, + status.watchedEpisodes ?: return@modifyData null ) + status } } /// modifies the current sync data, return null if you don't want to change it - private fun modifyData(update: ((SyncAPI.SyncStatus) -> (SyncAPI.SyncStatus?))) = + private fun modifyData(update: ((SyncAPI.AbstractSyncStatus) -> (SyncAPI.AbstractSyncStatus?))) = ioSafe { syncs.amap { (prefix, id) -> repos.firstOrNull { it.idPrefix == prefix }?.let { repo -> @@ -245,8 +247,12 @@ class SyncViewModel : ViewModel() { // shitty way to sort anilist first, as it has trailers while mal does not if (syncs.containsKey(aniListApi.idPrefix)) { try { // swap can throw error - Collections.swap(current, current.indexOfFirst { it.first == aniListApi.idPrefix }, 0) - } catch (t : Throwable) { + Collections.swap( + current, + current.indexOfFirst { it.first == aniListApi.idPrefix }, + 0 + ) + } catch (t: Throwable) { logError(t) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 33316020..b3225d5c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API @@ -257,6 +258,7 @@ class SettingsAccount : PreferenceFragmentCompat() { listOf( R.string.mal_key to malApi, R.string.anilist_key to aniListApi, + R.string.simkl_key to simklApi, R.string.opensubtitles_key to openSubtitlesApi, ) diff --git a/app/src/main/res/drawable/simkl_logo.xml b/app/src/main/res/drawable/simkl_logo.xml new file mode 100644 index 00000000..eb29fb5b --- /dev/null +++ b/app/src/main/res/drawable/simkl_logo.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ded7366b..13251c7c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -449,6 +449,7 @@ Put the title under the poster anilist_key + simkl_key mal_key opensubtitles_key nginx_key diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml index d4bae8c4..d3dbcb31 100644 --- a/app/src/main/res/xml/settings_account.xml +++ b/app/src/main/res/xml/settings_account.xml @@ -1,27 +1,32 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> + android:icon="@drawable/mal_logo" + android:key="@string/mal_key" /> - - - - + android:icon="@drawable/ic_anilist_icon" + android:key="@string/anilist_key" /> - - - - - - - - + + + + + + + + + + + + + + + \ No newline at end of file From 3ab9e11350aab59c4ada14ec33e6f72fa324f2e9 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 12 Aug 2023 23:41:53 +0200 Subject: [PATCH 164/570] fixed SimklApi subscription --- .../cloudstream3/syncproviders/providers/SimklApi.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 64afb0e2..64cebfc6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -316,9 +316,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** https://simkl.docs.apiary.io/#reference/sync/get-all-items/get-all-items-in-the-user's-watchlist */ data class AllItemsResponse( - val shows: List, - val anime: List, - val movies: List, + @JsonProperty("shows") + val shows: List = emptyList(), + @JsonProperty("anime") + val anime: List = emptyList(), + @JsonProperty("movies") + val movies: List = emptyList(), ) { companion object { fun merge(first: AllItemsResponse?, second: AllItemsResponse?): AllItemsResponse { From 0eb241e6cb4a6d2f5bff7728f7ea0ac7b0a0e904 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 12 Aug 2023 23:54:37 +0200 Subject: [PATCH 165/570] fixed fab expand --- .../com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt | 1 + 1 file changed, 1 insertion(+) 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 3ddaee61..633ee762 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 @@ -905,6 +905,7 @@ open class ResultFragmentPhone : FullScreenPlayer(), observe(viewModel.watchStatus) { watchType -> binding?.resultBookmarkFab?.apply { + setText(watchType.stringRes) if (watchType == WatchType.NONE) { context?.colorFromAttribute(R.attr.white) } else { From 74867bed1cc96b3a494fb107a6ff9c251579b525 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Sun, 13 Aug 2023 21:07:36 +0530 Subject: [PATCH 166/570] Update SpeedoStream.kt (#552) Fixes YoMovies Provider. --- .../java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt index 90104ace..3f6fff2f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt @@ -13,7 +13,7 @@ class SpeedoStream1 : SpeedoStream() { open class SpeedoStream : ExtractorApi() { override val name = "SpeedoStream" - override val mainUrl = "https://speedostream.com" + override val mainUrl = "https://speedostream.mom" override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?): List { From 4d98690adbc8a903b3545e094d5b9ca7099e408a Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 15 Aug 2023 02:05:07 +0200 Subject: [PATCH 167/570] small fix to home load --- .../cloudstream3/plugins/PluginManager.kt | 4 + .../cloudstream3/ui/home/HomeViewModel.kt | 14 ++- .../settings/extensions/ExtensionsFragment.kt | 107 +++++++++++------- .../main/res/layout/fragment_extensions.xml | 1 + 4 files changed, 81 insertions(+), 45 deletions(-) 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 4c32088a..87b0ba3b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -165,6 +165,9 @@ object PluginManager { var loadedLocalPlugins = false private set + + var loadedOnlinePlugins = false + private set private val gson = Gson() private suspend fun maybeLoadPlugin(context: Context, file: File) { @@ -278,6 +281,7 @@ object PluginManager { } // ioSafe { + loadedOnlinePlugins = true afterPluginsLoadedEvent.invoke(false) // } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index b1ced59e..e8cf8863 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -103,7 +103,7 @@ class HomeViewModel : ViewModel() { private var currentShuffledList: List = listOf() private fun autoloadRepo(): APIRepository { - return APIRepository(synchronized(apis) { apis.first { it.hasMainPage }}) + return APIRepository(synchronized(apis) { apis.first { it.hasMainPage } }) } private val _availableWatchStatusTypes = @@ -185,8 +185,9 @@ class HomeViewModel : ViewModel() { } private var onGoingLoad: Job? = null - private var isCurrentlyLoadingName : String? = null + private var isCurrentlyLoadingName: String? = null private fun loadAndCancel(api: MainAPI) { + //println("loaded ${api.name}") onGoingLoad?.cancel() isCurrentlyLoadingName = api.name onGoingLoad = load(api) @@ -290,7 +291,7 @@ class HomeViewModel : ViewModel() { } } - private fun load(api: MainAPI) : Job = ioSafe { + private fun load(api: MainAPI): Job = ioSafe { repo = //if (api != null) { APIRepository(api) //} else { @@ -455,9 +456,9 @@ class HomeViewModel : ViewModel() { fromUI: Boolean = false ) = ioSafe { + //println("trying to load $preferredApiName") // Since plugins are loaded in stages this function can get called multiple times. // The issue with this is that the homepage may be fetched multiple times while the first request is loading - val api = getApiFromNameNull(preferredApiName) // api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true val currentPage = page.value @@ -467,6 +468,7 @@ class HomeViewModel : ViewModel() { return@ioSafe } + val api = getApiFromNameNull(preferredApiName) if (preferredApiName == noneApi.name) { // just set to random if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) @@ -485,10 +487,12 @@ class HomeViewModel : ViewModel() { } else if (api == null) { // API is not found aka not loaded or removed, post the loading // progress if waiting for plugins, otherwise nothing - if(PluginManager.loadedLocalPlugins || PluginManager.checkSafeModeFile() || lastError != null) { + if (PluginManager.loadedOnlinePlugins || PluginManager.checkSafeModeFile() || lastError != null) { loadAndCancel(noneApi) } else { _page.postValue(Resource.Loading()) + if (preferredApiName != null) + _apiName.postValue(preferredApiName) } } else { // if the api is found, then set it to it and save key diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 8bc947c5..553e7675 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.settings.extensions import android.content.ClipboardManager import android.content.Context import android.content.DialogInterface +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -12,6 +13,8 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.core.view.marginBottom +import androidx.core.view.marginTop import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController @@ -33,7 +36,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import com.lagradost.cloudstream3.widget.LinearRecycleViewLayoutManager class ExtensionsFragment : Fragment() { var binding: FragmentExtensionsBinding? = null @@ -84,51 +86,76 @@ class ExtensionsFragment : Fragment() { setUpToolbar(R.string.extensions) - binding?.repoRecyclerView?.setLinearListLayout( - isHorizontal = false, - nextUp = R.id.settings_toolbar, //FOCUS_SELF, // back has no id so we cant :pensive: - nextDown = R.id.plugin_storage_appbar, - nextRight = FOCUS_SELF, - nextLeft = R.id.nav_rail_view - ) - binding?.repoRecyclerView?.adapter = RepoAdapter(false, { - findNavController().navigate( - R.id.navigation_settings_extensions_to_navigation_settings_plugins, - PluginsFragment.newInstance( - it.name, - it.url, - false - ) + binding?.repoRecyclerView?.apply { + setLinearListLayout( + isHorizontal = false, + nextUp = R.id.settings_toolbar, //FOCUS_SELF, // back has no id so we cant :pensive: + nextDown = R.id.plugin_storage_appbar, + nextRight = FOCUS_SELF, + nextLeft = R.id.nav_rail_view ) - }, { repo -> - // Prompt user before deleting repo - main { - val builder = AlertDialog.Builder(context ?: view.context) - val dialogClickListener = - DialogInterface.OnClickListener { _, which -> - when (which) { - DialogInterface.BUTTON_POSITIVE -> { - ioSafe { - RepositoryManager.removeRepository(view.context, repo) - extensionViewModel.loadStats() - extensionViewModel.loadRepositories() - } - } - DialogInterface.BUTTON_NEGATIVE -> {} - } + if (!isTrueTvSettings()) + binding?.addRepoButton?.let { button -> + button.post { + setPadding( + paddingLeft, + paddingTop, + paddingRight, + button.measuredHeight + button.marginTop + button.marginBottom + ) } + } - builder.setTitle(R.string.delete_repository) - .setMessage( - context?.getString(R.string.delete_repository_plugins) - ) - .setPositiveButton(R.string.delete, dialogClickListener) - .setNegativeButton(R.string.cancel, dialogClickListener) - .show().setDefaultFocus() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> + val dy = scrollY - oldScrollY + if (dy > 0) { //check for scroll down + binding?.addRepoButton?.shrink() // hide + } else if (dy < -5) { + binding?.addRepoButton?.extend() // show + } + } } - }) + adapter = RepoAdapter(false, { + findNavController().navigate( + R.id.navigation_settings_extensions_to_navigation_settings_plugins, + PluginsFragment.newInstance( + it.name, + it.url, + false + ) + ) + }, { repo -> + // Prompt user before deleting repo + main { + val builder = AlertDialog.Builder(context ?: view.context) + val dialogClickListener = + DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + ioSafe { + RepositoryManager.removeRepository(view.context, repo) + extensionViewModel.loadStats() + extensionViewModel.loadRepositories() + } + } + + DialogInterface.BUTTON_NEGATIVE -> {} + } + } + + builder.setTitle(R.string.delete_repository) + .setMessage( + context?.getString(R.string.delete_repository_plugins) + ) + .setPositiveButton(R.string.delete, dialogClickListener) + .setNegativeButton(R.string.cancel, dialogClickListener) + .show().setDefaultFocus() + } + }) + } observe(extensionViewModel.repositories) { binding?.repoRecyclerView?.isVisible = it.isNotEmpty() diff --git a/app/src/main/res/layout/fragment_extensions.xml b/app/src/main/res/layout/fragment_extensions.xml index b3583539..71dd372b 100644 --- a/app/src/main/res/layout/fragment_extensions.xml +++ b/app/src/main/res/layout/fragment_extensions.xml @@ -11,6 +11,7 @@ Date: Tue, 15 Aug 2023 18:37:33 +0000 Subject: [PATCH 168/570] Fix episode removal in simkl (#555) --- .../syncproviders/providers/SimklApi.kt | 75 ++++++++++++------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 64cebfc6..b4a9d789 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -60,8 +60,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { private var lastScoreTime = -1L companion object { - private const val clientId = BuildConfig.SIMKL_CLIENT_ID - private const val clientSecret = BuildConfig.SIMKL_CLIENT_SECRET + private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID + private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET private var lastLoginState = "" const val SIMKL_TOKEN_KEY: String = "simkl_token" @@ -498,6 +498,9 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { val episodes: Array?, override var isFavorite: Boolean? = null, override var maxEpisodes: Int? = null, + /** Save seen episodes separately to know the change from old to new. + * Required to remove seen episodes if count decreases */ + val oldEpisodes: Int, ) : SyncAPI.AbstractSyncStatus() override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { @@ -521,7 +524,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { score = foundItem.user_rating, watchedEpisodes = foundItem.watched_episodes_count, maxEpisodes = foundItem.total_episodes_count, - episodes = episodes + episodes = episodes, + oldEpisodes = foundItem.watched_episodes_count ?: 0, ) } else { return if (searchResult != null) { @@ -530,7 +534,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { score = 0, watchedEpisodes = 0, maxEpisodes = if (searchResult.type == "movie") 0 else null, - episodes = episodes + episodes = episodes, + oldEpisodes = 0, ) } else { null @@ -604,32 +609,46 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { SimklListStatusType.ReWatching.value ).contains(status.status) ) { - val cutEpisodes = simklStatus.episodes.take(watchedEpisodes) - - val (seasons, episodes) = if (cutEpisodes.any { it.season != null }) { - EpisodeMetadata.convertToSeasons(cutEpisodes) to null - } else { - null to EpisodeMetadata.convertToEpisodes(cutEpisodes) + suspend fun postEpisodes( + url: String, + rawEpisodes: List + ): Boolean { + val (seasons, episodes) = if (rawEpisodes.any { it.season != null }) { + EpisodeMetadata.convertToSeasons(rawEpisodes) to null + } else { + null to EpisodeMetadata.convertToEpisodes(rawEpisodes) + } + debugPrint { "Synced history using $url: seasons=${seasons?.toList()}, episodes=${episodes?.toList()}" } + return app.post( + url, + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + seasons, + episodes + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful } - debugPrint { "Synced history for ${status.watchedEpisodes} given size of ${simklStatus.episodes.size}: seasons=${seasons?.toList()}, episodes=${episodes?.toList()}" } - val episodeResponse = app.post( - "$mainUrl/sync/history", - json = StatusRequest( - shows = listOf( - HistoryMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - seasons, - episodes - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ) - episodeResponse.isSuccessful + // If episodes decrease: remove all episodes beyond watched episodes. + val removeResponse = if (simklStatus.oldEpisodes > watchedEpisodes) { + val removeEpisodes = simklStatus.episodes + .drop(watchedEpisodes) + postEpisodes("$mainUrl/sync/history/remove", removeEpisodes) + } else { + true + } + val cutEpisodes = simklStatus.episodes.take(watchedEpisodes) + val addResponse = postEpisodes("$mainUrl/sync/history/", cutEpisodes) + + removeResponse && addResponse } else true val newStatus = From d536dffaf559425bdb7b02232c3bfac8a951133d Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Wed, 16 Aug 2023 19:45:39 +0530 Subject: [PATCH 169/570] Fix Trailers not Working (#559) * Fix Trailers not Working * smol tip --- app/build.gradle.kts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3c12652a..b228fea0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -165,7 +165,7 @@ dependencies { androidTestImplementation("androidx.test:core") //implementation("io.karn:khttp-android:0.1.2") //okhttp instead -// implementation("org.jsoup:jsoup:1.13.1") + // implementation("org.jsoup:jsoup:1.13.1") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") implementation("androidx.preference:preference-ktx:1.2.0") @@ -220,8 +220,8 @@ dependencies { implementation("androidx.work:work-runtime-ktx:2.8.1") // Networking -// implementation("com.squareup.okhttp3:okhttp:4.9.2") -// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") + // implementation("com.squareup.okhttp3:okhttp:4.9.2") + // implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") implementation("com.github.Blatzar:NiceHttp:0.4.3") // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") @@ -243,11 +243,9 @@ dependencies { // used for subtitle decoding https://github.com/albfernandez/juniversalchardet implementation("com.github.albfernandez:juniversalchardet:2.4.0") - // slow af yt - //implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT") - // newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204 - implementation("com.github.TeamNewPipe:NewPipeExtractor:8495ad619e") + // this should be updated frequently to avoid trailer fu*kery + implementation("com.github.TeamNewPipe:NewPipeExtractor:1f08d28") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6") // Library/extensions searching with Levenshtein distance From d247640dcf2a3985390cb0e3d5fd4c8d82e7d4ad Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Wed, 16 Aug 2023 19:48:15 +0530 Subject: [PATCH 170/570] Play n Dowload button fix for NS*W results. (#557) * Play n Dowload button fix for NS*W results. * Revert MainAPI Changes * Tweaked ResultViewModel --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 011d133d..2fe3b012 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 @@ -1707,7 +1707,7 @@ class ResultViewModel2 : ViewModel() { else -> { if (response.type.isLiveStream()) R.string.play_livestream_button - else if (response.type.isMovieType()) // this wont break compatibility as you only need to override isMovieType + else if (response.isMovie()) // this wont break compatibility as you only need to override isMovieType R.string.play_movie_button else null } @@ -2340,4 +2340,4 @@ class ResultViewModel2 : ViewModel() { } } } -} \ No newline at end of file +} From 20da3807a2cb98b60b22e4ad65a96c037c254c58 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 17 Aug 2023 01:00:43 +0200 Subject: [PATCH 171/570] fixed search query for intent --- .../com/lagradost/cloudstream3/MainActivity.kt | 12 ++++++++---- .../cloudstream3/ui/search/SearchFragment.kt | 15 +++++++++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a8160d33..15b16078 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -286,7 +286,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { * * This is a very bad solution but I was unable to find a better one. **/ - private var nextSearchQuery: String? = null + var nextSearchQuery: String? = null /** * Fires every time a new batch of plugins have been loaded, no guarantee about how often this is run and on which thread @@ -362,9 +362,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { loadRepository(url) return true } else if (safeURI(str)?.scheme == appStringSearch) { + val query = str.substringAfter("$appStringSearch://") nextSearchQuery = - URLDecoder.decode(str.substringAfter("$appStringSearch://"), "UTF-8") - + try { + URLDecoder.decode(query, "UTF-8") + } catch (t : Throwable) { + logError(t) + query + } // Use both navigation views to support both layouts. // It might be better to use the QuickSearch. activity?.findViewById(R.id.nav_view)?.selectedItemId = @@ -1315,7 +1320,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (navDestination.matchDestination(R.id.navigation_search) && !nextSearchQuery.isNullOrBlank()) { bundle?.apply { this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery) - nextSearchQuery = null } } } 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 63213eb9..bdf82377 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 @@ -84,7 +84,7 @@ class SearchFragment : Fragment() { fun newInstance(query: String): Bundle { return Bundle().apply { - putString(SEARCH_QUERY, query) + if(query.isNotBlank()) putString(SEARCH_QUERY, query) } } } @@ -211,7 +211,7 @@ class SearchFragment : Fragment() { reloadRepos() binding?.apply { - val adapter: RecyclerView.Adapter? = + val adapter: RecyclerView.Adapter = SearchAdapter( ArrayList(), searchAutofitResults, @@ -530,11 +530,18 @@ class SearchFragment : Fragment() { searchMasterRecycler.layoutManager = GridLayoutManager(context, 1) // Automatically search the specified query, this allows the app search to launch from intent - arguments?.getString(SEARCH_QUERY)?.let { query -> + var sq = arguments?.getString(SEARCH_QUERY) ?: savedInstanceState?.getString(SEARCH_QUERY) + if(sq.isNullOrBlank()) { + sq = MainActivity.nextSearchQuery + } + + sq?.let { query -> if (query.isBlank()) return@let mainSearch.setQuery(query, true) // Clear the query as to not make it request the same query every time the page is opened - arguments?.putString(SEARCH_QUERY, null) + arguments?.remove(SEARCH_QUERY) + savedInstanceState?.remove(SEARCH_QUERY) + MainActivity.nextSearchQuery = null } } From c2b951a07866077e05d15e385111d2d74fe7e542 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 17 Aug 2023 01:19:24 +0200 Subject: [PATCH 172/570] fixed #560 lock locks orientation --- .../ui/player/FullScreenPlayer.kt | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) 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 9739b627..0f3c189d 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 @@ -2,9 +2,11 @@ package com.lagradost.cloudstream3.ui.player import android.animation.ObjectAnimator import android.annotation.SuppressLint +import android.app.Activity import android.content.Context import android.content.pm.ActivityInfo import android.content.res.ColorStateList +import android.content.res.Configuration import android.content.res.Resources import android.graphics.Color import android.media.AudioManager @@ -16,6 +18,7 @@ import android.util.DisplayMetrics import android.view.KeyEvent import android.view.LayoutInflater import android.view.MotionEvent +import android.view.Surface import android.view.View import android.view.ViewGroup import android.view.WindowManager @@ -56,6 +59,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.Vector2 import kotlin.math.* + const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking const val MINIMUM_VERTICAL_SWIPE = 2.0f // in percentage const val MINIMUM_HORIZONTAL_SWIPE = 2.0f // in percentage @@ -292,6 +296,36 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.getCurrentPreferredSubtitle() == null } + open fun lockOrientation(activity: Activity) { + val display = + (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + val rotation = display.rotation + val currentOrientation = activity.resources.configuration.orientation + var orientation = 0 + when (currentOrientation) { + Configuration.ORIENTATION_LANDSCAPE -> orientation = + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE + + Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + //Configuration.ORIENTATION_PORTRAIT -> orientation = + // if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT + } + activity.requestedOrientation = orientation + } + + private fun updateOrientation() { + activity?.apply { + if(lockRotation) { + if(isLocked) { + lockOrientation(this) + } + else { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + } + } + } + } + protected fun enterFullscreen() { if (isFullScreenPlayer) { activity?.hideSystemUI() @@ -301,8 +335,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { activity?.window?.attributes = params } } - if (lockRotation) - activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + updateOrientation() } protected fun exitFullscreen() { @@ -561,6 +594,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } isLocked = !isLocked + updateOrientation() + if (isLocked && isShowing) { playerBinding?.playerHolder?.postDelayed({ if (isLocked && isShowing) { From 590c74111cb945366ebd6a11278f2f9f827043c5 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 17 Aug 2023 23:10:21 +0200 Subject: [PATCH 173/570] fuck it we ball, m3u8 download is now fixed --- .../lagradost/cloudstream3/extractors/Cda.kt | 10 +- .../cloudstream3/utils/M3u8Helper.kt | 282 +++++++++--------- .../utils/VideoDownloadManager.kt | 221 ++++++-------- 3 files changed, 237 insertions(+), 276 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt index 6a2f399d..42f6eddb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt @@ -1,13 +1,11 @@ package com.lagradost.cloudstream3.extractors -import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.USER_AGENT +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 -import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import android.util.Log +import com.lagradost.cloudstream3.utils.Qualities import java.net.URLDecoder open class Cda: ExtractorApi() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 6c5117b4..6770e303 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -1,17 +1,16 @@ package com.lagradost.cloudstream3.utils +import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.mvvm.logError -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.delay import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec import kotlin.math.pow - +/** backwards api surface */ class M3u8Helper { companion object { - private val generator = M3u8Helper() suspend fun generateM3u8( source: String, streamUrl: String, @@ -20,34 +19,59 @@ class M3u8Helper { headers: Map = mapOf(), name: String = source ): List { - return generator.m3u8Generation( - M3u8Stream( - streamUrl = streamUrl, - quality = quality, - headers = headers, - ), null - ) - .map { stream -> - ExtractorLink( - source, - name = name, - stream.streamUrl, - referer, - stream.quality ?: Qualities.Unknown.value, - true, - stream.headers, - ) - } + return M3u8Helper2.generateM3u8(source, streamUrl, referer, quality, headers, name) } } + + data class M3u8Stream( + val streamUrl: String, + val quality: Int? = null, + val headers: Map = mapOf() + ) + + suspend fun m3u8Generation(m3u8: M3u8Stream, returnThis: Boolean? = true): List { + return M3u8Helper2.m3u8Generation(m3u8, returnThis) + } +} + +object M3u8Helper2 { + suspend fun generateM3u8( + source: String, + streamUrl: String, + referer: String, + quality: Int? = null, + headers: Map = mapOf(), + name: String = source + ): List { + return m3u8Generation( + M3u8Helper.M3u8Stream( + streamUrl = streamUrl, + quality = quality, + headers = headers, + ), null + ) + .map { stream -> + ExtractorLink( + source, + name = name, + stream.streamUrl, + referer, + stream.quality ?: Qualities.Unknown.value, + true, + stream.headers, + ) + } + } + private val ENCRYPTION_DETECTION_REGEX = Regex("#EXT-X-KEY:METHOD=([^,]+),") private val ENCRYPTION_URL_IV_REGEX = Regex("#EXT-X-KEY:METHOD=([^,]+),URI=\"([^\"]+)\"(?:,IV=(.*))?") private val QUALITY_REGEX = Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""") private val TS_EXTENSION_REGEX = - Regex("""(.*\.ts.*|.*\.jpg.*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts + Regex("""#EXTINF:.*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways + //Regex("""(.*\.(ts|jpg|html).*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts private fun absoluteExtensionDetermination(url: String): String? { val split = url.split("/") @@ -73,7 +97,7 @@ class M3u8Helper { } }.iterator() - private fun getDecrypter( + fun getDecrypter( secretKey: ByteArray, data: ByteArray, iv: ByteArray = "".toByteArray() @@ -91,13 +115,8 @@ class M3u8Helper { return st != null && (st.value.isNotEmpty() || st.destructured.component1() != "NONE") } - data class M3u8Stream( - val streamUrl: String, - val quality: Int? = null, - val headers: Map = mapOf() - ) - private fun selectBest(qualities: List): M3u8Stream? { + private fun selectBest(qualities: List): M3u8Helper.M3u8Stream? { val result = qualities.sortedBy { if (it.quality != null && it.quality <= 1080) it.quality else 0 }.filter { @@ -113,19 +132,16 @@ class M3u8Helper { } private fun isNotCompleteUrl(url: String): Boolean { - return !url.contains("https://") && !url.contains("http://") + return !url.startsWith("https://") && !url.startsWith("http://") } - suspend fun m3u8Generation(m3u8: M3u8Stream, returnThis: Boolean? = true): List { -// return listOf(m3u8) - val list = mutableListOf() + suspend fun m3u8Generation(m3u8: M3u8Helper.M3u8Stream, returnThis: Boolean? = true): List { + val list = mutableListOf() val m3u8Parent = getParentLink(m3u8.streamUrl) val response = app.get(m3u8.streamUrl, headers = m3u8.headers, verify = false).text -// var hasAnyContent = false for (match in QUALITY_REGEX.findAll(response)) { -// hasAnyContent = true var (quality, m3u8Link, m3u8Link2) = match.destructured if (m3u8Link.isEmpty()) m3u8Link = m3u8Link2 if (absoluteExtensionDetermination(m3u8Link) == "m3u8") { @@ -136,21 +152,21 @@ class M3u8Helper { println(m3u8.streamUrl) } list += m3u8Generation( - M3u8Stream( + M3u8Helper.M3u8Stream( m3u8Link, quality.toIntOrNull(), m3u8.headers ), false ) } - list += M3u8Stream( + list += M3u8Helper.M3u8Stream( m3u8Link, quality.toIntOrNull(), m3u8.headers ) } if (returnThis != false) { - list += M3u8Stream( + list += M3u8Helper.M3u8Stream( m3u8.streamUrl, Qualities.Unknown.value, m3u8.headers @@ -160,113 +176,111 @@ class M3u8Helper { return list } + data class LazyHlsDownloadData( + private val encryptionData: ByteArray, + private val encryptionIv: ByteArray, + private val isEncrypted: Boolean, + private val allTsLinks: List, + private val relativeUrl: String, + private val headers: Map, + ) { + val size get() = allTsLinks.size - data class HlsDownloadData( - val bytes: ByteArray, - val currentIndex: Int, - val totalTs: Int, - val errored: Boolean = false - ) - - suspend fun hlsYield( - qualities: List, - startIndex: Int = 0 - ): Iterator { - if (qualities.isEmpty()) return listOf( - HlsDownloadData( - byteArrayOf(), - 1, - 1, - true - ) - ).iterator() - - var selected = selectBest(qualities) - if (selected == null) { - selected = qualities[0] + suspend fun resolveLinkSafe( + index: Int, + tries: Int = 3, + failDelay: Long = 3000 + ): ByteArray? { + for (i in 0 until tries) { + try { + return resolveLink(index) + } catch (e: IllegalArgumentException) { + return null + } catch (t: Throwable) { + delay(failDelay) + } + } + return null } + + @Throws + suspend fun resolveLink(index: Int): ByteArray { + if (index < 0 || index >= size) throw IllegalArgumentException("index must be in the bounds of the ts") + val url = allTsLinks[index] + + val tsResponse = app.get(url, headers = headers, verify = false) + val tsData = tsResponse.body.bytes() + if (tsData.isEmpty()) throw ErrorLoadingException("no data") + + return if (isEncrypted) { + getDecrypter(encryptionData, tsData, encryptionIv) + } else { + tsData + } + } + } + + @Throws + suspend fun hslLazy( + qualities: List + ): LazyHlsDownloadData { + if (qualities.isEmpty()) throw IllegalArgumentException("qualities must be non empty") + val selected = selectBest(qualities) ?: qualities.first() val headers = selected.headers - val streams = qualities.map { m3u8Generation(it, false) }.flatten() - //val sslVerification = if (headers.containsKey("ssl_verification")) headers["ssl_verification"].toBoolean() else true - + // this selects the best quality of the qualities offered, + // due to the recursive nature of m3u8, we only go 2 depth val secondSelection = selectBest(streams.ifEmpty { listOf(selected) }) - if (secondSelection != null) { - val m3u8Response = - runBlocking { - app.get( - secondSelection.streamUrl, - headers = headers, - verify = false - ).text - } + ?: throw IllegalArgumentException("qualities has no streams") - var encryptionUri: String? - var encryptionIv = byteArrayOf() - var encryptionData = byteArrayOf() + val m3u8Response = + app.get( + secondSelection.streamUrl, + headers = headers, + verify = false + ).text - val encryptionState = isEncrypted(m3u8Response) + println("m3u8Response=$m3u8Response") - if (encryptionState) { - val match = - ENCRYPTION_URL_IV_REGEX.find(m3u8Response)!!.destructured // its safe to assume that its not going to be null - encryptionUri = match.component2() + // encryption, this is because crunchy uses it + var encryptionIv = byteArrayOf() + var encryptionData = byteArrayOf() - if (isNotCompleteUrl(encryptionUri)) { - encryptionUri = "${getParentLink(secondSelection.streamUrl)}/$encryptionUri" - } + val encryptionState = isEncrypted(m3u8Response) - encryptionIv = match.component3().toByteArray() - val encryptionKeyResponse = - runBlocking { app.get(encryptionUri, headers = headers, verify = false) } - encryptionData = encryptionKeyResponse.body?.bytes() ?: byteArrayOf() + if (encryptionState) { + // its safe to assume that its not going to be null + val match = + ENCRYPTION_URL_IV_REGEX.find(m3u8Response)!!.groupValues + + var encryptionUri = match[1] + + if (isNotCompleteUrl(encryptionUri)) { + encryptionUri = "${getParentLink(secondSelection.streamUrl)}/$encryptionUri" } - val allTs = TS_EXTENSION_REGEX.findAll(m3u8Response) - val allTsList = allTs.toList() - val totalTs = allTsList.size - if (totalTs == 0) { - return listOf(HlsDownloadData(byteArrayOf(), 1, 1, true)).iterator() - } - var lastYield = 0 - - val relativeUrl = getParentLink(secondSelection.streamUrl) - var retries = 0 - val tsByteGen = sequence { - loop@ for ((index, ts) in allTs.withIndex()) { - val url = if ( - isNotCompleteUrl(ts.destructured.component1()) - ) "$relativeUrl/${ts.destructured.component1()}" else ts.destructured.component1() - val c = index + 1 + startIndex - - while (lastYield != c) { - try { - val tsResponse = - runBlocking { app.get(url, headers = headers, verify = false) } - var tsData = tsResponse.body?.bytes() ?: byteArrayOf() - - if (encryptionState) { - tsData = getDecrypter(encryptionData, tsData, encryptionIv) - yield(HlsDownloadData(tsData, c, totalTs)) - lastYield = c - break - } - yield(HlsDownloadData(tsData, c, totalTs)) - lastYield = c - } catch (e: Exception) { - logError(e) - if (retries == 3) { - yield(HlsDownloadData(byteArrayOf(), c, totalTs, true)) - break@loop - } - ++retries - Thread.sleep(2_000) - } - } - } - } - return tsByteGen.iterator() + encryptionIv = match[2].toByteArray() + val encryptionKeyResponse = app.get(encryptionUri, headers = headers, verify = false) + encryptionData = encryptionKeyResponse.body.bytes() } - return listOf(HlsDownloadData(byteArrayOf(), 1, 1, true)).iterator() + val relativeUrl = getParentLink(secondSelection.streamUrl) + val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts -> + val value = ts.groupValues[1] + if (isNotCompleteUrl(value)) { + "$relativeUrl/${value}" + } else { + value + } + }.toList() + if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty") + + return LazyHlsDownloadData( + encryptionData = encryptionData, + encryptionIv = encryptionIv, + isEncrypted = encryptionState, + allTsLinks = allTsList, + relativeUrl = relativeUrl, + headers = headers + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index c138ea75..f4eb37b7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -15,7 +15,6 @@ import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData import androidx.preference.PreferenceManager import androidx.work.Data @@ -32,18 +31,15 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly import java.io.BufferedInputStream @@ -51,11 +47,9 @@ import java.io.File import java.io.InputStream import java.io.OutputStream import java.lang.Thread.sleep -import java.net.URI import java.net.URL import java.net.URLConnection import java.util.* -import kotlin.math.roundToInt const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general" const val DOWNLOAD_CHANNEL_NAME = "Downloads" @@ -92,7 +86,7 @@ object VideoDownloadManager { @DrawableRes const val pressToStopIcon = R.drawable.exo_icon_stop - private var updateCount : Int = 0 + private var updateCount: Int = 0 private val downloadDataUpdateCount = MutableLiveData() enum class DownloadType { @@ -687,7 +681,8 @@ object VideoDownloadManager { return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream) } - fun downloadThing( + @Throws + suspend fun downloadThing( context: Context, link: IDownloadableMinimum, name: String, @@ -696,9 +691,9 @@ object VideoDownloadManager { tryResume: Boolean, parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, - ): Int { + ): Int = withContext(Dispatchers.IO) { if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { - return ERROR_UNKNOWN + return@withContext ERROR_UNKNOWN } val basePath = context.getBasePath() @@ -714,7 +709,7 @@ object VideoDownloadManager { } val stream = setupStream(context, name, relativePath, extension, tryResume) - if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode + if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode val resume = stream.resume!! val fileStream = stream.fileStream!! @@ -766,7 +761,7 @@ object VideoDownloadManager { } val bytesTotal = contentLength + resumeLength - if (extension == "mp4" && bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG + if (extension == "mp4" && bytesTotal < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG parentId?.let { setKey( @@ -845,11 +840,13 @@ object VideoDownloadManager { DownloadActionType.Pause -> { isPaused = true; updateNotification() } + DownloadActionType.Stop -> { isStopped = true; updateNotification() removeKey(KEY_RESUME_PACKAGES, event.first.toString()) saveQueue() } + DownloadActionType.Resume -> { isPaused = false; updateNotification() } @@ -917,15 +914,17 @@ object VideoDownloadManager { } // RETURN MESSAGE - return when { + return@withContext when { isFailed -> { parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } ERROR_CONNECTION_ERROR } + isStopped -> { parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } deleteFile() } + else -> { parentId?.let { id -> downloadProgressEvent.invoke( @@ -989,6 +988,7 @@ object VideoDownloadManager { found.delete() this.createDirectory(directoryName) } + this.isDirectory -> this.createDirectory(directoryName) else -> this.parentFile?.createDirectory(directoryName) } @@ -1107,7 +1107,8 @@ object VideoDownloadManager { return SUCCESS_STOPPED } - private fun downloadHLS( + @Throws + private suspend fun downloadHLS( context: Context, link: ExtractorLink, name: String, @@ -1115,16 +1116,8 @@ object VideoDownloadManager { parentId: Int?, startIndex: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit - ): Int { + ): Int = withContext(Dispatchers.IO) { val extension = "mp4" - fun logcatPrint(vararg items: Any?) { - items.forEach { - println("[HLS]: $it") - } - } - - val m3u8Helper = M3u8Helper() - logcatPrint("initialised the HLS downloader.") val m3u8 = M3u8Helper.M3u8Stream( link.url, link.quality, mapOf("referer" to link.referer) @@ -1139,54 +1132,40 @@ object VideoDownloadManager { ) else folder val stream = setupStream(context, name, relativePath, extension, realIndex > 0) - if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode + if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - if (!stream.resume!!) realIndex = 0 - val fileLengthAdd = stream.fileLength!! - val tsIterator = runBlocking { - m3u8Helper.hlsYield(listOf(m3u8), realIndex) - } + if (stream.resume != true) realIndex = 0 + val fileLengthAdd = stream.fileLength ?: 0 + val items = M3u8Helper2.hslLazy(listOf(m3u8)) val displayName = getDisplayName(name, extension) val fileStream = stream.fileStream!! - val firstTs = tsIterator.next() - var isDone = false var isFailed = false var isPaused = false - var bytesDownloaded = firstTs.bytes.size.toLong() + fileLengthAdd - var tsProgress = 1L + realIndex - val totalTs = firstTs.totalTs.toLong() + var bytesDownloaded = fileLengthAdd + var tsProgress: Long = realIndex.toLong() + 1 // we don't want div by zero + val totalTs: Long = items.size.toLong() fun deleteFile(): Int { return delete(context, name, relativePath, extension, parentId, basePath.first) } - /* - Most of the auto generated m3u8 out there have TS of the same size. - And only the last TS might have a different size. - - But oh well, in cases of handmade m3u8 streams this will go all over the place ¯\_(ツ)_/¯ - So ya, this calculates an estimate of how many bytes the file is going to be. - - > (bytesDownloaded/tsProgress)*totalTs - */ fun updateInfo() { - parentId?.let { - setKey( - KEY_DOWNLOAD_INFO, - it.toString(), - DownloadedFileInfo( - (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - relativePath ?: "", - displayName, - tsProgress.toString(), - basePath = basePath.second - ) + setKey( + KEY_DOWNLOAD_INFO, + (parentId ?: return).toString(), + DownloadedFileInfo( + // approx bytes + totalBytes = (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), + relativePath = relativePath ?: "", + displayName = displayName, + extraInfo = tsProgress.toString(), + basePath = basePath.second ) - } + ) } updateInfo() @@ -1210,9 +1189,7 @@ object VideoDownloadManager { (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), ) ) - } catch (e: Exception) { - // IDK MIGHT ERROR - } + } catch (_: Throwable) {} } createNotificationCallback.invoke( @@ -1226,24 +1203,6 @@ object VideoDownloadManager { ) } - fun stopIfError(ts: M3u8Helper.HlsDownloadData): Int? { - if (ts.errored || ts.bytes.isEmpty()) { - val error: Int = if (!ts.errored) { - logcatPrint("Error: No stream was found.") - ERROR_UNKNOWN - } else { - logcatPrint("Error: Failed to fetch data.") - ERROR_CONNECTION_ERROR - } - isFailed = true - fileStream.close() - deleteFile() - updateNotification() - return error - } - return null - } - val notificationCoroutine = main { while (true) { if (!isDone) { @@ -1261,11 +1220,11 @@ object VideoDownloadManager { DownloadActionType.Stop -> { isFailed = true } + DownloadActionType.Pause -> { - isPaused = - true // Pausing is not supported since well...I need to know the index of the ts it was paused at - // it may be possible to store it in a variable, but when the app restarts it will be lost + isPaused = true } + DownloadActionType.Resume -> { isPaused = false } @@ -1278,32 +1237,22 @@ object VideoDownloadManager { try { if (parentId != null) downloadEvent -= downloadEventListener - } catch (e: Exception) { - logError(e) + } catch (t: Throwable) { + logError(t) } try { parentId?.let { downloadStatus.remove(it) } - } catch (e: Exception) { - logError(e) - // IDK MIGHT ERROR + } catch (t: Throwable) { + logError(t) } notificationCoroutine.cancel() } - stopIfError(firstTs).let { - if (it != null) { - closeAll() - return it - } - } - if (parentId != null) downloadEvent += downloadEventListener - fileStream.write(firstTs.bytes) - fun onFailed() { fileStream.close() deleteFile() @@ -1311,31 +1260,29 @@ object VideoDownloadManager { closeAll() } - for (ts in tsIterator) { + for (idx in realIndex until items.size) { while (isPaused) { if (isFailed) { onFailed() - return SUCCESS_STOPPED + return@withContext SUCCESS_STOPPED } - sleep(100) + delay(100) } if (isFailed) { onFailed() - return SUCCESS_STOPPED + return@withContext SUCCESS_STOPPED } - stopIfError(ts).let { - if (it != null) { - closeAll() - return it - } + val bytes = items.resolveLinkSafe(idx) ?: run { + isFailed = true + onFailed() + return@withContext ERROR_CONNECTION_ERROR } - fileStream.write(ts.bytes) - tsProgress = ts.currentIndex.toLong() - bytesDownloaded += ts.bytes.size.toLong() - logcatPrint("Download progress ${((tsProgress.toFloat() / totalTs.toFloat()) * 100).roundToInt()}%") + fileStream.write(bytes) + tsProgress = idx.toLong() + 1 + bytesDownloaded += bytes.size.toLong() updateInfo() } isDone = true @@ -1344,7 +1291,7 @@ object VideoDownloadManager { closeAll() updateInfo() - return SUCCESS_DOWNLOAD_DONE + return@withContext SUCCESS_DOWNLOAD_DONE } fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { @@ -1379,7 +1326,7 @@ object VideoDownloadManager { ) } - private fun downloadSingleEpisode( + private suspend fun downloadSingleEpisode( context: Context, source: String?, folder: String?, @@ -1405,25 +1352,29 @@ object VideoDownloadManager { null )?.extraInfo?.toIntOrNull() } else null - return downloadHLS(context, link, name, folder, ep.id, startIndex) { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback, - meta.hlsProgress, - meta.hlsTotal - ) + return suspendSafeApiCall { + downloadHLS(context, link, name, folder, ep.id, startIndex) { meta -> + main { + createNotification( + context, + source, + link.name, + ep, + meta.type, + meta.bytesDownloaded, + meta.bytesTotal, + notificationCallback, + meta.hlsProgress, + meta.hlsTotal + ) + } } - }.also { extractorJob.cancel() } + }.also { + extractorJob.cancel() + } ?: ERROR_UNKNOWN } - return normalSafeApiCall { + return suspendSafeApiCall { downloadThing(context, link, name, folder, "mp4", tryResume, ep.id) { meta -> main { createNotification( @@ -1468,17 +1419,15 @@ object VideoDownloadManager { DownloadResumePackage(item, index) ) val connectionResult = withContext(Dispatchers.IO) { - normalSafeApiCall { - downloadSingleEpisode( - context, - item.source, - item.folder, - item.ep, - link, - notificationCallback, - resume - ).also { println("Single episode finished with return code: $it") } - } + downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + resume + ).also { println("Single episode finished with return code: $it") } } if (connectionResult != null && connectionResult > 0) { // SUCCESS removeKey(KEY_RESUME_PACKAGES, id.toString()) From 61d63b17d819d7899e04314293d8f01b651ef22a Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Fri, 18 Aug 2023 02:41:59 +0530 Subject: [PATCH 174/570] chore: acra improvements and media3 bump (#562) * Acra Bump * Media3 bump --- app/build.gradle.kts | 20 +++++++++---------- .../lagradost/cloudstream3/AcraApplication.kt | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b228fea0..3b215dbc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -181,22 +181,22 @@ dependencies { // implementation("androidx.leanback:leanback-paging:1.1.0-alpha09") // Media 3 - implementation("androidx.media3:media3-common:1.1.0") - implementation("androidx.media3:media3-exoplayer:1.1.0") - implementation("androidx.media3:media3-datasource-okhttp:1.1.0") - implementation("androidx.media3:media3-ui:1.1.0") - implementation("androidx.media3:media3-session:1.1.0") - implementation("androidx.media3:media3-cast:1.1.0") - implementation("androidx.media3:media3-exoplayer-hls:1.1.0") - implementation("androidx.media3:media3-exoplayer-dash:1.1.0") + implementation("androidx.media3:media3-common:1.1.1") + implementation("androidx.media3:media3-exoplayer:1.1.1") + implementation("androidx.media3:media3-datasource-okhttp:1.1.1") + implementation("androidx.media3:media3-ui:1.1.1") + implementation("androidx.media3:media3-session:1.1.1") + implementation("androidx.media3:media3-cast:1.1.1") + implementation("androidx.media3:media3-exoplayer-hls:1.1.1") + implementation("androidx.media3:media3-exoplayer-dash:1.1.1") // Custom ffmpeg extension for audio codecs implementation("com.github.recloudstream:media-ffmpeg:1.1.0") //implementation("com.google.android.exoplayer:extension-leanback:2.14.0") // Bug reports - implementation("ch.acra:acra-core:5.8.4") - implementation("ch.acra:acra-toast:5.8.4") + implementation("ch.acra:acra-core:5.11.0") + implementation("ch.acra:acra-toast:5.11.0") compileOnly("com.google.auto.service:auto-service-annotations:1.0") //either for java sources: diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 069287b0..61d467c4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -121,10 +121,10 @@ class AcraApplication : Application() { buildConfigClass = BuildConfig::class.java reportFormat = StringFormat.JSON - reportContent = arrayOf( + reportContent = listOf( ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE, ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL, - ReportField.STACK_TRACE + ReportField.STACK_TRACE, ) // removed this due to bug when starting the app, moved it to when it actually crashes @@ -213,4 +213,4 @@ class AcraApplication : Application() { } } -} \ No newline at end of file +} From 8f6e8a8e99c349489f05294e2d46a9ce58afc1ae Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 18 Aug 2023 01:46:29 +0200 Subject: [PATCH 175/570] fixed #547 fuck inheritance --- app/build.gradle.kts | 2 +- .../ui/result/ResultFragmentPhone.kt | 53 ++++++++++++------- .../ui/result/ResultTrailerPlayer.kt | 3 -- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3b215dbc..f72ec0b0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -231,7 +231,7 @@ dependencies { // API because cba maintaining it myself implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") - implementation("com.github.discord:OverlappingPanels:0.1.3") + implementation("com.github.discord:OverlappingPanels:0.1.5") // debugImplementation because LeakCanary should only run in debug builds. //debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") 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 633ee762..ae0b7419 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 @@ -23,7 +23,6 @@ import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.RecyclerView import com.discord.panels.OverlappingPanelsLayout import com.discord.panels.PanelsChildGestureRegionObserver import com.google.android.gms.cast.framework.CastButtonFactory @@ -62,8 +61,6 @@ import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable -import com.lagradost.cloudstream3.utils.AppUtils.isLtr -import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.openBrowser import com.lagradost.cloudstream3.utils.ExtractorLink @@ -81,8 +78,13 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.VideoDownloadHelper -open class ResultFragmentPhone : FullScreenPlayer(), - PanelsChildGestureRegionObserver.GestureRegionsListener { +open class ResultFragmentPhone : FullScreenPlayer() { + private val gestureRegionsListener = object : PanelsChildGestureRegionObserver.GestureRegionsListener { + override fun onGestureRegionsUpdate(gestureRegions: List) { + binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) + } + } + protected lateinit var viewModel: ResultViewModel2 protected lateinit var syncModel: SyncViewModel @@ -210,15 +212,20 @@ open class ResultFragmentPhone : FullScreenPlayer(), loadTrailer() } + override fun onDestroy() { + super.onDestroy() + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { + unregister(it) + } + removeGestureRegionsUpdateListener(gestureRegionsListener) + } + } + override fun onDestroyView() { //somehow this still leaks and I dont know why???? // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt - PanelsChildGestureRegionObserver.Provider.get().let { obs -> - resultBinding?.resultCastItems?.let { - obs.unregister(it) - } - obs.removeGestureRegionsUpdateListener(this) - } + updateUIEvent -= ::updateUI binding = null resultBinding = null @@ -287,6 +294,8 @@ open class ResultFragmentPhone : FullScreenPlayer(), it.colorFromAttribute(R.attr.primaryBlackBackground) } super.onResume() + PanelsChildGestureRegionObserver.Provider.get() + .addGestureRegionsUpdateListener(gestureRegionsListener) } override fun onStop() { @@ -323,7 +332,16 @@ open class ResultFragmentPhone : FullScreenPlayer(), setUrl(storedData.url) syncModel.addFromUrl(storedData.url) val api = APIHolder.getApiFromNameNull(storedData.apiName) - PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(this) + + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { + register(it) + } + addGestureRegionsUpdateListener(gestureRegionsListener) + } + + + // ===== ===== ===== resultBinding?.apply { @@ -374,9 +392,8 @@ open class ResultFragmentPhone : FullScreenPlayer(), DownloadButtonSetup.handleDownloadClick(downloadClickEvent) } ) - resultCastItems.let { - PanelsChildGestureRegionObserver.Provider.get().register(it) - } + + resultScroll.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> val dy = scrollY - oldScrollY if (dy > 0) { //check for scroll down @@ -1055,11 +1072,7 @@ open class ResultFragmentPhone : FullScreenPlayer(), override fun onPause() { super.onPause() - PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(this) - } - - override fun onGestureRegionsUpdate(gestureRegions: List) { - binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) + PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(gestureRegionsListener) } private fun setRecommendations(rec: List?, validApiName: String?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index 1f663e31..eb8cb9b3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -118,9 +118,6 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { override fun onTracksInfoChanged() {} override fun exitedPipMode() {} - - override fun onGestureRegionsUpdate(gestureRegions: List) {} - private fun updateFullscreen(fullscreen: Boolean) { isFullScreenPlayer = fullscreen lockRotation = fullscreen From e95dc1db2a94a4f3f5583bc626e331129be77a38 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Fri, 18 Aug 2023 21:16:03 +0530 Subject: [PATCH 176/570] fix: cast items recycler (finally) (#564) * turn cast items visible(tools) * prevent cast gesture listener from permanent RIP in one lifecycle --- .../ui/result/ResultFragmentPhone.kt | 20 +++++++++---------- app/src/main/res/layout/fragment_result.xml | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) 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 ae0b7419..a932a57c 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 @@ -212,19 +212,17 @@ open class ResultFragmentPhone : FullScreenPlayer() { loadTrailer() } - override fun onDestroy() { - super.onDestroy() - PanelsChildGestureRegionObserver.Provider.get().apply { - resultBinding?.resultCastItems?.let { - unregister(it) - } - removeGestureRegionsUpdateListener(gestureRegionsListener) - } - } - override fun onDestroyView() { + //somehow this still leaks and I dont know why???? // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt + PanelsChildGestureRegionObserver.Provider.get().let { obs -> + resultBinding?.resultCastItems?.let { + obs.unregister(it) + } + + obs.removeGestureRegionsUpdateListener(gestureRegionsListener) + } updateUIEvent -= ::updateUI binding = null @@ -1127,4 +1125,4 @@ open class ResultFragmentPhone : FullScreenPlayer() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index ee3477b0..87de7186 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -476,7 +476,7 @@ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:itemCount="2" tools:listitem="@layout/cast_item" - tools:visibility="gone" /> + tools:visibility="visible" /> --> - \ No newline at end of file + From 56cb3d718188bf95e16cc062280bc5a16e42ea24 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 00:48:00 +0200 Subject: [PATCH 177/570] refactored download system for better preference + bugfixes --- .../cloudstream3/DownloaderTestImpl.kt | 2 +- .../com/lagradost/cloudstream3/MainAPI.kt | 2 +- .../ui/download/DownloadChildAdapter.kt | 2 +- .../ui/download/EasyDownloadButton.kt | 264 ----- .../ui/download/button/BaseFetchButton.kt | 61 +- .../ui/download/button/DownloadButton.kt | 17 +- .../ui/download/button/PieFetchButton.kt | 53 +- .../cloudstream3/utils/M3u8Helper.kt | 23 +- .../cloudstream3/utils/VideoDownloadHelper.kt | 6 +- .../utils/VideoDownloadManager.kt | 1016 ++++++++--------- .../main/res/drawable/baseline_stop_24.xml | 10 + 11 files changed, 604 insertions(+), 852 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt create mode 100644 app/src/main/res/drawable/baseline_stop_24.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt index 379a91e4..0a2db2bd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt @@ -50,7 +50,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do companion object { private const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" private var instance: DownloaderTestImpl? = null /** diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 7790f047..80332445 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -29,7 +29,7 @@ import java.util.* import kotlin.math.absoluteValue const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" //val baseHeader = mapOf("User-Agent" to USER_AGENT) val mapper = JsonMapper.builder().addModule(KotlinModule()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt index b4774cf8..1d7b5a83 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt @@ -23,7 +23,7 @@ data class VisualDownloadChildCached( val data: VideoDownloadHelper.DownloadEpisodeCached, ) -data class DownloadClickEvent(val action: Int, val data: EasyDownloadButton.IMinimumData) +data class DownloadClickEvent(val action: Int, val data: VideoDownloadHelper.DownloadEpisodeCached) class DownloadChildAdapter( var cardList: List, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt deleted file mode 100644 index 77878432..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt +++ /dev/null @@ -1,264 +0,0 @@ -package com.lagradost.cloudstream3.ui.download - -import android.animation.ObjectAnimator -import android.text.format.Formatter.formatShortFileSize -import android.view.View -import android.view.animation.DecelerateInterpolator -import android.widget.ImageView -import android.widget.TextView -import androidx.core.view.isGone -import androidx.core.view.isVisible -import androidx.core.widget.ContentLoadingProgressBar -import com.google.android.material.button.MaterialButton -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.Coroutines -import com.lagradost.cloudstream3.utils.IDisposable -import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons -import com.lagradost.cloudstream3.utils.VideoDownloadManager - -class EasyDownloadButton : IDisposable { - interface IMinimumData { - val id: Int - } - - private var _clickCallback: ((DownloadClickEvent) -> Unit)? = null - private var _imageChangeCallback: ((Pair) -> Unit)? = null - - override fun dispose() { - try { - _clickCallback = null - _imageChangeCallback = null - downloadProgressEventListener?.let { VideoDownloadManager.downloadProgressEvent -= it } - downloadStatusEventListener?.let { VideoDownloadManager.downloadStatusEvent -= it } - } catch (e: Exception) { - e.printStackTrace() - } - } - - private var downloadProgressEventListener: ((Triple) -> Unit)? = null - private var downloadStatusEventListener: ((Pair) -> Unit)? = - null - - fun setUpMaterialButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - downloadButton: MaterialButton, - textView: TextView?, - data: IMinimumData, - clickCallback: (DownloadClickEvent) -> Unit, - ) { - setUpDownloadButton( - setupCurrentBytes, - setupTotalBytes, - progressBar, - textView, - data, - downloadButton, - { - downloadButton.setIconResource(it.first) - downloadButton.text = it.second - }, - clickCallback - ) - } - - fun setUpMoreButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - downloadImage: ImageView, - textView: TextView?, - textViewProgress: TextView?, - clickableView: View, - isTextPercentage: Boolean, - data: IMinimumData, - clickCallback: (DownloadClickEvent) -> Unit, - ) { - setUpDownloadButton( - setupCurrentBytes, - setupTotalBytes, - progressBar, - textViewProgress, - data, - clickableView, - { (image, text) -> - downloadImage.isVisible = textViewProgress?.isGone ?: true - downloadImage.setImageResource(image) - textView?.text = text - }, - clickCallback, isTextPercentage - ) - } - - fun setUpButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - downloadImage: ImageView, - textView: TextView?, - data: IMinimumData, - clickCallback: (DownloadClickEvent) -> Unit, - ) { - setUpDownloadButton( - setupCurrentBytes, - setupTotalBytes, - progressBar, - textView, - data, - downloadImage, - { - downloadImage.setImageResource(it.first) - }, - clickCallback - ) - } - - private fun setUpDownloadButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - textView: TextView?, - data: IMinimumData, - downloadView: View, - downloadImageChangeCallback: (Pair) -> Unit, - clickCallback: (DownloadClickEvent) -> Unit, - isTextPercentage: Boolean = false - ) { - _clickCallback = clickCallback - _imageChangeCallback = downloadImageChangeCallback - var lastState: VideoDownloadManager.DownloadType? = null - var currentBytes = setupCurrentBytes ?: 0 - var totalBytes = setupTotalBytes ?: 0 - var needImageUpdate = true - - fun changeDownloadImage(state: VideoDownloadManager.DownloadType) { - lastState = state - if (currentBytes <= 0) needImageUpdate = true - val img = if (currentBytes > 0) { - when (state) { - VideoDownloadManager.DownloadType.IsPaused -> Pair( - R.drawable.ic_baseline_play_arrow_24, - R.string.download_paused - ) - VideoDownloadManager.DownloadType.IsDownloading -> Pair( - R.drawable.netflix_pause, - R.string.downloading - ) - else -> Pair(R.drawable.ic_baseline_delete_outline_24, R.string.downloaded) - } - } else { - Pair(R.drawable.netflix_download, R.string.download) - } - _imageChangeCallback?.invoke( - Pair( - img.first, - downloadView.context.getString(img.second) - ) - ) - } - - fun fixDownloadedBytes(setCurrentBytes: Long, setTotalBytes: Long, animate: Boolean) { - currentBytes = setCurrentBytes - totalBytes = setTotalBytes - - if (currentBytes == 0L) { - changeDownloadImage(VideoDownloadManager.DownloadType.IsStopped) - textView?.visibility = View.GONE - progressBar.visibility = View.GONE - } else { - if (lastState == VideoDownloadManager.DownloadType.IsStopped) { - changeDownloadImage(VideoDownloadManager.getDownloadState(data.id)) - } - textView?.visibility = View.VISIBLE - progressBar.visibility = View.VISIBLE - val currentMbString = formatShortFileSize(textView?.context, setCurrentBytes) - val totalMbString = formatShortFileSize(textView?.context, setTotalBytes) - - textView?.text = - if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else - textView?.context?.getString(R.string.download_size_format) - ?.format(currentMbString, totalMbString) - - progressBar.let { bar -> - bar.max = (setTotalBytes / 1000).toInt() - - if (animate) { - val animation: ObjectAnimator = ObjectAnimator.ofInt( - bar, - "progress", - bar.progress, - (setCurrentBytes / 1000).toInt() - ) - animation.duration = 500 - animation.setAutoCancel(true) - animation.interpolator = DecelerateInterpolator() - animation.start() - } else { - bar.progress = (setCurrentBytes / 1000).toInt() - } - } - } - } - - fixDownloadedBytes(currentBytes, totalBytes, false) - changeDownloadImage(VideoDownloadManager.getDownloadState(data.id)) - - downloadProgressEventListener = { downloadData: Triple -> - if (data.id == downloadData.first) { - if (downloadData.second != currentBytes || downloadData.third != totalBytes) { // TO PREVENT WASTING UI TIME - Coroutines.runOnMainThread { - fixDownloadedBytes(downloadData.second, downloadData.third, true) - changeDownloadImage(VideoDownloadManager.getDownloadState(data.id)) - } - } - } - } - - downloadStatusEventListener = - { downloadData: Pair -> - if (data.id == downloadData.first) { - if (lastState != downloadData.second || needImageUpdate) { // TO PREVENT WASTING UI TIME - Coroutines.runOnMainThread { - changeDownloadImage(downloadData.second) - } - } - } - } - - downloadProgressEventListener?.let { VideoDownloadManager.downloadProgressEvent += it } - downloadStatusEventListener?.let { VideoDownloadManager.downloadStatusEvent += it } - - downloadView.setOnClickListener { - if (currentBytes <= 0 || totalBytes <= 0) { - _clickCallback?.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) - } else { - val list = arrayListOf( - Pair(DOWNLOAD_ACTION_PLAY_FILE, R.string.popup_play_file), - Pair(DOWNLOAD_ACTION_DELETE_FILE, R.string.popup_delete_file), - ) - - // DON'T RESUME A DOWNLOADED FILE lastState != VideoDownloadManager.DownloadType.IsDone && - if ((currentBytes * 100 / totalBytes) < 98) { - list.add( - if (lastState == VideoDownloadManager.DownloadType.IsDownloading) - Pair(DOWNLOAD_ACTION_PAUSE_DOWNLOAD, R.string.popup_pause_download) - else - Pair(DOWNLOAD_ACTION_RESUME_DOWNLOAD, R.string.popup_resume_download) - ) - } - - it.popupMenuNoIcons( - list - ) { - _clickCallback?.invoke(DownloadClickEvent(itemId, data)) - } - } - } - - downloadView.setOnLongClickListener { - clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data)) - return@setOnLongClickListener true - } - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index 05f630a0..b43f1aac 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -22,7 +22,7 @@ data class DownloadMetadata( val progressPercentage: Long get() = if (downloadedLength < 1024) 0 else maxOf( 0, - minOf(100, (downloadedLength * 100L) / totalLength) + minOf(100, (downloadedLength * 100L) / (totalLength + 1)) ) } @@ -101,38 +101,41 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : open fun setProgress(downloadedBytes: Long, totalBytes: Long) { isZeroBytes = downloadedBytes == 0L - val steps = 10000L - progressBar.max = steps.toInt() - // div by zero error and 1 byte off is ok impo - val progress = (downloadedBytes * steps / (totalBytes + 1L)).toInt() + progressBar.post { + val steps = 10000L + progressBar.max = steps.toInt() + // div by zero error and 1 byte off is ok impo - val animation = ProgressBarAnimation( - progressBar, - progressBar.progress.toFloat(), - progress.toFloat() - ).apply { - fillAfter = true - duration = - if (progress > progressBar.progress) // we don't want to animate backward changes in progress - 100 - else - 0L - } + val progress = (downloadedBytes * steps / (totalBytes + 1L)).toInt() - if (isZeroBytes) { - progressText?.isVisible = false - } else { - progressText?.apply { - val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes) - val totalMbString = Formatter.formatShortFileSize(context, totalBytes) - text = - //if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else - context?.getString(R.string.download_size_format) - ?.format(currentMbString, totalMbString) + val animation = ProgressBarAnimation( + progressBar, + progressBar.progress.toFloat(), + progress.toFloat() + ).apply { + fillAfter = true + duration = + if (progress > progressBar.progress) // we don't want to animate backward changes in progress + 100 + else + 0L } - } - progressBar.startAnimation(animation) + if (isZeroBytes) { + progressText?.isVisible = false + } else { + progressText?.apply { + val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes) + val totalMbString = Formatter.formatShortFileSize(context, totalBytes) + text = + //if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else + context?.getString(R.string.download_size_format) + ?.format(currentMbString, totalMbString) + } + } + + progressBar.startAnimation(animation) + } } fun downloadStatusEvent(data: Pair) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt index bb2ba7b1..d97a4b88 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt @@ -21,14 +21,17 @@ class DownloadButton(context: Context, attributeSet: AttributeSet) : } override fun setStatus(status: DownloadStatusTell?) { - super.setStatus(status) - val txt = when (status) { - DownloadStatusTell.IsPaused -> R.string.download_paused - DownloadStatusTell.IsDownloading -> R.string.downloading - DownloadStatusTell.IsDone -> R.string.downloaded - else -> R.string.download + mainText?.post { + val txt = when (status) { + DownloadStatusTell.IsPaused -> R.string.download_paused + DownloadStatusTell.IsDownloading -> R.string.downloading + DownloadStatusTell.IsDone -> R.string.downloaded + else -> R.string.download + } + mainText?.setText(txt) } - mainText?.setText(txt) + super.setStatus(status) + } override fun setDefaultClickListener( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index 0b7a7fea..d20fcf93 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -174,7 +174,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : currentMetaData.apply { // DON'T RESUME A DOWNLOADED FILE lastState != VideoDownloadManager.DownloadType.IsDone && - if ((downloadedLength * 100 / totalLength) < 98) { + if (progressPercentage < 98) { list.add( if (status == VideoDownloadManager.DownloadType.IsDownloading) Pair(DOWNLOAD_ACTION_PAUSE_DOWNLOAD, R.string.popup_pause_download) @@ -248,33 +248,34 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : //progressBar.isVisible = // status != null && status != DownloadStatusTell.Complete && status != DownloadStatusTell.Error //progressBarBackground.isVisible = status != null && status != DownloadStatusTell.Complete - val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading + progressBarBackground.post { + val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading + if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { + val animation = AnimationUtils.loadAnimation(context, waitingAnimation) + progressBarBackground.startAnimation(animation) + } else { + progressBarBackground.clearAnimation() + } - if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { - val animation = AnimationUtils.loadAnimation(context, waitingAnimation) - progressBarBackground.startAnimation(animation) - } else { - progressBarBackground.clearAnimation() + val progressDrawable = + if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline + + progressBarBackground.background = + ContextCompat.getDrawable(context, progressDrawable) + + val drawable = getDrawableFromStatus(status) + statusView.setImageDrawable(drawable) + val isDrawable = drawable != null + + statusView.isVisible = isDrawable + val hide = hideWhenIcon && isDrawable + if (hide) { + progressBar.clearAnimation() + progressBarBackground.clearAnimation() + } + progressBarBackground.isGone = hide + progressBar.isGone = hide } - - val progressDrawable = - if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline - - progressBarBackground.background = - ContextCompat.getDrawable(context, progressDrawable) - - val drawable = getDrawableFromStatus(status) - statusView.setImageDrawable(drawable) - val isDrawable = drawable != null - - statusView.isVisible = isDrawable - val hide = hideWhenIcon && isDrawable - if (hide) { - progressBar.clearAnimation() - progressBarBackground.clearAnimation() - } - progressBarBackground.isGone = hide - progressBar.isGone = hide } override fun resetView() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 6770e303..1fb3a72d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -186,6 +186,27 @@ object M3u8Helper2 { ) { val size get() = allTsLinks.size + suspend fun resolveLinkWhileSafe( + index: Int, + tries: Int = 3, + failDelay: Long = 3000, + condition : (() -> Boolean) + ): ByteArray? { + for (i in 0 until tries) { + if(!condition()) return null + + try { + val out = resolveLink(index) + return if(condition()) out else null + } catch (e: IllegalArgumentException) { + return null + } catch (t: Throwable) { + delay(failDelay) + } + } + return null + } + suspend fun resolveLinkSafe( index: Int, tries: Int = 3, @@ -240,8 +261,6 @@ object M3u8Helper2 { verify = false ).text - println("m3u8Response=$m3u8Response") - // encryption, this is because crunchy uses it var encryptionIv = byteArrayOf() var encryptionData = byteArrayOf() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt index a76cc115..d1614bc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt @@ -2,20 +2,18 @@ package com.lagradost.cloudstream3.utils import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.ui.download.EasyDownloadButton - object VideoDownloadHelper { data class DownloadEpisodeCached( @JsonProperty("name") val name: String?, @JsonProperty("poster") val poster: String?, @JsonProperty("episode") val episode: Int, @JsonProperty("season") val season: Int?, - @JsonProperty("id") override val id: Int, + @JsonProperty("id") val id: Int, @JsonProperty("parentId") val parentId: Int, @JsonProperty("rating") val rating: Int?, @JsonProperty("description") val description: String?, @JsonProperty("cacheTime") val cacheTime: Long, - ) : EasyDownloadButton.IMinimumData + ) data class DownloadHeaderCached( @JsonProperty("apiName") val apiName: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index f4eb37b7..0334103f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -15,7 +15,6 @@ import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri -import androidx.lifecycle.MutableLiveData import androidx.preference.PreferenceManager import androidx.work.Data import androidx.work.ExistingWorkPolicy @@ -30,6 +29,8 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.services.VideoDownloadService @@ -42,13 +43,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly -import java.io.BufferedInputStream +import java.io.Closeable import java.io.File +import java.io.IOException import java.io.InputStream import java.io.OutputStream -import java.lang.Thread.sleep import java.net.URL -import java.net.URLConnection import java.util.* const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general" @@ -60,34 +60,31 @@ object VideoDownloadManager { private var currentDownloads = mutableListOf() private const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" - @DrawableRes - const val imgDone = R.drawable.rddone + @get:DrawableRes + val imgDone get() = R.drawable.rddone - @DrawableRes - const val imgDownloading = R.drawable.rdload + @get:DrawableRes + val imgDownloading get() = R.drawable.rdload - @DrawableRes - const val imgPaused = R.drawable.rdpause + @get:DrawableRes + val imgPaused get() = R.drawable.rdpause - @DrawableRes - const val imgStopped = R.drawable.rderror + @get:DrawableRes + val imgStopped get() = R.drawable.rderror - @DrawableRes - const val imgError = R.drawable.rderror + @get:DrawableRes + val imgError get() = R.drawable.rderror - @DrawableRes - const val pressToPauseIcon = R.drawable.ic_baseline_pause_24 + @get:DrawableRes + val pressToPauseIcon get() = R.drawable.ic_baseline_pause_24 - @DrawableRes - const val pressToResumeIcon = R.drawable.ic_baseline_play_arrow_24 + @get:DrawableRes + val pressToResumeIcon get() = R.drawable.ic_baseline_play_arrow_24 - @DrawableRes - const val pressToStopIcon = R.drawable.exo_icon_stop - - private var updateCount: Int = 0 - private val downloadDataUpdateCount = MutableLiveData() + @get:DrawableRes + val pressToStopIcon get() = R.drawable.baseline_stop_24 enum class DownloadType { IsPaused, @@ -251,9 +248,8 @@ object VideoDownloadManager { total: Long, notificationCallback: (Int, Notification) -> Unit, hlsProgress: Long? = null, - hlsTotal: Long? = null, - - ): Notification? { + hlsTotal: Long? = null + ): Notification? { try { if (total <= 0) return null// crash, invalid data @@ -336,14 +332,28 @@ object VideoDownloadManager { } val bigText = - if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { - (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix" - } else if (state == DownloadType.IsFailed) { - downloadFormat.format(context.getString(R.string.download_failed), rowTwo) - } else if (state == DownloadType.IsDone) { - downloadFormat.format(context.getString(R.string.download_done), rowTwo) - } else { - downloadFormat.format(context.getString(R.string.download_canceled), rowTwo) + when (state) { + DownloadType.IsDownloading, DownloadType.IsPaused -> { + (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix" + } + + DownloadType.IsFailed -> { + downloadFormat.format( + context.getString(R.string.download_failed), + rowTwo + ) + } + + DownloadType.IsDone -> { + downloadFormat.format(context.getString(R.string.download_done), rowTwo) + } + + else -> { + downloadFormat.format( + context.getString(R.string.download_canceled), + rowTwo + ) + } } val bodyStyle = NotificationCompat.BigTextStyle() @@ -351,14 +361,28 @@ object VideoDownloadManager { builder.setStyle(bodyStyle) } else { val txt = - if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { - rowTwo - } else if (state == DownloadType.IsFailed) { - downloadFormat.format(context.getString(R.string.download_failed), rowTwo) - } else if (state == DownloadType.IsDone) { - downloadFormat.format(context.getString(R.string.download_done), rowTwo) - } else { - downloadFormat.format(context.getString(R.string.download_canceled), rowTwo) + when (state) { + DownloadType.IsDownloading, DownloadType.IsPaused -> { + rowTwo + } + + DownloadType.IsFailed -> { + downloadFormat.format( + context.getString(R.string.download_failed), + rowTwo + ) + } + + DownloadType.IsDone -> { + downloadFormat.format(context.getString(R.string.download_done), rowTwo) + } + + else -> { + downloadFormat.format( + context.getString(R.string.download_canceled), + rowTwo + ) + } } builder.setContentText(txt) @@ -681,6 +705,171 @@ object VideoDownloadManager { return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream) } + /** This class handles the notifications, as well as the relevant key */ + data class DownloadMetaData( + private val id: Int?, + var bytesDownloaded: Long = 0, + var totalBytes: Long? = null, + + // notification metadata + private var lastUpdatedMs: Long = 0, + private val createNotificationCallback: (CreateNotificationMetadata) -> Unit, + + private var internalType: DownloadType = DownloadType.IsPending, + + // how many segments that we have downloaded + var hlsProgress: Int = 0, + // how many segments that exist + var hlsTotal: Int? = null, + // this is how many segments that has been written to the file + // will always be <= hlsProgress as we may keep some in a buffer + var hlsWrittenProgress: Long = 0, + + // this is used for copy with metadata on how much we have downloaded for setKey + private var downloadFileInfoTemplate: DownloadedFileInfo? = null + ) : Closeable { + val approxTotalBytes: Long + get() = totalBytes ?: hlsTotal?.let { total -> + (bytesDownloaded * (total / hlsProgress.toFloat())).toLong() + } ?: 0L + + private val isHLS get() = hlsTotal != null + + private val downloadEventListener = { event: Pair -> + if (event.first == id) { + when (event.second) { + DownloadActionType.Pause -> { + type = DownloadType.IsPaused + } + + DownloadActionType.Stop -> { + type = DownloadType.IsStopped + removeKey(KEY_RESUME_PACKAGES, event.first.toString()) + saveQueue() + } + + DownloadActionType.Resume -> { + type = DownloadType.IsDownloading + } + } + } + } + + private fun updateFileInfo() { + if (id == null) return + downloadFileInfoTemplate?.let { template -> + setKey( + KEY_DOWNLOAD_INFO, + id.toString(), + template.copy( + totalBytes = approxTotalBytes, + extraInfo = if (isHLS) hlsWrittenProgress.toString() else null + ) + ) + } + } + + fun setDownloadFileInfoTemplate(template: DownloadedFileInfo) { + downloadFileInfoTemplate = template + updateFileInfo() + } + + init { + if (id != null) { + downloadEvent += downloadEventListener + } + } + + override fun close() { + // as we may need to resume hls downloads, we save the current written index + if (isHLS) { + updateFileInfo() + } + if (id != null) { + downloadEvent -= downloadEventListener + downloadStatus -= id + } + } + + var type + get() = internalType + set(value) { + internalType = value + notify() + } + + companion object { + const val UPDATE_RATE_MS: Long = 1000L + } + + @JvmName("DownloadMetaDataNotify") + private fun notify() { + lastUpdatedMs = System.currentTimeMillis() + try { + val bytes = approxTotalBytes + + // notification creation + if (isHLS) { + createNotificationCallback( + CreateNotificationMetadata( + internalType, + bytesDownloaded, + bytes, + hlsTotal = hlsTotal?.toLong(), + hlsProgress = hlsProgress.toLong() + ) + ) + } else { + createNotificationCallback( + CreateNotificationMetadata( + internalType, + bytesDownloaded, + bytes, + ) + ) + } + + // as hls has an approx file size we want to update this metadata + if (isHLS) { + updateFileInfo() + } + + // push all events, this *should* not crash, TODO MUTEX? + if (id != null) { + downloadStatus[id] = type + downloadProgressEvent(Triple(id, bytesDownloaded, bytes)) + downloadStatusEvent(id to type) + } + } catch (t: Throwable) { + logError(t) + } + } + + private fun checkNotification() { + if (lastUpdatedMs + UPDATE_RATE_MS > System.currentTimeMillis()) return + notify() + } + + + /** adds the length and pushes a notification if necessary */ + fun addBytes(length: Long) { + bytesDownloaded += length + // we don't want to update the notification after it is paused, + // download progress may not stop directly when we "pause" it + if (type == DownloadType.IsDownloading) checkNotification() + } + + /** adds the length + hsl progress and pushes a notification if necessary */ + fun addSegment(length: Long) { + hlsProgress += 1 + addBytes(length) + } + + fun setWrittenSegment(segmentIndex: Int) { + hlsWrittenProgress = segmentIndex.toLong() + 1L + } + } + @Throws suspend fun downloadThing( context: Context, @@ -692,253 +881,225 @@ object VideoDownloadManager { parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, ): Int = withContext(Dispatchers.IO) { + // we cant download torrents with this implementation, aria2c might be used in the future if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { return@withContext ERROR_UNKNOWN } - val basePath = context.getBasePath() - - val displayName = getDisplayName(name, extension) - val relativePath = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.first.isDownloadDir()) getRelativePath( - folder - ) else folder - - fun deleteFile(): Int { - return delete(context, name, relativePath, extension, parentId, basePath.first) - } - - val stream = setupStream(context, name, relativePath, extension, tryResume) - if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - - val resume = stream.resume!! - val fileStream = stream.fileStream!! - val fileLength = stream.fileLength!! - - // CONNECT - val connection: URLConnection = - URL(link.url.replace(" ", "%20")).openConnection() // IDK OLD PHONES BE WACK - - // SET CONNECTION SETTINGS - connection.connectTimeout = 10000 - connection.setRequestProperty("Accept-Encoding", "identity") - connection.setRequestProperty("user-agent", USER_AGENT) - if (link.referer.isNotEmpty()) connection.setRequestProperty("referer", link.referer) - - // extra stuff - connection.setRequestProperty( - "sec-ch-ua", - "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"" + var fileStream: OutputStream? = null + var requestStream: InputStream? = null + val metadata = DownloadMetaData( + totalBytes = 0, + bytesDownloaded = 0, + createNotificationCallback = createNotificationCallback, + id = parentId, ) + try { + // get the file path + val (baseFile, basePath) = context.getBasePath() + val displayName = getDisplayName(name, extension) + val relativePath = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.isDownloadDir()) getRelativePath( + folder + ) else folder - connection.setRequestProperty("sec-ch-ua-mobile", "?0") - connection.setRequestProperty("accept", "*/*") - // dataSource.setRequestProperty("Sec-Fetch-Site", "none") //same-site - connection.setRequestProperty("sec-fetch-user", "?1") - connection.setRequestProperty("sec-fetch-mode", "navigate") - connection.setRequestProperty("sec-fetch-dest", "video") - link.headers.entries.forEach { - connection.setRequestProperty(it.key, it.value) - } + // set up the download file + val stream = setupStream(context, name, relativePath, extension, tryResume) + if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode + fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN + val resume = stream.resume ?: return@withContext ERROR_UNKNOWN + val fileLength = stream.fileLength ?: return@withContext ERROR_UNKNOWN + val resumeAt = (if (resume) fileLength else 0) + metadata.bytesDownloaded = resumeAt + metadata.type = DownloadType.IsPending - if (resume) - connection.setRequestProperty("Range", "bytes=${fileLength}-") - val resumeLength = (if (resume) fileLength else 0) + // set up a connection + val request = app.get( + link.url.replace(" ", "%20"), + headers = link.headers + mapOf( + "Accept-Encoding" to "identity", + "accept" to "*/*", + "user-agent" to USER_AGENT, + "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", + "sec-fetch-mode" to "navigate", + "sec-fetch-dest" to "video", + "sec-fetch-user" to "?1", + "sec-ch-ua-mobile" to "?0", + ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap(), + referer = link.referer, + verify = false + ) - // ON CONNECTION - connection.connect() + // init variables + val contentLength = request.size ?: 0 + metadata.totalBytes = contentLength + resumeAt - val contentLength = try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // fuck android - connection.contentLengthLong - } else { - connection.getHeaderField("content-length").toLongOrNull() - ?: connection.contentLength.toLong() - } - } catch (e: Exception) { - logError(e) - 0L - } - val bytesTotal = contentLength + resumeLength - - if (extension == "mp4" && bytesTotal < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG - - parentId?.let { - setKey( - KEY_DOWNLOAD_INFO, - it.toString(), + // save + metadata.setDownloadFileInfoTemplate( DownloadedFileInfo( - bytesTotal, - relativePath ?: "", - displayName, - basePath = basePath.second + totalBytes = metadata.approxTotalBytes, + relativePath = relativePath ?: "", + displayName = displayName, + basePath = basePath ) ) + + // total length is less than 5mb, that is too short and something has gone wrong + if (extension == "mp4" && metadata.approxTotalBytes < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION + + // read the buffer into the filestream, this is equivalent of transferTo + requestStream = request.body.byteStream() + metadata.type = DownloadType.IsDownloading + + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var read: Int + while (requestStream.read(buffer, 0, DEFAULT_BUFFER_SIZE).also { read = it } >= 0) { + fileStream.write(buffer, 0, read) + + // wait until not paused + while (metadata.type == DownloadType.IsPaused) delay(100) + // if stopped then break to delete + if (metadata.type == DownloadType.IsStopped) break + metadata.addBytes(read.toLong()) + } + + if (metadata.type == DownloadType.IsStopped) { + return@withContext delete( + context, + name, + relativePath, + extension, + parentId, + baseFile + ) + } + + metadata.type = DownloadType.IsDone + return@withContext SUCCESS_DOWNLOAD_DONE + } catch (e: IOException) { + // some sort of IO error, this should not happened + // we just rethrow it + logError(e) + throw e + } catch (t: Throwable) { + // some sort of network error, will error + + // note that when failing we don't want to delete the file, + // only user interaction has that power + metadata.type = DownloadType.IsFailed + return@withContext ERROR_CONNECTION_ERROR + } finally { + fileStream?.closeQuietly() + requestStream?.closeQuietly() + metadata.close() } + } - // Could use connection.contentType for mime types when creating the file, - // however file is already created and players don't go of file type - // https://stackoverflow.com/questions/23714383/what-are-all-the-possible-values-for-http-content-type-header - // might receive application/octet-stream - /*if (!connection.contentType.isNullOrEmpty() && !connection.contentType.startsWith("video")) { - return ERROR_WRONG_CONTENT // CONTENT IS NOT VIDEO, SHOULD NEVER HAPPENED, BUT JUST IN CASE - }*/ + @Throws + private suspend fun downloadHLS( + context: Context, + link: ExtractorLink, + name: String, + folder: String?, + parentId: Int?, + startIndex: Int?, + createNotificationCallback: (CreateNotificationMetadata) -> Unit, + parallelConnections: Int = 3 + ): Int = withContext(Dispatchers.IO) { + require(parallelConnections >= 1) - // READ DATA FROM CONNECTION - val connectionInputStream: InputStream = BufferedInputStream(connection.inputStream) - val buffer = ByteArray(1024) - var count: Int - var bytesDownloaded = resumeLength + val metadata = DownloadMetaData( + createNotificationCallback = createNotificationCallback, + id = parentId + ) + val extension = "mp4" - var isPaused = false - var isStopped = false - var isDone = false - var isFailed = false + var fileStream: OutputStream? = null + try { + // the start .ts index + var startAt = startIndex ?: 0 - // TO NOT REUSE CODE - fun updateNotification() { - val type = when { - isDone -> DownloadType.IsDone - isStopped -> DownloadType.IsStopped - isFailed -> DownloadType.IsFailed - isPaused -> DownloadType.IsPaused - else -> DownloadType.IsDownloading - } + // set up the file data + val (baseFile, basePath) = context.getBasePath() + val relativePath = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.isDownloadDir()) getRelativePath( + folder + ) else folder + val displayName = getDisplayName(name, extension) + val stream = setupStream(context, name, relativePath, extension, startAt > 0) + if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode + if (stream.resume != true) startAt = 0 + fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN - parentId?.let { id -> - try { - downloadStatus[id] = type - downloadStatusEvent.invoke(Pair(id, type)) - downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal)) - } catch (e: Exception) { - // IDK MIGHT ERROR - } - } - - createNotificationCallback.invoke( - CreateNotificationMetadata( - type, - bytesDownloaded, - bytesTotal + // push the metadata + metadata.bytesDownloaded = stream.fileLength ?: 0 + metadata.hlsProgress = startAt + metadata.type = DownloadType.IsPending + metadata.setDownloadFileInfoTemplate( + DownloadedFileInfo( + totalBytes = 0, + relativePath = relativePath ?: "", + displayName = displayName, + basePath = basePath ) ) - /*createNotification( - context, - source, - link.name, - ep, - type, - bytesDownloaded, - bytesTotal - )*/ - } - val downloadEventListener = { event: Pair -> - if (event.first == parentId) { - when (event.second) { - DownloadActionType.Pause -> { - isPaused = true; updateNotification() + // do the initial get request to fetch the segments + val m3u8 = M3u8Helper.M3u8Stream( + link.url, link.quality, mapOf( + "Accept-Encoding" to "identity", + "accept" to "*/*", + "user-agent" to USER_AGENT, + ) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap() + ) + val items = M3u8Helper2.hslLazy(listOf(m3u8)) + + metadata.hlsTotal = items.size + metadata.type = DownloadType.IsDownloading + + // does several connections in parallel instead of a regular for loop to improve + // download speed + (startAt until items.size).chunked(parallelConnections).forEach { subset -> + // wait until not paused + while (metadata.type == DownloadType.IsPaused) delay(100) + // if stopped then break to delete + if (metadata.type == DownloadType.IsStopped) return@forEach + + subset.amap { idx -> + idx to items.resolveLinkSafe(idx)?.also { bytes -> + metadata.addSegment(bytes.size.toLong()) } - - DownloadActionType.Stop -> { - isStopped = true; updateNotification() - removeKey(KEY_RESUME_PACKAGES, event.first.toString()) - saveQueue() - } - - DownloadActionType.Resume -> { - isPaused = false; updateNotification() + }.forEach { (idx, bytes) -> + if (bytes == null) { + metadata.type = DownloadType.IsFailed + return@withContext ERROR_CONNECTION_ERROR } + fileStream.write(bytes) + metadata.setWrittenSegment(idx) } } - } - if (parentId != null) - downloadEvent += downloadEventListener - - // UPDATE DOWNLOAD NOTIFICATION - val notificationCoroutine = main { - while (true) { - if (!isPaused) { - updateNotification() - } - for (i in 1..10) { - delay(100) - } - } - } - - // THE REAL READ - try { - while (true) { - count = connectionInputStream.read(buffer) - if (count < 0) break - bytesDownloaded += count - // downloadProgressEvent.invoke(Pair(id, bytesDownloaded)) // Updates too much for any UI to keep up with - while (isPaused) { - sleep(100) - if (isStopped) { - break - } - } - if (isStopped) { - break - } - fileStream.write(buffer, 0, count) - } - } catch (e: Exception) { - logError(e) - isFailed = true - updateNotification() - } - - // REMOVE AND EXIT ALL - fileStream.close() - connectionInputStream.close() - notificationCoroutine.cancel() - - try { - if (parentId != null) - downloadEvent -= downloadEventListener - } catch (e: Exception) { - logError(e) - } - - try { - parentId?.let { - downloadStatus.remove(it) - } - } catch (e: Exception) { - // IDK MIGHT ERROR - } - - // RETURN MESSAGE - return@withContext when { - isFailed -> { - parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } - ERROR_CONNECTION_ERROR + if (metadata.type == DownloadType.IsStopped) { + return@withContext delete( + context, + name, + relativePath, + extension, + parentId, + baseFile + ) } - isStopped -> { - parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } - deleteFile() - } - - else -> { - parentId?.let { id -> - downloadProgressEvent.invoke( - Triple( - id, - bytesDownloaded, - bytesTotal - ) - ) - } - isDone = true - updateNotification() - SUCCESS_DOWNLOAD_DONE - } + metadata.type = DownloadType.IsDone + return@withContext SUCCESS_DOWNLOAD_DONE + } catch (t: Throwable) { + logError(t) + metadata.type = DownloadType.IsFailed + return@withContext ERROR_UNKNOWN + } finally { + fileStream?.closeQuietly() + metadata.close() } } @@ -1107,192 +1268,6 @@ object VideoDownloadManager { return SUCCESS_STOPPED } - @Throws - private suspend fun downloadHLS( - context: Context, - link: ExtractorLink, - name: String, - folder: String?, - parentId: Int?, - startIndex: Int?, - createNotificationCallback: (CreateNotificationMetadata) -> Unit - ): Int = withContext(Dispatchers.IO) { - val extension = "mp4" - - val m3u8 = M3u8Helper.M3u8Stream( - link.url, link.quality, mapOf("referer" to link.referer) - ) - - var realIndex = startIndex ?: 0 - val basePath = context.getBasePath() - - val relativePath = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.first.isDownloadDir()) getRelativePath( - folder - ) else folder - - val stream = setupStream(context, name, relativePath, extension, realIndex > 0) - if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - - if (stream.resume != true) realIndex = 0 - val fileLengthAdd = stream.fileLength ?: 0 - val items = M3u8Helper2.hslLazy(listOf(m3u8)) - - val displayName = getDisplayName(name, extension) - - val fileStream = stream.fileStream!! - - var isDone = false - var isFailed = false - var isPaused = false - var bytesDownloaded = fileLengthAdd - var tsProgress: Long = realIndex.toLong() + 1 // we don't want div by zero - val totalTs: Long = items.size.toLong() - - fun deleteFile(): Int { - return delete(context, name, relativePath, extension, parentId, basePath.first) - } - - fun updateInfo() { - setKey( - KEY_DOWNLOAD_INFO, - (parentId ?: return).toString(), - DownloadedFileInfo( - // approx bytes - totalBytes = (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - relativePath = relativePath ?: "", - displayName = displayName, - extraInfo = tsProgress.toString(), - basePath = basePath.second - ) - ) - } - - updateInfo() - - fun updateNotification() { - val type = when { - isDone -> DownloadType.IsDone - isFailed -> DownloadType.IsFailed - isPaused -> DownloadType.IsPaused - else -> DownloadType.IsDownloading - } - - parentId?.let { id -> - try { - downloadStatus[id] = type - downloadStatusEvent.invoke(Pair(id, type)) - downloadProgressEvent.invoke( - Triple( - id, - bytesDownloaded, - (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - ) - ) - } catch (_: Throwable) {} - } - - createNotificationCallback.invoke( - CreateNotificationMetadata( - type, - bytesDownloaded, - (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - tsProgress, - totalTs - ) - ) - } - - val notificationCoroutine = main { - while (true) { - if (!isDone) { - updateNotification() - } - for (i in 1..10) { - delay(100) - } - } - } - - val downloadEventListener = { event: Pair -> - if (event.first == parentId) { - when (event.second) { - DownloadActionType.Stop -> { - isFailed = true - } - - DownloadActionType.Pause -> { - isPaused = true - } - - DownloadActionType.Resume -> { - isPaused = false - } - } - updateNotification() - } - } - - fun closeAll() { - try { - if (parentId != null) - downloadEvent -= downloadEventListener - } catch (t: Throwable) { - logError(t) - } - try { - parentId?.let { - downloadStatus.remove(it) - } - } catch (t: Throwable) { - logError(t) - } - notificationCoroutine.cancel() - } - - if (parentId != null) - downloadEvent += downloadEventListener - - fun onFailed() { - fileStream.close() - deleteFile() - updateNotification() - closeAll() - } - - for (idx in realIndex until items.size) { - while (isPaused) { - if (isFailed) { - onFailed() - return@withContext SUCCESS_STOPPED - } - delay(100) - } - - if (isFailed) { - onFailed() - return@withContext SUCCESS_STOPPED - } - - val bytes = items.resolveLinkSafe(idx) ?: run { - isFailed = true - onFailed() - return@withContext ERROR_CONNECTION_ERROR - } - - fileStream.write(bytes) - tsProgress = idx.toLong() + 1 - bytesDownloaded += bytes.size.toLong() - updateInfo() - } - isDone = true - fileStream.close() - updateNotification() - - closeAll() - updateInfo() - return@withContext SUCCESS_DOWNLOAD_DONE - } fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { return getFileName(context, metadata.name, metadata.episode, metadata.season) @@ -1353,22 +1328,30 @@ object VideoDownloadManager { )?.extraInfo?.toIntOrNull() } else null return suspendSafeApiCall { - downloadHLS(context, link, name, folder, ep.id, startIndex) { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback, - meta.hlsProgress, - meta.hlsTotal - ) + downloadHLS( + context, + link, + name, + folder, + ep.id, + startIndex, + createNotificationCallback = { meta -> + main { + createNotification( + context, + source, + link.name, + ep, + meta.type, + meta.bytesDownloaded, + meta.bytesTotal, + notificationCallback, + meta.hlsProgress, + meta.hlsTotal + ) + } } - } + ) }.also { extractorJob.cancel() } ?: ERROR_UNKNOWN @@ -1392,7 +1375,7 @@ object VideoDownloadManager { }.also { extractorJob.cancel() } ?: ERROR_UNKNOWN } - fun downloadCheck( + suspend fun downloadCheck( context: Context, notificationCallback: (Int, Notification) -> Unit, ): Int? { if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) { @@ -1407,42 +1390,55 @@ object VideoDownloadManager { currentDownloads.add(id) - main { - try { - for (index in (pkg.linkIndex ?: 0) until item.links.size) { - val link = item.links[index] - val resume = pkg.linkIndex == index + try { + for (index in (pkg.linkIndex ?: 0) until item.links.size) { + val link = item.links[index] + val resume = pkg.linkIndex == index - setKey( - KEY_RESUME_PACKAGES, - id.toString(), - DownloadResumePackage(item, index) + setKey( + KEY_RESUME_PACKAGES, + id.toString(), + DownloadResumePackage(item, index) + ) + + var connectionResult = + downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + resume + ) + //.also { println("Single episode finished with return code: $it") } + + // retry every link at least once + if (connectionResult <= 0) { + connectionResult = downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + true ) - val connectionResult = withContext(Dispatchers.IO) { - downloadSingleEpisode( - context, - item.source, - item.folder, - item.ep, - link, - notificationCallback, - resume - ).also { println("Single episode finished with return code: $it") } - } - if (connectionResult != null && connectionResult > 0) { // SUCCESS - removeKey(KEY_RESUME_PACKAGES, id.toString()) - break - } else if (index == item.links.lastIndex) { - downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) - } } - } catch (e: Exception) { - logError(e) - } finally { - currentDownloads.remove(id) - // Because otherwise notifications will not get caught by the workmanager - downloadCheckUsingWorker(context) + + if (connectionResult > 0) { // SUCCESS + removeKey(KEY_RESUME_PACKAGES, id.toString()) + break + } else if (index == item.links.lastIndex) { + downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) + } } + } catch (e: Exception) { + logError(e) + } finally { + currentDownloads.remove(id) + // Because otherwise notifications will not get caught by the work manager + downloadCheckUsingWorker(context) } } return null @@ -1538,26 +1534,13 @@ object VideoDownloadManager { return context.getKey(KEY_RESUME_PACKAGES, id.toString()) } - fun downloadFromResume( + suspend fun downloadFromResume( context: Context, pkg: DownloadResumePackage, notificationCallback: (Int, Notification) -> Unit, setKey: Boolean = true ) { if (!currentDownloads.any { it == pkg.item.ep.id }) { -// if (currentDownloads.size == maxConcurrentDownloads) { -// main { -//// showToast( // can be replaced with regular Toast -//// context, -//// "${pkg.item.ep.mainName}${pkg.item.ep.episode?.let { " ${context.getString(R.string.episode)} $it " } ?: " "}${ -//// context.getString( -//// R.string.queued -//// ) -//// }", -//// Toast.LENGTH_SHORT -//// ) -// } -// } downloadQueue.addLast(pkg) downloadCheck(context, notificationCallback) if (setKey) saveQueue() @@ -1590,7 +1573,7 @@ object VideoDownloadManager { return false }*/ - fun downloadEpisode( + suspend fun downloadEpisode( context: Context?, source: String?, folder: String?, @@ -1599,13 +1582,12 @@ object VideoDownloadManager { notificationCallback: (Int, Notification) -> Unit, ) { if (context == null) return - if (links.isNotEmpty()) { - downloadFromResume( - context, - DownloadResumePackage(DownloadItem(source, folder, ep, links), null), - notificationCallback - ) - } + if (links.isEmpty()) return + downloadFromResume( + context, + DownloadResumePackage(DownloadItem(source, folder, ep, links), null), + notificationCallback + ) } /** Worker stuff */ diff --git a/app/src/main/res/drawable/baseline_stop_24.xml b/app/src/main/res/drawable/baseline_stop_24.xml new file mode 100644 index 00000000..100cb1fc --- /dev/null +++ b/app/src/main/res/drawable/baseline_stop_24.xml @@ -0,0 +1,10 @@ + + + From a05616e3e8c6c0373f88a7c6dcc022d42ca7188d Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 01:37:48 +0200 Subject: [PATCH 178/570] fix --- .../cloudstream3/extractors/Mp4Upload.kt | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt index 93a280ed..e746b286 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt @@ -10,24 +10,39 @@ open class Mp4Upload : ExtractorApi() { override var name = "Mp4Upload" override var mainUrl = "https://www.mp4upload.com" private val srcRegex = Regex("""player\.src\("(.*?)"""") - override val requiresReferer = true + private val srcRegex2 = Regex("""player\.src\([\w\W]*src: "(.*?)"""") + override val requiresReferer = true + private val idMatch = Regex("""mp4upload\.com/(embed-|)([A-Za-z0-9]*)""") override suspend fun getUrl(url: String, referer: String?): List? { - with(app.get(url)) { - getAndUnpack(this.text).let { unpackedText -> - val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull() - srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link -> - return listOf( - ExtractorLink( - name, - name, - link, - url, - quality ?: Qualities.Unknown.value, - ) - ) - } - } + val realUrl = idMatch.find(url)?.groupValues?.get(2)?.let { id -> + "$mainUrl/embed-$id.html" + } ?: url + val response = app.get(realUrl) + val unpackedText = getAndUnpack(response.text) + val quality = + unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull() + srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link -> + return listOf( + ExtractorLink( + name, + name, + link, + url, + quality ?: Qualities.Unknown.value, + ) + ) + } + srcRegex2.find(unpackedText)?.groupValues?.get(1)?.let { link -> + return listOf( + ExtractorLink( + name, + name, + link, + url, + quality ?: Qualities.Unknown.value, + ) + ) } return null } From 35e1b8b4dcbe5172afc8f4cf23a673a84f99c194 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 01:38:40 +0200 Subject: [PATCH 179/570] bump --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f72ec0b0..71015e31 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,7 +52,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.3" + versionName = "4.1.4" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") From e20e3dcfd3ce450b0efe61d64db4a34413652c72 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 04:46:47 +0200 Subject: [PATCH 180/570] fixed some bugs caused by new download update --- app/build.gradle.kts | 2 +- .../utils/DownloadFileWorkManager.kt | 17 +- .../utils/VideoDownloadManager.kt | 271 ++++++++++-------- 3 files changed, 165 insertions(+), 125 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 71015e31..708a2083 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,7 +52,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.4" + versionName = "4.1.5" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt index c1eb649b..aa424c08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt @@ -7,6 +7,7 @@ import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_INFO @@ -25,15 +26,16 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo override suspend fun doWork(): Result { val key = workerParams.inputData.getString("key") try { - println("KEY $key") if (key == DOWNLOAD_CHECK) { - downloadCheck(applicationContext, ::handleNotification)?.let { - awaitDownload(it) - } + downloadCheck(applicationContext, ::handleNotification) } else if (key != null) { - val info = applicationContext.getKey(WORK_KEY_INFO, key) + val info = + applicationContext.getKey(WORK_KEY_INFO, key) val pkg = - applicationContext.getKey(WORK_KEY_PACKAGE, key) + applicationContext.getKey( + WORK_KEY_PACKAGE, + key + ) if (info != null) { downloadEpisode( applicationContext, @@ -43,10 +45,8 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo info.links, ::handleNotification ) - awaitDownload(info.ep.id) } else if (pkg != null) { downloadFromResume(applicationContext, pkg, ::handleNotification) - awaitDownload(pkg.item.ep.id) } removeKeys(key) } @@ -73,6 +73,7 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo VideoDownloadManager.DownloadType.IsDone, VideoDownloadManager.DownloadType.IsFailed, VideoDownloadManager.DownloadType.IsStopped -> { isDone = true } + else -> Unit } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 0334103f..ef0d9d8a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -26,6 +26,7 @@ import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType @@ -723,7 +724,7 @@ object VideoDownloadManager { var hlsTotal: Int? = null, // this is how many segments that has been written to the file // will always be <= hlsProgress as we may keep some in a buffer - var hlsWrittenProgress: Long = 0, + var hlsWrittenProgress: Int = 0, // this is used for copy with metadata on how much we have downloaded for setKey private var downloadFileInfoTemplate: DownloadedFileInfo? = null @@ -798,6 +799,15 @@ object VideoDownloadManager { notify() } + fun onDelete() { + bytesDownloaded = 0 + hlsWrittenProgress = 0 + hlsProgress = 0 + + //internalType = DownloadType.IsStopped + notify() + } + companion object { const val UPDATE_RATE_MS: Long = 1000L } @@ -842,6 +852,9 @@ object VideoDownloadManager { } } catch (t: Throwable) { logError(t) + if (BuildConfig.DEBUG) { + throw t + } } } @@ -866,7 +879,7 @@ object VideoDownloadManager { } fun setWrittenSegment(segmentIndex: Int) { - hlsWrittenProgress = segmentIndex.toLong() + 1L + hlsWrittenProgress = segmentIndex + 1 } } @@ -916,16 +929,18 @@ object VideoDownloadManager { // set up a connection val request = app.get( link.url.replace(" ", "%20"), - headers = link.headers + mapOf( - "Accept-Encoding" to "identity", - "accept" to "*/*", - "user-agent" to USER_AGENT, - "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", - "sec-fetch-mode" to "navigate", - "sec-fetch-dest" to "video", - "sec-fetch-user" to "?1", - "sec-ch-ua-mobile" to "?0", - ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap(), + headers = link.headers.appendAndDontOverride( + mapOf( + "Accept-Encoding" to "identity", + "accept" to "*/*", + "user-agent" to USER_AGENT, + "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", + "sec-fetch-mode" to "navigate", + "sec-fetch-dest" to "video", + "sec-fetch-user" to "?1", + "sec-ch-ua-mobile" to "?0", + ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap() + ), referer = link.referer, verify = false ) @@ -964,14 +979,14 @@ object VideoDownloadManager { } if (metadata.type == DownloadType.IsStopped) { - return@withContext delete( - context, - name, - relativePath, - extension, - parentId, - baseFile - ) + // we need to close before delete + fileStream.closeQuietly() + metadata.onDelete() + if (deleteFile(context, baseFile, relativePath ?: "", displayName)) { + return@withContext SUCCESS_STOPPED + } else { + return@withContext ERROR_DELETING_FILE + } } metadata.type = DownloadType.IsDone @@ -995,6 +1010,18 @@ object VideoDownloadManager { } } + /** Helper function to make sure duplicate attributes don't get overriden or inserted without lowercase cmp + * example: map("a" to 1) appendAndDontOverride map("A" to 2, "a" to 3, "c" to 4) = map("a" to 1, "c" to 4) + * */ + private fun Map.appendAndDontOverride(rhs: Map): Map { + val out = this.toMutableMap() + val current = this.keys.map { it.lowercase() } + for ((key, value) in rhs) { + if (current.contains(key.lowercase())) continue + out[key] = value + } + return out + } @Throws private suspend fun downloadHLS( @@ -1047,11 +1074,13 @@ object VideoDownloadManager { // do the initial get request to fetch the segments val m3u8 = M3u8Helper.M3u8Stream( - link.url, link.quality, mapOf( - "Accept-Encoding" to "identity", - "accept" to "*/*", - "user-agent" to USER_AGENT, - ) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap() + link.url, link.quality, link.headers.appendAndDontOverride( + mapOf( + "Accept-Encoding" to "identity", + "accept" to "*/*", + "user-agent" to USER_AGENT, + ) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap() + ) ) val items = M3u8Helper2.hslLazy(listOf(m3u8)) @@ -1081,14 +1110,14 @@ object VideoDownloadManager { } if (metadata.type == DownloadType.IsStopped) { - return@withContext delete( - context, - name, - relativePath, - extension, - parentId, - baseFile - ) + // we need to close before delete + fileStream.closeQuietly() + metadata.onDelete() + if (deleteFile(context, baseFile, relativePath ?: "", displayName)) { + return@withContext SUCCESS_STOPPED + } else { + return@withContext ERROR_DELETING_FILE + } } metadata.type = DownloadType.IsDone @@ -1194,7 +1223,7 @@ object VideoDownloadManager { return (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace( '/', File.separatorChar - ) + ).replace("${File.separatorChar}${File.separatorChar}", File.separatorChar.toString()) } /** @@ -1224,7 +1253,7 @@ object VideoDownloadManager { return this != null && this.filePath == getDownloadDir()?.filePath } - private fun delete( + /*private fun delete( context: Context, name: String, folder: String?, @@ -1235,7 +1264,7 @@ object VideoDownloadManager { val displayName = getDisplayName(name, extension) // delete all subtitle files - if (extension == "mp4") { + if (extension != "vtt" && extension != "srt") { try { delete(context, name, folder, "vtt", parentId, basePath) delete(context, name, folder, "srt", parentId, basePath) @@ -1248,9 +1277,9 @@ object VideoDownloadManager { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.isDownloadDir()) { val relativePath = getRelativePath(folder) val lastContent = - context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName) - if (lastContent != null) { - context.contentResolver.delete(lastContent, null, null) + context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName) ?: return ERROR_DELETING_FILE + if(context.contentResolver.delete(lastContent, null, null) <= 0) { + return ERROR_DELETING_FILE } } else { val dir = basePath?.gotoDir(folder) @@ -1260,13 +1289,12 @@ object VideoDownloadManager { // Cleans up empty directory if (dir.listFiles()?.isEmpty() == true) dir.delete() } -// } parentId?.let { downloadDeleteEvent.invoke(parentId) } } return SUCCESS_STOPPED - } + }*/ fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { @@ -1377,71 +1405,71 @@ object VideoDownloadManager { suspend fun downloadCheck( context: Context, notificationCallback: (Int, Notification) -> Unit, - ): Int? { - if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) { - val pkg = downloadQueue.removeFirst() - val item = pkg.item - val id = item.ep.id - if (currentDownloads.contains(id)) { // IF IT IS ALREADY DOWNLOADING, RESUME IT - downloadEvent.invoke(Pair(id, DownloadActionType.Resume)) - /** ID needs to be returned to the work-manager to properly await notification */ - return id - } + ) { + if (!(currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0)) return - currentDownloads.add(id) - - try { - for (index in (pkg.linkIndex ?: 0) until item.links.size) { - val link = item.links[index] - val resume = pkg.linkIndex == index - - setKey( - KEY_RESUME_PACKAGES, - id.toString(), - DownloadResumePackage(item, index) - ) - - var connectionResult = - downloadSingleEpisode( - context, - item.source, - item.folder, - item.ep, - link, - notificationCallback, - resume - ) - //.also { println("Single episode finished with return code: $it") } - - // retry every link at least once - if (connectionResult <= 0) { - connectionResult = downloadSingleEpisode( - context, - item.source, - item.folder, - item.ep, - link, - notificationCallback, - true - ) - } - - if (connectionResult > 0) { // SUCCESS - removeKey(KEY_RESUME_PACKAGES, id.toString()) - break - } else if (index == item.links.lastIndex) { - downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) - } - } - } catch (e: Exception) { - logError(e) - } finally { - currentDownloads.remove(id) - // Because otherwise notifications will not get caught by the work manager - downloadCheckUsingWorker(context) - } + val pkg = downloadQueue.removeFirst() + val item = pkg.item + val id = item.ep.id + if (currentDownloads.contains(id)) { // IF IT IS ALREADY DOWNLOADING, RESUME IT + downloadEvent.invoke(Pair(id, DownloadActionType.Resume)) + /** ID needs to be returned to the work-manager to properly await notification */ + // return id } - return null + + currentDownloads.add(id) + try { + for (index in (pkg.linkIndex ?: 0) until item.links.size) { + val link = item.links[index] + val resume = pkg.linkIndex == index + + setKey( + KEY_RESUME_PACKAGES, + id.toString(), + DownloadResumePackage(item, index) + ) + + var connectionResult = + downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + resume + ) + //.also { println("Single episode finished with return code: $it") } + + // retry every link at least once + if (connectionResult <= 0) { + connectionResult = downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + true + ) + } + + if (connectionResult > 0) { // SUCCESS + removeKey(KEY_RESUME_PACKAGES, id.toString()) + break + } else if (index == item.links.lastIndex) { + downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) + } + } + } catch (e: Exception) { + logError(e) + } finally { + currentDownloads.remove(id) + // Because otherwise notifications will not get caught by the work manager + downloadCheckUsingWorker(context) + } + + // return id } fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? { @@ -1500,23 +1528,21 @@ object VideoDownloadManager { return success } - private fun deleteFile(context: Context, id: Int): Boolean { - val info = - context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return false - downloadEvent.invoke(Pair(id, DownloadActionType.Stop)) - downloadProgressEvent.invoke(Triple(id, 0, 0)) - downloadStatusEvent.invoke(Pair(id, DownloadType.IsStopped)) - downloadDeleteEvent.invoke(id) - val base = basePathToFile(context, info.basePath) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) { + private fun deleteFile( + context: Context, + folder: UniFile?, + relativePath: String, + displayName: String + ): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && folder.isDownloadDir()) { val cr = context.contentResolver ?: return false val fileUri = - cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName) + cr.getExistingDownloadUriOrNullQ(relativePath, displayName) ?: return true // FILE NOT FOUND, ALREADY DELETED return cr.delete(fileUri, null, null) > 0 // IF DELETED ROWS IS OVER 0 } else { - val file = base?.gotoDir(info.relativePath)?.findFile(info.displayName) + val file = folder?.gotoDir(relativePath)?.findFile(displayName) // val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName) // val dFile = File(normalPath) if (file?.exists() != true) return true @@ -1530,6 +1556,17 @@ object VideoDownloadManager { } } + private fun deleteFile(context: Context, id: Int): Boolean { + val info = + context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return false + downloadEvent.invoke(Pair(id, DownloadActionType.Stop)) + downloadProgressEvent.invoke(Triple(id, 0, 0)) + downloadStatusEvent.invoke(id to DownloadType.IsStopped) + downloadDeleteEvent.invoke(id) + val base = basePathToFile(context, info.basePath) + return deleteFile(context, base, info.relativePath, info.displayName) + } + fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? { return context.getKey(KEY_RESUME_PACKAGES, id.toString()) } @@ -1544,10 +1581,12 @@ object VideoDownloadManager { downloadQueue.addLast(pkg) downloadCheck(context, notificationCallback) if (setKey) saveQueue() + //ret } else { - downloadEvent.invoke( + downloadEvent( Pair(pkg.item.ep.id, DownloadActionType.Resume) ) + //null } } From f571596bbc69d1fca24fb99dcef86227f03a4a80 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Sat, 19 Aug 2023 19:34:21 +0530 Subject: [PATCH 181/570] fix: expand resume watching sheet and ft: ripple on profile drawable when pressed (#566) * add ripple to profile icon on home * Update HomeParentItemAdapterPreview.kt --- .../cloudstream3/ui/home/HomeParentItemAdapterPreview.kt | 4 ++-- app/src/main/res/layout/fragment_home_head.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 943f784a..13497a99 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 @@ -539,7 +539,7 @@ class HomeParentItemAdapterPreview( resumeAdapter.updateList(resumeWatching) if (binding is FragmentHomeHeadBinding) { - binding.homeBookmarkParentItemTitle.setOnClickListener { + binding.homeWatchParentItemTitle.setOnClickListener { viewModel.popup( HomeViewModel.ExpandableHomepageList( HomePageList( @@ -578,4 +578,4 @@ class HomeParentItemAdapterPreview( } } } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/fragment_home_head.xml b/app/src/main/res/layout/fragment_home_head.xml index e13b96a8..90386ccf 100644 --- a/app/src/main/res/layout/fragment_home_head.xml +++ b/app/src/main/res/layout/fragment_home_head.xml @@ -61,7 +61,7 @@ android:layout_height="match_parent" android:layout_gravity="center" android:layout_marginStart="-50dp" - android:background="?android:attr/selectableItemBackgroundBorderless" + android:foreground="?android:attr/selectableItemBackgroundBorderless" android:contentDescription="@string/account" android:nextFocusLeft="@id/home_search" android:padding="10dp" @@ -288,4 +288,4 @@ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listitem="@layout/home_result_grid" /> - \ No newline at end of file + From b3abf1e45fbf2a532b551f9d69dbf7c674765ba7 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 17:03:27 +0200 Subject: [PATCH 182/570] fixed decryption --- .../cloudstream3/utils/M3u8Helper.kt | 24 ++++++++----------- .../utils/VideoDownloadManager.kt | 2 ++ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 1fb3a72d..5c0b45de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -88,21 +88,17 @@ object M3u8Helper2 { } } - private val defaultIvGen = sequence { - var initial = 1 + private fun defaultIv(index: Int) : ByteArray { + return toBytes16Big(index+1) + } - while (true) { - yield(toBytes16Big(initial)) - ++initial - } - }.iterator() - - fun getDecrypter( + fun getDecrypted( secretKey: ByteArray, data: ByteArray, - iv: ByteArray = "".toByteArray() + iv: ByteArray = byteArrayOf(), + index : Int, ): ByteArray { - val ivKey = if (iv.isEmpty()) defaultIvGen.next() else iv + val ivKey = if (iv.isEmpty()) defaultIv(index) else iv val c = Cipher.getInstance("AES/CBC/PKCS5Padding") val skSpec = SecretKeySpec(secretKey, "AES") val ivSpec = IvParameterSpec(ivKey) @@ -234,7 +230,7 @@ object M3u8Helper2 { if (tsData.isEmpty()) throw ErrorLoadingException("no data") return if (isEncrypted) { - getDecrypter(encryptionData, tsData, encryptionIv) + getDecrypted(encryptionData, tsData, encryptionIv, index) } else { tsData } @@ -272,13 +268,13 @@ object M3u8Helper2 { val match = ENCRYPTION_URL_IV_REGEX.find(m3u8Response)!!.groupValues - var encryptionUri = match[1] + var encryptionUri = match[2] if (isNotCompleteUrl(encryptionUri)) { encryptionUri = "${getParentLink(secondSelection.streamUrl)}/$encryptionUri" } - encryptionIv = match[2].toByteArray() + encryptionIv = match[3].toByteArray() val encryptionKeyResponse = app.get(encryptionUri, headers = headers, verify = false) encryptionData = encryptionKeyResponse.body.bytes() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index ef0d9d8a..dc3eaa25 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -803,6 +803,8 @@ object VideoDownloadManager { bytesDownloaded = 0 hlsWrittenProgress = 0 hlsProgress = 0 + if (id != null) + downloadDeleteEvent(id) //internalType = DownloadType.IsStopped notify() From 98b641714073e6bb84f74bd9ea3e19a03ed857aa Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 21:37:14 +0200 Subject: [PATCH 183/570] made downloader faster with parallel downloads --- .../ui/result/ResultViewModel2.kt | 6 +- .../utils/VideoDownloadManager.kt | 438 ++++++++++++++++-- 2 files changed, 395 insertions(+), 49 deletions(-) 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 2fe3b012..bdd27091 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 @@ -593,10 +593,8 @@ class ResultViewModel2 : ViewModel() { folder, if (link.url.contains(".srt")) ".srt" else "vtt", false, - null - ) { - // no notification - } + null, createNotificationCallback = {} + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index dc3eaa25..d8ef7e85 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -40,14 +40,20 @@ import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly import java.io.Closeable import java.io.File import java.io.IOException -import java.io.InputStream import java.io.OutputStream import java.net.URL import java.util.* @@ -710,6 +716,8 @@ object VideoDownloadManager { data class DownloadMetaData( private val id: Int?, var bytesDownloaded: Long = 0, + var bytesWritten: Long = 0, + var totalBytes: Long? = null, // notification metadata @@ -732,10 +740,21 @@ object VideoDownloadManager { val approxTotalBytes: Long get() = totalBytes ?: hlsTotal?.let { total -> (bytesDownloaded * (total / hlsProgress.toFloat())).toLong() - } ?: 0L + } ?: bytesDownloaded private val isHLS get() = hlsTotal != null + private var stopListener: (() -> Unit)? = null + + /** on cancel button pressed or failed invoke this once and only once */ + fun setOnStop(callback: (() -> Unit)) { + stopListener = callback + } + + fun removeStopListener() { + stopListener = null + } + private val downloadEventListener = { event: Pair -> if (event.first == id) { when (event.second) { @@ -747,6 +766,8 @@ object VideoDownloadManager { type = DownloadType.IsStopped removeKey(KEY_RESUME_PACKAGES, event.first.toString()) saveQueue() + stopListener?.invoke() + stopListener = null } DownloadActionType.Resume -> { @@ -783,13 +804,14 @@ object VideoDownloadManager { override fun close() { // as we may need to resume hls downloads, we save the current written index - if (isHLS) { + if (isHLS || totalBytes == null) { updateFileInfo() } if (id != null) { downloadEvent -= downloadEventListener downloadStatus -= id } + stopListener = null } var type @@ -846,6 +868,11 @@ object VideoDownloadManager { updateFileInfo() } + if (internalType == DownloadType.IsStopped || internalType == DownloadType.IsFailed) { + stopListener?.invoke() + stopListener = null + } + // push all events, this *should* not crash, TODO MUTEX? if (id != null) { downloadStatus[id] = type @@ -874,6 +901,10 @@ object VideoDownloadManager { if (type == DownloadType.IsDownloading) checkNotification() } + fun addBytesWritten(length: Long) { + bytesWritten += length + } + /** adds the length + hsl progress and pushes a notification if necessary */ fun addSegment(length: Long) { hlsProgress += 1 @@ -885,6 +916,173 @@ object VideoDownloadManager { } } + /** bytes have the size end-start where the byte range is [start,end) + * note that ByteArray is a pointer and therefore cant be stored without cloning it */ + data class LazyStreamDownloadResponse( + val bytes: ByteArray, + val startByte: Long, + val endByte: Long, + ) { + val size get() = endByte - startByte + + override fun toString(): String { + return "$startByte->$endByte" + } + + override fun equals(other: Any?): Boolean { + if (other !is LazyStreamDownloadResponse) return false + return other.startByte == startByte && other.endByte == endByte + } + + override fun hashCode(): Int { + return Objects.hash(startByte, endByte) + } + } + + data class LazyStreamDownloadData( + private val url: String, + private val headers: Map, + private val referer: String, + /** This specifies where chunck i starts and ends, + * bytes=${chuckStartByte[ i ]}-${chuckStartByte[ i+1 ] -1} + * where out of bounds => bytes=${chuckStartByte[ i ]}- */ + private val chuckStartByte: LongArray, + val totalLength: Long?, + val downloadLength: Long?, + val chuckSize: Long, + val bufferSize: Int, + ) { + val size get() = chuckStartByte.size + + /** returns what byte it has downloaded, + * so start at 10 and download 4 bytes = return 14 + * + * the range is [startByte, endByte) to be able to do [a, b) [b, c) ect + * + * [a, null) will return inclusive to eof = [a, eof] + * + * throws an error if initial get request fails, can be specified as return startByte + * */ + @Throws + private suspend fun resolve( + startByte: Long, + endByte: Long?, + callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) + ): Long = withContext(Dispatchers.IO) { + var currentByte: Long = startByte + val stopAt = endByte ?: Long.MAX_VALUE + if (currentByte >= stopAt) return@withContext currentByte + + val request = app.get( + url, + headers = headers + mapOf( + // range header is inclusive so [startByte, endByte-1] = [startByte, endByte) + // if nothing at end the server will continue until eof + "Range" to "bytes=$startByte-" // ${endByte?.minus(1)?.toString() ?: "" } + ), + referer = referer, + verify = false + ) + val requestStream = request.body.byteStream() + + val buffer = ByteArray(bufferSize) + var read: Int + + try { + while (requestStream.read(buffer, 0, bufferSize).also { read = it } >= 0) { + val start = currentByte + currentByte += read.toLong() + + // this stops overflow + if (currentByte >= stopAt) { + callback(LazyStreamDownloadResponse(buffer, start, stopAt)) + break + } else { + callback(LazyStreamDownloadResponse(buffer, start, currentByte)) + } + } + } catch (e: CancellationException) { + throw e + } catch (t: Throwable) { + logError(t) + } finally { + requestStream.closeQuietly() + } + + return@withContext currentByte + } + + /** retries the resolve n times and returns true if successful */ + suspend fun resolveSafe( + index: Int, + retries: Int = 3, + callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) + ): Boolean { + var start = chuckStartByte.getOrNull(index) ?: return false + val end = chuckStartByte.getOrNull(index + 1) + + for (i in 0 until retries) { + try { + // in case + start = resolve(start, end, callback) + // no end defined, so we don't care exactly where it ended + if (end == null) return true + // we have download more or exactly what we needed + if (start >= end) return true + } catch (e: IllegalStateException) { + return false + } catch (e: CancellationException) { + return false + } catch (t: Throwable) { + continue + } + } + return false + } + + } + + @Throws + suspend fun streamLazy( + url: String, + headers: Map, + referer: String, + startByte: Long, + /** how many bytes every connection should be, by default it is 10 MiB */ + chuckSize: Long = (1 shl 20) * 10, + /** maximum bytes in the buffer that responds */ + bufferSize: Int = DEFAULT_BUFFER_SIZE + ): LazyStreamDownloadData { + // we don't want to make a separate connection for every 1kb + require(chuckSize > 1000) + + val contentLength = + app.head(url = url, headers = headers, referer = referer, verify = false).size + + var downloadLength: Long? = null + var totalLength: Long? = null + + val ranges = if (contentLength == null) { + LongArray(1) { startByte } + } else { + downloadLength = contentLength - startByte + totalLength = contentLength + LongArray((downloadLength / chuckSize).toInt()) { idx -> + startByte + idx * chuckSize + } + } + return LazyStreamDownloadData( + url = url, + headers = headers, + referer = referer, + chuckStartByte = ranges, + downloadLength = downloadLength, + totalLength = totalLength, + chuckSize = chuckSize, + bufferSize = bufferSize + ) + } + @Throws suspend fun downloadThing( context: Context, @@ -895,6 +1093,7 @@ object VideoDownloadManager { tryResume: Boolean, parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, + parallelConnections: Int = 3 ): Int = withContext(Dispatchers.IO) { // we cant download torrents with this implementation, aria2c might be used in the future if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { @@ -902,7 +1101,7 @@ object VideoDownloadManager { } var fileStream: OutputStream? = null - var requestStream: InputStream? = null + //var requestStream: InputStream? = null val metadata = DownloadMetaData( totalBytes = 0, bytesDownloaded = 0, @@ -926,11 +1125,13 @@ object VideoDownloadManager { val fileLength = stream.fileLength ?: return@withContext ERROR_UNKNOWN val resumeAt = (if (resume) fileLength else 0) metadata.bytesDownloaded = resumeAt + metadata.bytesWritten = resumeAt metadata.type = DownloadType.IsPending - // set up a connection - val request = app.get( - link.url.replace(" ", "%20"), + val items = streamLazy( + url = link.url.replace(" ", "%20"), + referer = link.referer, + startByte = resumeAt, headers = link.headers.appendAndDontOverride( mapOf( "Accept-Encoding" to "identity", @@ -941,17 +1142,12 @@ object VideoDownloadManager { "sec-fetch-dest" to "video", "sec-fetch-user" to "?1", "sec-ch-ua-mobile" to "?0", - ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap() - ), - referer = link.referer, - verify = false + ) + ) ) - // init variables - val contentLength = request.size ?: 0 - metadata.totalBytes = contentLength + resumeAt - - // save + metadata.totalBytes = items.totalLength + metadata.type = DownloadType.IsDownloading metadata.setDownloadFileInfoTemplate( DownloadedFileInfo( totalBytes = metadata.approxTotalBytes, @@ -961,23 +1157,166 @@ object VideoDownloadManager { ) ) - // total length is less than 5mb, that is too short and something has gone wrong - if (extension == "mp4" && metadata.approxTotalBytes < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION + val currentMutex = Mutex() + val current = (0 until items.size).iterator() - // read the buffer into the filestream, this is equivalent of transferTo - requestStream = request.body.byteStream() - metadata.type = DownloadType.IsDownloading + val fileMutex = Mutex() + // start to data + val pendingData: HashMap = + hashMapOf() - val buffer = ByteArray(DEFAULT_BUFFER_SIZE) - var read: Int - while (requestStream.read(buffer, 0, DEFAULT_BUFFER_SIZE).also { read = it } >= 0) { - fileStream.write(buffer, 0, read) + val jobs = (0 until parallelConnections).map { + launch { - // wait until not paused - while (metadata.type == DownloadType.IsPaused) delay(100) - // if stopped then break to delete - if (metadata.type == DownloadType.IsStopped) break - metadata.addBytes(read.toLong()) + // this may seem a bit complex but it more or less acts as a queue system + // imagine we do the downloading [0,3] and it response in the order 0,2,3,1 + // file: [_,_,_,_] queue: [_,_,_,_] Initial condition + // file: [X,_,_,_] queue: [_,_,_,_] + added 0 directly to file + // file: [X,_,_,_] queue: [_,_,X,_] + added 2 to queue + // file: [X,_,_,_] queue: [_,_,X,X] + added 3 to queue + // file: [X,X,_,_] queue: [_,_,X,X] + added 1 directly to file + // file: [X,X,X,X] queue: [_,_,_,_] write the queue and remove from it + + val callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) = + callback@{ response -> + if (!isActive) return@callback + fileMutex.withLock { + // wait until not paused + while (metadata.type == DownloadType.IsPaused) delay(100) + // if stopped then throw + if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed) { + this.cancel() + return@callback + } + + val responseSize = response.size + metadata.addBytes(response.size) + + if (response.startByte == metadata.bytesWritten) { + // if we are first in the queue then write it directly + fileStream.write( + response.bytes, + 0, + responseSize.toInt() + ) + metadata.addBytesWritten(responseSize) + } else { + // otherwise append to queue, we need to clone the bytes as they will be overridden otherwise + pendingData[response.startByte] = + response.copy(bytes = response.bytes.clone()) + } + + while (true) { + // remove the current queue start, so no possibility of + // while(true) { continue } in case size = 0, and removed extra + // garbage + val pending = pendingData.remove(metadata.bytesWritten) ?: break + + val size = pending.size + + fileStream.write( + pending.bytes, + 0, + size.toInt() + ) + metadata.addBytesWritten(size) + } + } + } + + // this will take up the first available job and resolve + while (true) { + if (!isActive) return@launch + fileMutex.withLock { + if (metadata.type == DownloadType.IsStopped) return@launch + } + + // just in case, we never want this to fail due to multithreading + val index = currentMutex.withLock { + if (!current.hasNext()) return@launch + current.nextInt() + } + + // in case something has gone wrong set to failed if the fail is not caused by + // user cancellation + if (!items.resolveSafe(index, callback = callback)) { + fileMutex.withLock { + if (metadata.type != DownloadType.IsStopped) { + metadata.type = DownloadType.IsFailed + } + } + return@launch + } + } + } + } + + // fast stop as the jobs may be in a slow request + metadata.setOnStop { + jobs.forEach { job -> + try { + job.cancel() + } catch (t: Throwable) { + logError(t) + } + } + } + + jobs.forEach { it.join() } + + // jobs are finished so we don't want to stop them anymore + metadata.removeStopListener() + + // set up a connection + //val request = app.get( + // link.url.replace(" ", "%20"), + // headers = link.headers.appendAndDontOverride( + // mapOf( + // "Accept-Encoding" to "identity", + // "accept" to "*/*", + // "user-agent" to USER_AGENT, + // "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", + // "sec-fetch-mode" to "navigate", + // "sec-fetch-dest" to "video", + // "sec-fetch-user" to "?1", + // "sec-ch-ua-mobile" to "?0", + // ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap() + // ), + // referer = link.referer, + // verify = false + //) + + // init variables + //val contentLength = request.size ?: 0 + //metadata.totalBytes = contentLength + resumeAt + //// save + //metadata.setDownloadFileInfoTemplate( + // DownloadedFileInfo( + // totalBytes = metadata.approxTotalBytes, + // relativePath = relativePath ?: "", + // displayName = displayName, + // basePath = basePath + // ) + //) + //// total length is less than 5mb, that is too short and something has gone wrong + //if (extension == "mp4" && metadata.approxTotalBytes < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION + //// read the buffer into the filestream, this is equivalent of transferTo + //requestStream = request.body.byteStream() + //metadata.type = DownloadType.IsDownloading + //val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + //var read: Int + //while (requestStream.read(buffer, 0, DEFAULT_BUFFER_SIZE).also { read = it } >= 0) { + // fileStream.write(buffer, 0, read) + // // wait until not paused + // while (metadata.type == DownloadType.IsPaused) delay(100) + // // if stopped then break to delete + // if (metadata.type == DownloadType.IsStopped) break + // metadata.addBytes(read.toLong()) + //} + + + if (metadata.type == DownloadType.IsFailed) { + return@withContext ERROR_CONNECTION_ERROR } if (metadata.type == DownloadType.IsStopped) { @@ -1003,11 +1342,12 @@ object VideoDownloadManager { // note that when failing we don't want to delete the file, // only user interaction has that power + metadata.removeStopListener() metadata.type = DownloadType.IsFailed return@withContext ERROR_CONNECTION_ERROR } finally { fileStream?.closeQuietly() - requestStream?.closeQuietly() + //requestStream?.closeQuietly() metadata.close() } } @@ -1388,20 +1728,28 @@ object VideoDownloadManager { } return suspendSafeApiCall { - downloadThing(context, link, name, folder, "mp4", tryResume, ep.id) { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback - ) - } - } + downloadThing( + context, + link, + name, + folder, + "mp4", + tryResume, + ep.id, + createNotificationCallback = { meta -> + main { + createNotification( + context, + source, + link.name, + ep, + meta.type, + meta.bytesDownloaded, + meta.bytesTotal, + notificationCallback + ) + } + }) }.also { extractorJob.cancel() } ?: ERROR_UNKNOWN } From 61ca0a56bea81ef7bd18f943006df56ff4b8e707 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sat, 19 Aug 2023 21:38:29 +0200 Subject: [PATCH 184/570] Translations update from Hosted Weblate (#546) Co-authored-by: Alexandru Co-authored-by: Alexthegib Co-authored-by: Alexthegib Co-authored-by: Amir Co-authored-by: Astrid Co-authored-by: Carlos Luiz Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> Co-authored-by: Danilo Co-authored-by: Eryk Michalak Co-authored-by: Ettore Atalan Co-authored-by: Fjuro Co-authored-by: Htet Oo Hlaing Co-authored-by: Imprevisible Co-authored-by: Jan Haider Co-authored-by: Jimuel Mallari Co-authored-by: Massimo Pissarello Co-authored-by: Milo Ivir Co-authored-by: PiterDev Co-authored-by: Rex_sa Co-authored-by: Reza Almanda Co-authored-by: Rudy Tantono Co-authored-by: Skrripy Co-authored-by: dabao1955 Co-authored-by: gallegonovato Co-authored-by: george kitsoukakis Co-authored-by: infoekcz Co-authored-by: tuan041 --- app/src/main/res/values-ar/strings.xml | 3 +- app/src/main/res/values-bp/strings.xml | 79 ++- app/src/main/res/values-cs/strings.xml | 3 +- app/src/main/res/values-es/strings.xml | 3 +- app/src/main/res/values-fil/strings.xml | 2 + app/src/main/res/values-fr/strings.xml | 24 +- app/src/main/res/values-in/strings.xml | 4 +- app/src/main/res/values-it/strings.xml | 3 +- app/src/main/res/values-my/strings.xml | 556 ++++++++++++++++++ app/src/main/res/values-nl/strings.xml | 5 +- app/src/main/res/values-pl/strings.xml | 5 +- app/src/main/res/values-pt/strings.xml | 3 +- app/src/main/res/values-qt/strings.xml | 3 +- app/src/main/res/values-uk/strings.xml | 37 +- app/src/main/res/values-vi/strings.xml | 15 +- fastlane/metadata/android/pt/changelogs/2.txt | 1 + .../metadata/android/pt/full_description.txt | 10 + .../metadata/android/pt/short_description.txt | 1 + fastlane/metadata/android/pt/title.txt | 1 + fastlane/metadata/android/vi/changelogs/2.txt | 1 + .../metadata/android/vi/full_description.txt | 10 + .../metadata/android/vi/short_description.txt | 1 + fastlane/metadata/android/vi/title.txt | 1 + 23 files changed, 734 insertions(+), 37 deletions(-) create mode 100644 app/src/main/res/values-fil/strings.xml create mode 100644 app/src/main/res/values-my/strings.xml create mode 100644 fastlane/metadata/android/pt/changelogs/2.txt create mode 100644 fastlane/metadata/android/pt/full_description.txt create mode 100644 fastlane/metadata/android/pt/short_description.txt create mode 100644 fastlane/metadata/android/pt/title.txt create mode 100644 fastlane/metadata/android/vi/changelogs/2.txt create mode 100644 fastlane/metadata/android/vi/full_description.txt create mode 100644 fastlane/metadata/android/vi/short_description.txt create mode 100644 fastlane/metadata/android/vi/title.txt diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 9b440e6f..987211a5 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -584,4 +584,5 @@ @string/default_subtitles لا توجد اضافة في المستودع المستودع لم يتم العثور عليه، تحقق من العنوان اوجرب شبكة افتراضية خاصة(vpn) - + لقد صوتت بالفعل + \ 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 38424e56..2a3bdb27 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 + Pôster Episode Poster Main Poster Next Random @@ -66,7 +66,7 @@ Erro Carregando Links Armazenamento Interno Dub - Leg + Sub Deletar Arquivo Assistir Arquivo Retomar Download @@ -257,7 +257,7 @@ Não mostrar de novo Pular essa Atualização Atualizar - Qualidade preferida + Qualidade preferida de reprodução (Wi-fi) Máximo de caracteres do título de vídeos Resolução do player de vídeo Tamanho do buffer do vídeo @@ -428,4 +428,75 @@ Começa o próximo episódio quando o atual termina Ativar NSFW em fornecedores compatíveis Fornecedores - + Reverter + Ações + votou com sucesso + Baixando atualização do aplicativo… + Referencias + Atualizações do App + Tocar com CloudStream + Automaticamente instale todos os plugins não instalados dos repositórios adicionados. + Reproduzir Trailer + Navegador + Copia de Segurança + A Barra de Progresso pode ser usada quando o player estiver oculto + Inscrever + Essa lista está vazia. Tente mudar para outra. + Reproduzir Livestream + Log do Teste + Baixa plugins automaticamente + Selecione o modo para filtrar os plugins baixados + Teste falhou + A Barra de Progresso pode ser usada quando o player estiver visível + Organizar + Sim + Você tem certeza que deseja sair\? + Instalando atualização do aplicativo… + Editar + Perfis + Exibindo Player - procure na Barra de Progresso + remover dos assitidos + Extensões + Alfabética(A => Z) + Abrir com + Selecionar Biblioteca + Passou + Sua biblioteca está vazia :0 +\nEntre numa conta de biblioteca ou adicione Midias para sua biblioteca local. + Qualidade preferida de reprodução (Dados Móveis) + Legado + Biblioteca + Não + Trilhas Sonoras + Votação(Baixa para Alta) + Atualização iniciada + Conteúdo +18 + Ajuda + Processo de configuração de Redo + Não pudemos instalar a nova versão do App + instalador de pacotes + Organizar por + Votação(Alta para Baixa) + Alfabética(Z => A) + Qualidade + Perfil de plano de fundo + Aqui você pode alterar como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais alta, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte e da prioridade da qualidade é a prioridade do vídeo. +\n +\nFonte A: 3 +\nQualidade B: 7 +\nTerá uma prioridade de vídeo combinada de 10. +\n +\nNOTA: Se a soma for 10 ou mais, o Player pulará automaticamente o carregamento quando o link for carregado! + Arquivo de modo de segurança encontrado! +\nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido. + Inscrevel em %d + Episódio %d Lançado + Selecionar padrão + Disinscrevel em %d + Alguns aparelhos não possuem suporte para este pacote de instalação. Tente a opção legada se a atualização não instalar. + Dados móveis + Perfil %d + Atualizando shows inscritos + Player oculto - Procure na barra de progresso + Conteúdo +18 + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index f304199e..165dbbb4 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -576,4 +576,5 @@ V repozitáři nebyly nalezeny žádné doplňky Repozitář nenalezen, zkontrolujte adresu URL a zkuste použít VPN @string/default_subtitles - + Již jste hlasovali + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 42e07c90..6326211e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -552,4 +552,5 @@ @string/default_subtitles No se encontraron complementos en el repositorio Repositorio no encontrado, comprueba la URL y prueba la VPN - + Ya has votado + \ No newline at end of file diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-fil/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 36c1cf1f..4cc56207 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -530,4 +530,26 @@ Joueur représenté - Montant de la recherche Joueur caché - Montant de la recherche Impossible d\'accéder à GitHub. Activation du proxy jsDelivr… - + Vous avez déjà voté + Désactivé + Ici, vous pouvez modifier la façon dont les sources sont ordonnées. Si une vidéo a une priorité plus élevée, elle apparaîtra plus haut dans la sélection de la source. La somme de la priorité source et de la priorité qualité est la priorité vidéo. +\n +\nSource A : 3 +\nQualité B : 7 +\nLa priorité vidéo combinée sera de 10. +\n +\nREMARQUE : Si la somme est de 10 ou plus, le joueur sautera automatiquement le chargement lorsque ce lien est chargé ! + Aucun plugin trouvé dans ce dossier + Dossier non trouvé, vérifiez l\'url et essayé un VPN + Données mobiles + Définir par défaut + Utiliser + Modifier + Profils + Aide + Profil %d + Wi-Fi + Qualités + L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s + Sélectionnez le mode pour filtrer le téléchargement des plugins + \ 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 2bd86090..87a01ff9 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -574,4 +574,6 @@ Pilih mode untuk memfilter unduhan plugin Tidak ada plugin yang ditemukan di repositori Repositori tidak ditemukan, periksa URL dan coba VPN - + Kamu sudah voting + \@string/default_subtitles + \ 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 dddc57c4..7cca78ca 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -574,4 +574,5 @@ Seleziona la modalità per filtrare il download dei plugin @string/default_subtitles Disabilita - + Hai già votato + \ No newline at end of file diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml new file mode 100644 index 00000000..cde13ba5 --- /dev/null +++ b/app/src/main/res/values-my/strings.xml @@ -0,0 +1,556 @@ + + + သရုပ်ဆောင်များ: %s + %dရက် %ddနာရီ %ddမိနစ် + %ddနာရီ %ddမိနစ် + %ddမိနစ် + ပိုစတာ + အပိုင်း ပိုစတာ + မိန်း ပိုစတာ + နောက် ကျပန်း + နောက်သို့ + နောက်ခံပုံရိပ်ကို အကြိုကြည့်ရန် + အဆင့်: %.1f + အပ်ဒိတ်အသစ်! +\n%s -> %s + စစ်ထုတ်မှု + %d မိနစ် + CloudStream + CloudStream ဖြင့်ကြည့်ရန် + ပင်မ + ရှာရန် + ရှာရန်… + ရှာရန် %s… + အချက်အလက်မရှိပါ + အခြားရွေးစရာများ + နောက်အပိုင်း + ကဏ္ဍများ + မျှဝေမည် + ဘရောက်ဇာတွင်ဖွင့်ရန် + ဘရောက်ဇာ + မစောင့်တော့ပါ + ကြည့်နေသည် + ကြည့်ပြီး + ကြည့်ခြင်းရပ်ထားသော + ဘာမျှ + လင့်များချိတ်ဆက်ရာတွင်အချို့အယွင်း + ဖုန်း သိုလှောင်ရုံ + စာမှတ်များ စစ်ထုတ်မှု + စာမှတ်များ + ဖယ်ရှားရန် + ကြည့်ရှုမှုအခြေအနေသတ်မှတ်ခြင်း + မိတ္တူကူးရန် + ပိတ်ရန် + ရှင်းလင်းရန် + သိမ်းဆည်းရန် + ကြည့်ရှုမှုအရှိန် + နောက်ခံ အရောင် + ဝင်းဒိုး အရောင် + အစွန်းနားပုံစံ + စာတန်းထိုး အမြင့် + ဖောင့် + ဖောင့် အရွယ်အစား + အမျိုးအစားများအသုံးပြု၍ရှာရန် + %d အက်ပ်ဖန်တီးသူတွေကိုကျေးဇူးတင်ကြောင်းပို့မည် + အလိုအလျောက် ဘာသာစကားရွေးချယ်ခြင်း + ဒေါင်းလုဒ် လုပ်ထားသော ဘာသာစကားများ + မူရင်းပုံစံအတိုင်းပြန်ထားရန်ဖိထားပါ + ဆက်လက်ကြည့်ရှုမည် + ဖယ်ရှားရန် + ပိုမို၍ + \@string/home_play + ဒီဟာကတောရပ်တစ်ခုပါ ဗီပီအန်တစ်ခုသုံးဖို့အကြံပြုပါတယ် + ဖော်ပြချက် + ဇာတ်လမ်းသွား မတွေ့ပါ + ဖော်ပြချက် မရိှပါ + Logcat ပြရန် 🐈 + Log + ရုပ်ပုံထပ် + ကြည့်ရှုမှု စခရင်အရွယ်အစားချိန်ညိှမှု + စာတန်းထိုးများ + ကြည့်ရှုမှုစာတန်းထိုးပြုပြင်စရာများ + Eigengravy လုပ်ဆောင်မှု + ရစ်ရန်ဘယ်ညာဆွဲပါ + သင်ရောက်နေတဲ့နေရာပြောင်းရန်ဘယ်ညာဆွဲပါ + ပြုပြင်စရာရိှပါက ပွတ်ဆွဲပါ + နောက်အပိုင်းကို အလိုအလျောက် ဖွင့်ပါ + ကျော်ရန်နှစ်ချက်နှိပ်ပါ + ရပ်ရန်နှစ်ချက်နှိပ်ပါ + ကျော်လိုသောပမာဏ (စက္ကန့်များ) + ရှေ့သို့ကျော်ရန် သို့ နောက်သို့ရစ်ရန် ဘယ် သို့ ညာ ပေါ်မှာနှစ်ချက်နှိပ်ပါ + ရပ်ရန် အလယ်တွင်နှစ်ချက်နှိပ်ပါ + ဖုန်းအလင်းအမှောင်အတိုင်းသုံးမည် + အက်ပ်ကြည့်ရှုမှုထဲမှာ ဖုန်းအလင်းအမှောင်အတိုင်းသုံးမည် + ကြည့်ရှုမှုတိုးတက်ခြင်းကိုအပ်ဒိတ်လုပ်ပါ + အရန်သိမ်းဖိုင်မှပြန်သိုလှောင်မည် + အရန်သိမ်းမည် + အရန်သိမ်းဖိုင်များရယူပြီး + အရန်သိမ်းဖိုင်မှပြန်သိုလှောငးခြင်မအောင်မြင်ပါ %s + သိုလှောင်ပြီး + သိုလှောင်ရုံခွင့်ပြုချက်မရိှပါ။ပြန်ကြိုးစားပါ။ + အရန်သိမ်းနေစဥ်အချို့အယွင်း %s + လိုက်ဘရီ + အပ်ဒိတ်များနှင့်အရန်သိမ်းဆည်းမှု + နက်နက်ရှိုင်းရှိုင်းရှာခြင်း + သင့်ကိုဝန်ဆောင်မှုပေးသူအလိုက်ရှာဖွေမှုရလဒ်များပေးမည် + ချို့ယွင်းမှုအကြီးစားဖြစ်မှသာဒေတာများပေးပို့ပါ + anime များအတွက်ဖြည့်စွက်အပိုင်းကိုပြရန် + ထွေလာများကိုပြရန် + Kitsu မှ ပိုစတာများကိုပြရန် + အလိုအလျောက် ဖြည့်စွက်လုပ်ဆောင်ချက်များကိုအပ်ဒိတ်တင်ခြင်း + အပိုလုပ်ဆောင်ချက်များကိုစစ်ထုတ်ရန်မုဒ်ရွေးပါ + အက်ပ်အပ်ဒိတ်များပြရန် + အစီအစဥ်ချခြင်းကိုပြန်စမည် + အက်ပ်ထည့်သွင်းခြင်း + အချို့ဖုန်းတွေက အက်ပ်ထည့်သွင်းခြင်းလုပ်ဆောင်ချက်အသစ်ကို မပံ့ပိုးပါဘူး။အကယ်၍အလုပ်မဖြစ်ပါကသမားရိုးကျနည်းလမ်းကိုအသုံးပြုပါ။ + Github + ဤဝန်ဆောင်မှုပေးသူသည် Chromecast ကိုမပံ့ပိုးပါ + လင့်များမတွေ့ပါ + ကလစ်ဘုတ်သို့မိတ္တူကူးပြီး + အပိုင်းကြည့်မည် + အတွဲ + အတွဲမရှိပါ + အပိုင်း + အပိုင်းများ + %d-%d + ဒါကအပြီးဖျက်ခြင်းဖြစ်ပါသည် %s +\nသင်သေချာပါသလား။ + %dမိနစ် +\nကျန်ရိှသည် + ထုတ်လွှင့်နေဆဲ + ထုတ်လွှင့်မှုပြီးဆုံး + အခြေအနေ + ခုနစ် + အဆင့်သတ်မှတ်ချက် + ကြာချိန် + ဆိုဒ် + အကျဥ်းချုပ် + နောက်အစီအစဥ် + စာတန်းထိုးမထည့် + ပုံသေ + \@string/default_subtitles + ကျန်ရှိသော + အက်ပ် + ရုပ်ရှင်များ + ဇာတ်လမ်းတွဲများ + ကာတွန်းများ + Anime + ေတာရပ်များ + မှတ်တမ်းရုပ်ရှင်များ + OVA + အာရှ ဒရာမာများ + တိုက်ရိုက်ထုတ်လွှင်မှုများ + အပြာဗီဒီယိုများ + အခြား + ရုပ်ရှင် + ဇာတ်လမ်းတွဲ + OVA + တောရပ် + မှတ်တမ်းရုပ်ရှင် + အာရှ ဒရာမာ + တိုက်ရိုက်ထုတ်လွှင့်မှု + အပြာဗီဒီယို + ဗီဒီယို + ရင်းမြစ်အချို့အယွင်း + အဝေးထိန်းချုပ်မှုအချို့အယွင်း + တင်ဆက်သူ အချို့အယွင်း + မျှော်လင့်မထားသော အချို့အယွင်း + Chromecast အပိုင်း + Chromecast ဖန်သားပြင် + အက်ပ်တွင်းဖွင့် + ဖွင့်ရန် %s + ဘရောက်ဇာထဲမှာ ဖွင့်ရန် + လင့်ကူးယူရန် + အလိုအလျောက်ဒေါင်းလုဒ် + လင့်များကို ပြန်စစ်ရန် + အရည်အသွေး အမှတ်အသား + နောက်ခံအသံ အမှတ်အသား + စာတန်း အမှတ်အသား + ခေါင်းစဥ် + အပ်ဒိတ်မရှိပါ + အပ်ဒိတ်စစ်ရန် + လော့ခ်ခတ်ရန် + ရင်းမြစ် + OPကိုကျော်ရန် + ဒီအပ်ဒိတ်ကိုကျော်ပါ + အပ်ဒိတ် + ခေါင်းစဥ်အတွက်စာလုံးရေပြည့်ခြင်း + ကြည့်ရှုမှု အရည်အသွေး + ဗီဒီယိုရှေ့ပြေးသိမ်းဆည်းမှုပမာဏ + ကြည့်ရှုမှုဘားမြင်တွေ့ရချိန်တွင်ပြသသောပမာဏ + ဝှက်ထားသောကြည့်ရှုပြီးသောပမာဏ + ဝှက်ထားသည့်အခါ အသုံးပြုသည့် ရှာဖွေမှုပမာဏ + Android TV ကဲ့သို့သော မမ်မိုရီနည်းသော စက်ပစ္စည်းများတွင် သတ်မှတ်နှုန်း အလွန်မြင့်မားပါက ပျက်စီးမှုများ ဖြစ်စေသည်။ + DNS over HTTPS + raw.githubusercontent.com ပရောက်စီ + GitHub သို့ မရောက်ရှိနိုင်ပါ။ jsDelivr ပရောက်စီကို ဖွင့်နေသည်… + ကလုန်း ဆိုဒ် + ဆိုဒ်ကိုဖယ်ရှားရန် + မတူညီသော URL တစ်ခုဖြင့် ရှိပြီးသား ဝဘ်ဆိုက်တစ်ခု၏ ပုံတူတစ်ခုကို ထည့်ပါ + ဒေါင်းလုဒ်လမ်းကြောင်း + NGINX ဆာဗာ URL + Dubbed/Subbed Anime ကိုပြသပါ + မျက်နှာပြင်နှင့် အံကိုက် + ဆန့်သည် + ချဲ့သည် + ရှင်းလင်းချက် + ISP ရှောင်လွှဲမှုများ + လင့်များ + အက်ပ်အပ်ဒိတ်များ + Extensions + ဆောင်ရွက်ချက်များ + Cache + Android တီဗွီ + စာတန်းထိုးများ + ပုံသေများ + ပုံပန်းသဏ္ဌာန် + အထွေထွေ + ပင်မစာမျက်နှာမှာကျပန်းခလုတ်ကိုပြပါ + %s အပိုင်း %d + အပိုင်း %d ထုတ်လွှင့်ပြသမည် + ပိုစတာ + ပံ့ပိုးပေးသောဝန်ဆောင်မှုပြောင်းရန် + အရှိန် (%.2fx) + ဒေါင်းလုဒ်များ + ပြင်ဆင်ရန် + ခဏစောင့်ပါ… + ကြည့်ဆဲ + ကြည့်ရန် + ပြန်ကြည့်နေသည် + စာတန်းထိုး + ရုပ်ရှင်ကြည့်မည် + ထွေလာ ကြည့်မည် + လိုက်ခ် ကြည့်မည် + တောရပ် ကြည့်မည် + ရင်းမြစ်များ + ချိတ်ဆက်မှုပြန်ကြိုးစား… + နောက်သို့ + အပိုင်း ကြည့်မည် + ဒေါင်းလုဒ် + ဒေါင်းလုဒ် လုပ်ပြီး + ဒေါင်းလုဒ် လုပ်နေသည် + ဒေါင်းလုဒ် ရပ်ထား + ဒေါင်းလုဒ်စတင် + ဒေါင်းလုဒ် မအောင်မြင် + ဒေါင်းလုဒ် ပယ်ဖျက်ပြီး + ဒေါင်းလုဒ်ပြီးစီး + အပ်ဒိတ်စတင် + တိုက်ရိုက်ကြည့်မည် + နောက်ခံအသံ + ရှာဖွေမှုရလဒ်များတွင်ရွေးချယ်ထားသောဗီဒီယိုအရည်အသွေးကိုဝှက်ထားရန် + စာတန်းထိုး + ဖိုင်ဖျက်ရန် + ဖိုင်ကို ဖွင့်ရန် + ဒေါင်းလုဒ် ဆက်လုပ်ရန် + ဒေါင်းလုဒ် ရပ်ရန် + အလိုအလျောက်အက်ပ်ချို့ယွင်းချက်ပေးပို့ခြင်းကိုပိတ်မည် + ပိုမို၍ + ပ့ံပိုးပေးသောဝန်ဆောင်မှုများအသုံးပြု၍ရှာရန် + ဝုက်ရန် + ကျေးဇူးတင်ကြောင်းမပို့ရသေး + ကြည့်မည် + အချက်အလက် + စာတန်းထိုး ဘာသာစကား + ဒီမှာနေရာချခြင်းဖြင့်ဖောင့်များကိုသွင်းပါ %s + အတည်ပြု + ပယ်ဖျက်ရန် + စာတန်းထိုး ပြုပြင်ခြင်း + စာသား အရောင် + အနားကွပ် အရောင် + ဒီပံ့ပိုးမှုကောင်းမွန်စွာအလုပ်လုပ်ရန်ဗီပီအန်တစ်ခုလိုနိုင်ပါတယ် + အသေးစိတ်အချက်အလက်များပြမထားပါ။ဝဘ်ဆိုဒ်ပေါ်မှာမရှိလျှင်ကြည့်ရှု၍မရနိုင်ပါ။ + ဖြည့်စွက်လုပ်ဆောင်ချက်များကို အလိုအလျောက်ဒေါင်းလုဒ်လုပ်ခြင်း + ရီပိုစစ်ထရီများမှမထည့်သွင်းရသေးသောဖြည့်စွက်လုပ်ဆောင်ချက်များအားလုံးကိုထည့်သွင်းပါ။ + ပြန်ကြည့်ခြင်းကိုအသေးစား ကြည့်ရှုမှုတွင်ဆက်ပြပါ + အနက်ရောင်ဘောင်များကို ဖယ်ရှားရန် + အက်ပ်ထဲဝင်လိုက်သည့်နှင့်အက်ပ်အပ်ဒိတ်ကိုစစ်ဆေးပါ။ + Chromecast စာတန်းထိုးများ + Chromecast စာတန်းထိုး ပြုပြင်ရန် + ကြည့်ရှုမှုပုံစံထဲမှာအရိှန်ရွေးစရာတစ်ခုထည့်ရန် + အသံအတိုးအကျယ်နှင့်အလင်းအမှောင်များကိုချိန်ညိှရန် ဘယ် သို့ ညာ ဘက်တွင် အပေါ်အောက်ဆွဲပါ + ယခုကြည့်နေသောအပိုင်းပြီးပါကနောက်အပိုင်းကိုဖွင့်ပါ + သင့်၏အပိုင်းကြည်ရှုမှုရောက်ရှိနေရာကိုအလိုအလျောက်သိမ်းဆည်းပါ + ရှာရန် + အကောင့်များ + အချက်အလက် + ဒေတာများမပို့ရန် + ကြည့်ရှုပြီးသောအချိန်ပမာဏ + Android TV ကဲ့သို့သော သိုလှောင်မှုနေရာနည်းပါးသော စက်ပစ္စည်းများတွင် အလွန်မြင့်မားစွာ သတ်မှတ်ပါက ပြဿနာများ ဖြစ်လာနိုင်သည်။ + ISP ပိတ်ဆို့ခြင်းကို ကျော်လွှားရန်အတွက် အသုံးဝင်သည် + jsDelivr ကို အသုံးပြု၍ GitHub ပိတ်ဆို့ခြင်းကို ကျော်ဖြတ်သည်။ အပ်ဒိတ်များကို ရက်အနည်းငယ်ကြာအောင် နှောင့်နှေးစေနိုင်သည်။ + အရန်သိမ်းထားသော + လက်ဟန်များ + ကြည့်ရှုမှုလုပ်ဆောင်ချက်များ + အပြင်အဆင် + လုပ်ဆောင်ချက်များ + ကျပန်းခလုတ် + ရှေ့ပြေးအပ်ဒိတ်များကိုထည့်သွင်းပါ + ပုံမှန်အပ်ဒိတ်များအစား ရှေ့ပြေးအက်ဒိတ်များကိုရှာပါ + တူညီသောအက်ပ်ရေးသားသူများ၏ ဝတ္ထုရှည်များဖတ်နိုင်သည့် အက်ပ် + တူညီသောအက်ပ်ရေးသားသူများ၏ Anime အက်ပ် + Discord ကိုဝင်ရန် + အက်ပ်ရေးသားသူများထံ ကျေးဇူးတင်စာပို့မည် + ပေးခဲ့သောစာအရေအတွက် + အက်ပ်ဘာသာစကား + မူလအခြေအနေများကိုပြန်ထားပါ + စိတ်မကောင်းပါ။အက်ပ်ရပ်တန့်သွားပါတယ်။အမည်မဖော်ထားတဲ့တင်ပြချက်ကို အက်ပ်ရေးသားသူများထံ ပို့မှာဖြစ်ပါတယ် + %s %d%s + အတွဲ + %d %s + အပိုင်း + အပိုင်းများမတွေ့ပါ + ဖိုင်ကိုဖျက်ရန် + ဖျက်ရန် + ရပ်ရန် + စရန် + မအောင်မြင်ပါ + ကျော်ဖြတ်ပြီး + ကြည့်လက်စ + -30 + +30 + အသုံးပြုပြီးသော + ကာတွန်း + Anime + ဒေါင်းလုဒ် အချို့အယွင်း၊သိုလှောင်ရုံခွင့်ပြုချက်တွေကိုစစ်ဆေးပါ + ဒေါင်းလုဒ် ကြေးမုံ + စာတန်းထိုးများကို ဒေါင်းလုဒ်လုပ်ရန် + ပိုစတာပေါ်ရှိ UI အစိတ်အပိုင်းများကို ပြောင်းပါ + ပြန်ညိှ + နောက်ထပ်မပြရန် + ဝိုင်ဖိုင်ဖြင့်ကြည့်စဥ်ဗီဒီယိုအရည်အသွေး + မိုဘိုင်းဒေတာဖြင့်ကြည့်စဥ်ဗီဒီယိုအရည်အသွေး + ဗီဒီယိုရှေ့ပြေးသိမ်းဆည်းမှုအကွာအဝေး + ဗီဒီယိုcacheအများ + ဗီဒီယို cache နှင့် ရုပ်ပုံ cache များကိူရှင်းလင်းရန် + ပံ့ပိုးပေးထားသည့် ဝန်ဆောင်မှုများပေါ်တွင် အပြာဗီဒီယို ကို ဖွင့်ပါ + ဝန်ဆောင်မှုပံ့ပိုးသူဘာသာစကား + အက်ပ်အပြင်အဆင် + ဦးစားပေးမီဒီယာ + အက်ပ် အပြင်အဆင် + ပိုစတာခေါင်းစဉ်တည်နေရာ + ခေါင်းစဉ်ကို ပိုစတာအောက်မှာ ထားပါ + ဖုန်းအပြင်အဆင် + အင်မြူလိတ်တာ အပြင်အဆင် + အဓိကအရောင် + အကြံပြုသည် + ဒီဗွီဒီ + 4K + ထုတ်လွှင့်ရန် လင့်ခ် နှင့်ချိတ်ပါ + ရည်ညွှန်းသည် + ရှေ့သို့ + နောက်သို့ + အပ်ဒိတ်လုပ်ပြီး %d ဖြည့်စွက်များ + ဒေါင်းလုဒ်မလုပ်ရသေး: %d + ဝဘ်ဘရောက်ဇာ + အက်ပ်မတွေ့ပါ + ဘာသာစကားအားလုံး + ကျော်ရန် %s + အစမှပြန်စ + ရောထားသောအဆုံးပိုင်း + ရောထားသောအစပိုင်း + ခရက်ဒစ်များ + အစ + သေချာသည် + သမားရိုးကျ + ထည့်သွင်းသူ + ထွက်ချိန်တွင် အက်ပ်ကို အပ်ဒိတ်လုပ်ပါမည် + CloudStream တွင် မူရင်းအတိုင်း ထည့်သွင်းထားသည့်ဆိုက်များ မရှိပါ။ ရီပိုစစ်ထရီများ မှ ဆိုဒ် များကို ထည့်သွင်းရန်လိုအပ်သည်။ +\n +\nSky UK Limited မှ ဦးနှောက်မဲ့ DMCA ကို ဖယ်ရှားလိုက်ခြင်းကြောင့် 🤮 ကျွန်ုပ်တို့သည် ရီပိုစစ်ထရီဆိုဒ်ကို အက်ပ်တွင် ချိတ်ဆက်၍မရပါ။ +\n +\nကျွန်ုပ်တို့၏ Discord တွင်ပါဝင်ပါ သို့မဟုတ် အွန်လိုင်းတွင်ရှာဖွေပါ။ + အခြားသူများ၏ရီပိုစစ်ထရီများကိုရှာဖွေမည် + အသံများ + အသံဖိုင်များ + ဗီဒီယိုအသံဖိုင်များ + ပြန်စတင်ချိန်မှာအသုံးပြုပါ + ရပ်ရန် + လုံခြုံသောမုဖွင့်ရန် + ဖုန်းတွင်းကြည့်ရှုမှု + ဦးစားပေး ဗီဒီယိုဖွင့်စက် + ပြဿနာဖြစ်စေသည့်အရာကို သင်ရှာဖွေရာတွင် အထောက်အကူဖြစ်စေရန်အတွက် ပျက်စီးမှုတစ်ခုကြောင့် အဆက်များအားလုံးကို ပိတ်ထားသည်။ + အဆင့်သတ်မှတ်ချက်များ: %s + ပျက်စီးမှုအချက်အလက်ကို ကြည့်ပါ + ဖော်ပြချက် + ဗားရှင်း + အခြေအနေ + အရွယ်အစား + ရေးသားသူများ + HLS ဖွင့်စဥ် + ကြည့်ရှုခဲ့သည်များ + အက်ပ်၏ ဗားရှင်းအသစ်ကို ထည့်သွင်း၍မရပါ + အစပိုင်း/အဆုံးပိုင်းအတွက် ကျော်နိုင်သော ပေါ့ပ်အပ်များကို ပြပါ + စာသားအလွန်များသဖြင့်ကလစ်ဘုတ်တွင် သိမ်းဆည်း၍မရပါ။ + ပံ့ပိုးပေးသူများ + အပြင်အဆင် + အလိုအလျောက် + တီဗွီအပြင်အဆင် + သင့်စကားဝှက် + သင့်ယူဇာနိမ်း + သင့်အီးမေးလ် လိပ်စာ + 127.0.0.1 + သင့်ဆိုဒ် + example.com + အရိပ် + ထမြောက်မှု + စာတန်းထိုးများ ထပ်တူပြုရန် + စာတန်းထိုးများ အလွန်စောနေပါက %d ms ဒီဟာကိုသုံးပါ + The quick brown fox jumps over the lazy dog + တင်ပြီး %s + ဖိုင်မှတင်သွင်းပြီး + ရုံရိုက် + အကြည် + အကြည် + UHD + HDR + SDR + Web + WP + SD + ကြည့်ရှုမှု + ပိုစတာပုံရိပ် + စာတန်းထိုးများမှ bloat ကိုဖယ်ရှားပါ + နှစ်သက်ရာ မီဒီယာဘာသာစကားဖြင့် စစ်ထုတ်ပါ + အပိုများ + ဒေတာမမှန်ပါ + URL မမှန်ပါ + အချို့အယွင်း + စာတန်းထိုးများမှ ပိတ်ထားသော စာတန်းများကို ဖယ်ရှားပါ + ထွေလာ + ဖြည့်စွက်များ + ရီပိုစစ်ထရီ ဖြည့်စွက်များအားလုံးကိုဖျက်မည်ဖြစ်သည် + ရီပိုစစ်ထရီ ကိုဖျက်ရန် + ဤရီပိုစစ်ထရီမှ ဖြည့်စွက်များအားလုံးကို ဒေါင်းလုဒ်လုပ်မှာလား\? + %s (ပိတ်ပြီး) + ထောက်ပံ့ထားသော + ဘာသာစကား + အဆက်များကိုအရင်သွင်းပါ + VLC + MPV + ဝဘ်ထဲတွင်ဖွင့်ရန် + အစပိုင်း + အဆုံးပိုင်း + ကြည့်ရှုခဲ့သည်များကိုရှင်းရန် + ကြည့်ပြီးသည်မှဖယ်ရှားရန် + သင်ထွက်ရန်သေချာပြီလား + မသေချာပါ + အက်ပ်အပ်ဒိတ်အားဒေါင်းလုဒ်လုပ်နေသည်… + အက်ပ်အပ်ဒိတ်အားသွင်းနေသည်… + အပ်ဒိတ်ဖြစ်မှု (အသစ် မှ အဟောင်း) + သင့်လိုက်ဘရီသည် ဗလာဖြစ်နေသည် :( +\nအကောင့်ဝင်ပါ သို့မဟုတ် သင့်ဖုန်းလိုက်ဘရီတွင် ကြည့်စရာများထည့်ပါ။ + သုံးရန် + တည်းဖြတ်ရန် + အရည်အသွေးများ + စာတန်းထိုး ကုဒ်လုပ်ခြင်း + ပံ့ပိုးပေးသူ စစ်ဆေးမှု + %s %s + အကောင့်ဝင်မည် + အကောင့်ပြောင်းမည် + ချိန်ညိှခြင်း + /%d + အင်တာနက်မှ တင်သွင်းမည် + နောက်ခံ + အကောင့်မဝင်ရောက်နိုင်ပါ %s + ဘာမျှ + အနည်းဆုံး + အနားကွပ် + ကျပန်း + ချုံ့ပြီး + 1000 ms + စာတန်းထိုး ကြန့်ကြာမှု + စာတန်းထိုးများအလွန်နောက်ကျနေပါက %d ms ဒီဟာကိုသုံးပါ + စာတန်းထိုး ကြန့်ကြာမှု သတ်မှတ်ထားခြင်းမရှိ + ရုံရိုက် + ရုံရိုက် + TC + အရည်အသွေး + အစီအစဥ်ချခြင်းကိုကျော်မည် + ချို့ယွင်းမှုသတင်းပေးပို့ခြင်း + ဘာတွေကြည့်ချင်လဲ + ပြီးပြီ + အဆက်များ + မသွင်းနိုင်ပါ %s + သင့်စက်ပစ္စည်းနှင့် ကိုက်ညီစေရန် အက်ပ်၏အသွင်အပြင်ကို ပြောင်းလဲပါ + ရီပိုစစ်ထရီထည့်ရန် + အသက်ပြည့်ပြီးသူများသာ + ရီပိုစစ်ထရီအမည် + ရီပိုစစ်ထရီ URL + ဖြည့်စွက်များ ထည့်ပြီး + ဤဘာသာစကားများဖြင့် ဗီဒီယိုများကို ကြည့်ရှုပါ + ဖြည့်စွက်များ ဒေါင်းလုဒ်လုပ်ပြီး + ဖြည့်စွက်များဖျက်ပြီး + ဒေါင်းလုဒ်လုပ်ခြင်း စတင်သည် %d %s… + ဒေါင်းလုဒ်လုပ်ပြီး %d %s + အားလုံး %s ဒေါင်းလုဒ်လုပ်ပြီးသား + ရီပိုစစ်ထရီထဲတွင်ဖြည့်စွက်များမတွေ့ပါ + ရီပိုစစ်ထရီမတွေ့ပါ၊URLကိုပြန်စစ်ပြီးဗီပီအန်ဖြင့်ကြိုးစားကြည့်ပါ + အသုတ်လိုက် ဒေါင်းလုဒ် + ဖြည့်စွက် + သင်အသုံးပြုလိုသောဆိုက်များစာရင်းကို ဒေါင်းလုဒ်လုပ်ပါ + ဒေါင်းလုဒ်လုပ်ပြီး: %d + ပိတ်ပြီး: %d + ဘာသာစကားကုဒ် (en) + အကောင့် + အကောင့်ထွက်မည် + အကောင့်ထည့်မည် + အကောင့်ဖွင့်မည် + စောင့်ကြည့်ခြင်းထည့်မည် + ထည့်ပြီး %s + အဆင့်သတ်မှတ်ထားပြီး + %d / 10 + /\?\? + %s ချိတ်ဆက်ပြီး + ပိတ်ပါ + ပုံမှန် + အားလုံး + အပြည့် + ဖိုင်ဒေါင်းလုဒ်လုပ်ပြီး + အဓိက + ထောက်ပံ့သည် + ရင်းမြစ် + မကြာမီလာမည်… + TS + Blu-ray + အရည်အသွေးနှင့်ခေါင်းစဥ် + ခေါင်းစဥ် + အိုင်ဒီမမှန်ပါ + အများမြင်နိုင်သော + စာတန်းထိုးအားလုံးကို စာလုံးအကြီးပြောင်းပါ + ပြန်စတင်မည် + ကြည့်ပြီးအဖြစ်မှတ်ရန် + အစီအစဥ်ချမှု + အစီအစဥ် + အဆင့်သတ်မှတ်ချက် (အမြင့်ဆုံးမှအနိမ့်ဆုံးသို့) + အဆင့်သတ်မှတ်ချက် (အနိမ့်ဆုံး မှ အမြင့်ဆုံးသို့) + အပ်ဒိတ်ဖြစ်မှု (အဟောင် မှ အသစ်) + အက္ခရာစဥ်လိုက် (A မှ Z) + အက္ခရာစဥ်လိုက် (Z မှ A) + ပြောင်းပြန် + စာရင်းသွင်းထားသောရှိုးများကိုအပ်ဒိတ်လုပ်နေသည် + စာရင်းသွင်းပြီး + စာရင်းသွင်းပြီး %s + စာရင်းသွင်းမှုပယ်ဖျက်ပြီး %s + ဤစာရင်းသည် ဗလာဖြစ်နေသည်။ အခြားတစ်ခုသို့ ပြောင်းကြည့်ပါ။ + Safe mode ဖိုင်ကို တွေ့ရှိခဲ့သည်။ +\nဖိုင်ကိုမဖယ်ရှားမချင်း စတင်ဖွင့်စတွင် မည်သည့် extension များကိုမျှ မတင်ပါ။ + အပိုင်းသစ် %d ထွက်ပြီ + ပရိုဖိုင် %d + ဝိုင်ဖိုင် + မိုဘိုင်းဒေတာ + ပုံသေထားရန် + ပရိုဖိုင်များ + အကူအညီ + ဤနေရာတွင် သင်သည် အရင်းအမြစ်များကို မည်ကဲ့သို့ အစီအစဥ်ချမည်ကို ပြောင်းလဲနိုင်သည်။ ဗီဒီယိုတစ်ခုတွင် ပိုမိုဦးစားပေးပါက ရင်းမြစ်ရွေးချယ်မှုတွင် ပိုမိုမြင့်မားလာမည်ဖြစ်သည်။ အရင်းအမြစ် ဦးစားပေးနှင့် အရည်အသွေး ဦးစားပေး၏ ပေါင်းစုသည် ဗီဒီယို ဦးစားပေးဖြစ်သည်။ +\n +\nအရင်းအမြစ် A: 3 +\nအရည်အသွေး B: 7 +\nပေါင်းစပ်ဗီဒီယို ဦးစားပေး 10 ခု ရှိပါမည်။ +\n +\nမှတ်ချက်- ပေါင်းလဒ်သည် 10 သို့မဟုတ် ထို့ထက်ပိုပါက ထိုလင့်ခ်ကို တင်သည့်အခါ ဗီဒီယိုဖွင့်စက်သည် အလိုအလျောက် ဒေါင်းလုဒ်ကို ကျော်သွားမည်ဖြစ်သည် + ပရိုဖိုင်နောက်ခံ + UI ကို မှန်ကန်စွာ ဖန်တီး၍မရပါ၊ ၎င်းသည် အဓိက ချို့ယွင်းချက်တစ်ခုဖြစ်ပြီး ချက်ချင်းသတင်းပို့သင့်သည်။ %s + သင်နဂိုတည်းကသတ်မှတ်ပြီး + လိုက်ဘရီရွေးချယ်ရန် + ဖြင့်ဖွင့်မည် + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 5f60ac14..c33bf107 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -573,5 +573,6 @@ Selecteer een modus om het downloaden van plug-ins te filteren Uitzetten De gebruikersinterface kon niet correct worden gemaakt, dit is een ERNSTIG PROBLEEM en moet onmiddellijk gerapporteerd worden %s - @string/default_subtitles - + \@string/default_subtitles + Je hebt al gestemd + \ 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 6db36065..38c56f0d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -553,4 +553,7 @@ Wybierz tryb filtrowania pobieranych rozszerzeń Wyłączać @string/default_subtitles - + Nie znaleziono żadnych wtyczek w repozytorium + Już oddano głos + Nie znaleziono tego repozytorium, sprawdź adres URL lub spróbuj połączyć się przez VPN + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b2504e84..75d1ddbc 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -552,4 +552,5 @@ Desativar Não foram encontrados plugins no repositório Repositório não encontrado, verifique o URL e tente a VPN - + Você já votou + \ No newline at end of file diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index f763d795..cce4e7d3 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -248,4 +248,5 @@ aoaaaaaoooghhh oooooh uuaagh @string/home_play - + oouuhhh ahhooo-ahah + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 2c5d4197..14b2334f 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -80,14 +80,14 @@ Сюжет не знайдено Опис не знайдено Показати Logcat 🐈 - Продовження відтворення в мініатюрному плеєрі поверх інших застосунків + Продовжує відтворення в мініатюрному плеєрі поверх інших застосунків Прибирає чорні рамки Субтитри Субтитри Chromecast Налаштування субтитрів Chromecast Режим Eigengravy Проведіть пальцем, щоб змінити налаштування - Проведіть пальцем вгору або вниз, ліворуч або праворуч, щоб змінити яскравість чи гучність + Проведіть вгору або вниз з лівого або правого боку, щоб змінити яскравість чи гучність Відтворювати наступний епізод після закінчення поточного Головна CloudStream @@ -121,8 +121,8 @@ Колір тексту Колір контуру Автовідтворення наступного епізоду - Проведіть пальцем з боку в бік, щоб керувати своїм положенням у відео - %d Бананів для розробників + Проведіть з боку в бік, щоб керувати своїм положенням у відео + %d бананів для розробників Кнопка зміни розміру плеєра @string/home_play Для коректної роботи цього постачальника може знадобитися VPN @@ -133,7 +133,7 @@ Проведіть пальцем, щоб перемотати Двічі торкніться, щоб перемотати Двічі торкніться для паузи - Крок перемотки (Секунди) + Крок перемотки (секунди) Натисніть двічі посередині, щоб призупинити відтворення відео Використовувати яскравість системи Оновити прогрес перегляду @@ -150,8 +150,8 @@ Надає результати пошуку, розділені за постачальниками Надсилає дані лише про збої Не надсилає даних - Показати заповнюючий епізод для аніме - Показати трейлери + Показувати філери до аніме + Показувати трейлери Приховати вибрану якість відео в результатах пошуку Автоматичне завантаження плагінів Показувати оновлення застосунку @@ -214,12 +214,12 @@ Завантажити дзеркало Перевірити наявність оновлень Заблокувати - Пропустити OP + Пропускати OP Не показувати знову Оновити Бажана якість перегляду (WiFi) Заголовок - Перемикання елементів інтерфейсу на плакаті + Перемикання елементів інтерфейсу на постері Оновлення не знайдено Двічі торкніться праворуч або ліворуч, щоб перемотати відео вперед або назад Використовуйте системну яскравість у плеєрі замість темної накладки @@ -227,10 +227,10 @@ Торренти Автоматична синхронізація прогресу поточного епізоду Відсутні дозволи на зберігання. Будь ласка, спробуйте ще раз. - Показати постери від Kitsu + Показувати постери від Kitsu Автоматичне оновлення плагінів Автоматично встановлювати всі ще не встановлені плагіни з доданих репозиторіїв. - Автоматично шукати нові оновлення після запуску застосунку. + Автоматично шукає нові оновлення після запуску застосунку. Оновлення до бета-версій Посилання скопійовано в буфер обміну Деякі телефони не підтримують новий інсталятор пакетів. Спробуйте стару версію, якщо оновлення не встановлюються. @@ -354,7 +354,7 @@ DNS через HTTPS Шлях завантаження Додайте клон існуючого сайту, з іншою URL-адресою - Відображати мітку Дубляж/Субтитри в аніме + Відображати мітку Дубляж/Субтитри до аніме Застереження Розширення Дії @@ -382,7 +382,7 @@ Підтримка Фон Blu-ray - Видалити закриті титри з субтитрів + Видаляти закриті титри з субтитрів DVD Недійсні дані Фільтрувати за бажаною мовою медіа @@ -400,7 +400,7 @@ HD TS TC - Видалити роздуття субтитрів + Видаляти роздуття субтитрів Referer Далі Дивіться відео на цих мовах @@ -451,8 +451,8 @@ Вбудований плеєр VLC MPV - Відтворення веб-відео - Веб-браузер + Web Video Cast + Веббраузер Ендінґ Коротке повторення Пропустити %s @@ -462,7 +462,7 @@ Вступ Очистити історію Історія - Показувати спливаючі вікна для опенінґу/ендінґу + Показує спливаюче вікно для пропуску опенінґу/ендінґу Забагато тексту. Не вдалося зберегти в буфер обміну. Позначити як переглянуте Ви впевнені що хочете вийти\? @@ -552,4 +552,5 @@ @string/default_subtitles Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії - + Ви вже проголосували + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 4b394227..f7abd6db 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -267,7 +267,7 @@ Cập nhật Chất lượng xem ưu tiên (WiFi) Kí tự tối đa trên tiêu đề - Độ phân giải trình phát video + Nội dung trình phát video Kích thước bộ nhớ đệm video Thời lượng bộ nhớ đệm Lưu bộ nhớ đệm video trên ổ cứng @@ -380,8 +380,8 @@ Web Ảnh áp phích Trình phát - Độ phân giải và Tiêu đề - Tiêu đề + Độ phân giải và Tên nguồn + Tên nguồn Độ phân giải Id không hợp lệ Lỗi dữ liệu @@ -561,4 +561,11 @@ \n \nLƯU Ý: Nếu tổng là 10 hoặc nhiều hơn, trình phát sẽ tự động bỏ tải khi liên kết đó được tải! Các phẩm chất - + Bạn đã bình chọn + Vô hiệu hoá + Không tìm thấy tiện ích, hãy kiểm tra URL và thử VPN + Không tìm thấy plugin + Không thể khởi tạo UI, đây là một LỖI LỚN và cần được báo cáo ngay lập tức tới %s + Chọn chế độ để lọc plugin tải xuống + \@string/default_subtitles + \ No newline at end of file diff --git a/fastlane/metadata/android/pt/changelogs/2.txt b/fastlane/metadata/android/pt/changelogs/2.txt new file mode 100644 index 00000000..1153e632 --- /dev/null +++ b/fastlane/metadata/android/pt/changelogs/2.txt @@ -0,0 +1 @@ +- Adicionado o registo de alterações! diff --git a/fastlane/metadata/android/pt/full_description.txt b/fastlane/metadata/android/pt/full_description.txt new file mode 100644 index 00000000..48bf36ce --- /dev/null +++ b/fastlane/metadata/android/pt/full_description.txt @@ -0,0 +1,10 @@ +O CloudStream-3 permite-lhe transmitir e descarregar filmes, séries de TV e anime. + +A aplicação é fornecida sem quaisquer anúncios e análises e +suporta vários sites de trailers e filmes, e muito mais, por exemplo + +Marcadores + +Downloads de legendas + +Suporte para Chromecast diff --git a/fastlane/metadata/android/pt/short_description.txt b/fastlane/metadata/android/pt/short_description.txt new file mode 100644 index 00000000..d0392f34 --- /dev/null +++ b/fastlane/metadata/android/pt/short_description.txt @@ -0,0 +1 @@ +Transmita e transfira filmes, séries de TV e anime. diff --git a/fastlane/metadata/android/pt/title.txt b/fastlane/metadata/android/pt/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/pt/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/vi/changelogs/2.txt b/fastlane/metadata/android/vi/changelogs/2.txt new file mode 100644 index 00000000..e03e458e --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/2.txt @@ -0,0 +1 @@ +- Đã thêm Nhật ký thay đổi! diff --git a/fastlane/metadata/android/vi/full_description.txt b/fastlane/metadata/android/vi/full_description.txt new file mode 100644 index 00000000..90ea7ab7 --- /dev/null +++ b/fastlane/metadata/android/vi/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 cho phép bạn xem và tải xuống phim lẻ, phim bộ và anime. + +Ứng dụng không có quảng cáo hay và phân tích nào, +đồng thời hỗ trợ nhiều trang web xem phim, v.v. + +Đánh dấu + +Tải phụ đề + +Hỗ trợ Chromecast diff --git a/fastlane/metadata/android/vi/short_description.txt b/fastlane/metadata/android/vi/short_description.txt new file mode 100644 index 00000000..e4e20bd5 --- /dev/null +++ b/fastlane/metadata/android/vi/short_description.txt @@ -0,0 +1 @@ +Xem và tải xuống phim lẻ, phim bộ và anime. diff --git a/fastlane/metadata/android/vi/title.txt b/fastlane/metadata/android/vi/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/vi/title.txt @@ -0,0 +1 @@ +CloudStream From 6948bf807373d349844701c4e2eb7e3a438179e0 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Sat, 19 Aug 2023 19:38:45 +0000 Subject: [PATCH 185/570] chore(locales): fix locale issues --- .../lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 2 ++ app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fil/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 4 ++-- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-my/strings.xml | 6 +++--- app/src/main/res/values-nl/strings.xml | 4 ++-- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-qt/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 4 ++-- 16 files changed, 22 insertions(+), 20 deletions(-) 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 d76eba1e..2c81ad1f 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 @@ -69,6 +69,7 @@ val appLanguages = arrayListOf( Triple("", "Esperanto", "eo"), Triple("", "español", "es"), Triple("", "فارسی", "fa"), + Triple("", "fil", "fil"), Triple("", "français", "fr"), Triple("", "galego", "gl"), Triple("", "हिन्दी", "hi"), @@ -84,6 +85,7 @@ val appLanguages = arrayListOf( Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), Triple("", "bahasa Melayu", "ms"), + Triple("", "ဗမာစာ", "my"), Triple("", "Nederlands", "nl"), Triple("", "norsk nynorsk", "nn"), Triple("", "norsk bokmål", "no"), diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 987211a5..0c11f7e9 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -585,4 +585,4 @@ لا توجد اضافة في المستودع المستودع لم يتم العثور عليه، تحقق من العنوان اوجرب شبكة افتراضية خاصة(vpn) لقد صوتت بالفعل - \ 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 2a3bdb27..425293e4 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -499,4 +499,4 @@ Atualizando shows inscritos Player oculto - Procure na barra de progresso Conteúdo +18 - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 165dbbb4..46bd860d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -577,4 +577,4 @@ Repozitář nenalezen, zkontrolujte adresu URL a zkuste použít VPN @string/default_subtitles Již jste hlasovali - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 6326211e..8e9f9c2c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -553,4 +553,4 @@ No se encontraron complementos en el repositorio Repositorio no encontrado, comprueba la URL y prueba la VPN Ya has votado - \ No newline at end of file + diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index a6b3daec..42eba3cc 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4cc56207..208e6140 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -552,4 +552,4 @@ Qualités L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s Sélectionnez le mode pour filtrer le téléchargement des plugins - \ 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 87a01ff9..d514bcc4 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -575,5 +575,5 @@ Tidak ada plugin yang ditemukan di repositori Repositori tidak ditemukan, periksa URL dan coba VPN Kamu sudah voting - \@string/default_subtitles - \ No newline at end of file + @string/default_subtitles + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 7cca78ca..0c34e89a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -575,4 +575,4 @@ @string/default_subtitles Disabilita Hai già votato - \ No newline at end of file + diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index cde13ba5..0cb44373 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -58,7 +58,7 @@ ဆက်လက်ကြည့်ရှုမည် ဖယ်ရှားရန် ပိုမို၍ - \@string/home_play + @string/home_play ဒီဟာကတောရပ်တစ်ခုပါ ဗီပီအန်တစ်ခုသုံးဖို့အကြံပြုပါတယ် ဖော်ပြချက် ဇာတ်လမ်းသွား မတွေ့ပါ @@ -128,7 +128,7 @@ နောက်အစီအစဥ် စာတန်းထိုးမထည့် ပုံသေ - \@string/default_subtitles + @string/default_subtitles ကျန်ရှိသော အက်ပ် ရုပ်ရှင်များ @@ -553,4 +553,4 @@ သင်နဂိုတည်းကသတ်မှတ်ပြီး လိုက်ဘရီရွေးချယ်ရန် ဖြင့်ဖွင့်မည် - \ No newline at end of file + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index c33bf107..d19726fd 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -573,6 +573,6 @@ Selecteer een modus om het downloaden van plug-ins te filteren Uitzetten De gebruikersinterface kon niet correct worden gemaakt, dit is een ERNSTIG PROBLEEM en moet onmiddellijk gerapporteerd worden %s - \@string/default_subtitles + @string/default_subtitles Je hebt al gestemd - \ 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 38c56f0d..a170d610 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -556,4 +556,4 @@ Nie znaleziono żadnych wtyczek w repozytorium Już oddano głos Nie znaleziono tego repozytorium, sprawdź adres URL lub spróbuj połączyć się przez VPN - \ No newline at end of file + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 75d1ddbc..908ddb0d 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -553,4 +553,4 @@ Não foram encontrados plugins no repositório Repositório não encontrado, verifique o URL e tente a VPN Você já votou - \ No newline at end of file + diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index cce4e7d3..9c68c008 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -249,4 +249,4 @@ oooooh uuaagh @string/home_play oouuhhh ahhooo-ahah - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 14b2334f..e0db1c0e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index f7abd6db..217d2791 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -567,5 +567,5 @@ Không tìm thấy plugin Không thể khởi tạo UI, đây là một LỖI LỚN và cần được báo cáo ngay lập tức tới %s Chọn chế độ để lọc plugin tải xuống - \@string/default_subtitles - \ No newline at end of file + @string/default_subtitles + From a3009af4f585c010cd5018037123fefd644f8921 Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Sat, 19 Aug 2023 19:48:10 +0000 Subject: [PATCH 186/570] Add Native Crash Handler (#565) * Add NativeCrashHandler * Safer init --- app/CMakeLists.txt | 6 +++ app/build.gradle.kts | 6 +++ app/src/main/cpp/native-lib.cpp | 28 ++++++++++++ .../lagradost/cloudstream3/AcraApplication.kt | 1 + .../cloudstream3/NativeCrashHandler.kt | 44 +++++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 app/CMakeLists.txt create mode 100644 app/src/main/cpp/native-lib.cpp create mode 100644 app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 00000000..7f7fd14c --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,6 @@ +# Set this to the minimum version your project supports. +cmake_minimum_required(VERSION 3.18) +project(CrashHandler) +find_library(log-lib log) +add_library(native-lib SHARED src/main/cpp/native-lib.cpp) +target_link_libraries(native-lib ${log-lib}) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 708a2083..d6515289 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,6 +32,12 @@ android { enable = true } + externalNativeBuild { + cmake { + path("CMakeLists.txt") + } + } + signingConfigs { create("prerelease") { if (prereleaseStoreFile != null) { diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp new file mode 100644 index 00000000..f4cb531f --- /dev/null +++ b/app/src/main/cpp/native-lib.cpp @@ -0,0 +1,28 @@ +#include +#include +#include + +#define TAG "CloudStream Crash Handler" +volatile sig_atomic_t gSignalStatus = 0; +void handleNativeCrash(int signal) { + gSignalStatus = signal; +} + +extern "C" JNIEXPORT void JNICALL +Java_com_lagradost_cloudstream3_NativeCrashHandler_initNativeCrashHandler(JNIEnv *env, jobject) { + #define REGISTER_SIGNAL(X) signal(X, handleNativeCrash); + REGISTER_SIGNAL(SIGSEGV) + #undef REGISTER_SIGNAL +} + +//extern "C" JNIEXPORT void JNICALL +//Java_com_lagradost_cloudstream3_NativeCrashHandler_triggerNativeCrash(JNIEnv *env, jobject thiz) { +// int *p = nullptr; +// *p = 0; +//} + +extern "C" JNIEXPORT int JNICALL +Java_com_lagradost_cloudstream3_NativeCrashHandler_getSignalStatus(JNIEnv *env, jobject) { + //__android_log_print(ANDROID_LOG_INFO, TAG, "Got signal status %d", gSignalStatus); + return gSignalStatus; +} \ 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 61d467c4..32702657 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -106,6 +106,7 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) : class AcraApplication : Application() { override fun onCreate() { super.onCreate() + NativeCrashHandler.initCrashHandler() Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt new file mode 100644 index 00000000..e5cb2702 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt @@ -0,0 +1,44 @@ +package com.lagradost.cloudstream3 + +import com.lagradost.cloudstream3.MainActivity.Companion.lastError +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.plugins.PluginManager.checkSafeModeFile +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +object NativeCrashHandler { + // external fun triggerNativeCrash() + private external fun initNativeCrashHandler() + private external fun getSignalStatus(): Int + + private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch { + while (true) { + delay(10_000) + val signal = getSignalStatus() + // Signal is initialized to zero + if (signal == 0) continue + + // Do not crash in safe mode! + if (lastError != null) continue + if (checkSafeModeFile()) continue + + throw RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n") + } + } + + fun initCrashHandler() { + try { + System.loadLibrary("native-lib") + initNativeCrashHandler() + } catch (t: Throwable) { + // Make debug crash. + if (BuildConfig.DEBUG) throw t + logError(t) + return + } + + initSignalPolling() + } +} \ No newline at end of file From c4852ce440736105d32a18f1774ee65dead98808 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 20 Aug 2023 01:29:50 +0200 Subject: [PATCH 187/570] made HSL downloader even faster --- .../cloudstream3/utils/M3u8Helper.kt | 5 + .../utils/VideoDownloadManager.kt | 215 +++++++++++------- 2 files changed, 132 insertions(+), 88 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 5c0b45de..11dfa441 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.app +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec @@ -196,6 +197,8 @@ object M3u8Helper2 { return if(condition()) out else null } catch (e: IllegalArgumentException) { return null + } catch (e : CancellationException) { + return null } catch (t: Throwable) { delay(failDelay) } @@ -213,6 +216,8 @@ object M3u8Helper2 { return resolveLink(index) } catch (e: IllegalArgumentException) { return null + } catch (e : CancellationException) { + return null } catch (t: Throwable) { delay(failDelay) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index d8ef7e85..89094f3f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -43,6 +43,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -1083,6 +1084,39 @@ object VideoDownloadManager { ) } + /** Helper function to make sure duplicate attributes don't get overriden or inserted without lowercase cmp + * example: map("a" to 1) appendAndDontOverride map("A" to 2, "a" to 3, "c" to 4) = map("a" to 1, "c" to 4) + * */ + private fun Map.appendAndDontOverride(rhs: Map): Map { + val out = this.toMutableMap() + val current = this.keys.map { it.lowercase() } + for ((key, value) in rhs) { + if (current.contains(key.lowercase())) continue + out[key] = value + } + return out + } + + private fun List.cancel() { + forEach { job -> + try { + job.cancel() + } catch (t: Throwable) { + logError(t) + } + } + } + + private suspend fun List.join() { + forEach { job -> + try { + job.join() + } catch (t: Throwable) { + logError(t) + } + } + } + @Throws suspend fun downloadThing( context: Context, @@ -1166,8 +1200,9 @@ object VideoDownloadManager { hashMapOf() val jobs = (0 until parallelConnections).map { - launch { + launch(Dispatchers.IO) { + // @downloadexplanation // this may seem a bit complex but it more or less acts as a queue system // imagine we do the downloading [0,3] and it response in the order 0,2,3,1 // file: [_,_,_,_] queue: [_,_,_,_] Initial condition @@ -1177,6 +1212,10 @@ object VideoDownloadManager { // file: [X,X,_,_] queue: [_,_,X,X] + added 1 directly to file // file: [X,X,X,X] queue: [_,_,_,_] write the queue and remove from it + // note that this is a bit more complex compared to hsl as ever segment + // will return several bytearrays, and is therefore chained by the byte + // so every request has a front and back byte instead of an index + // this *requires* that no gap exist due because of resolve val callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) = callback@{ response -> if (!isActive) return@callback @@ -1228,10 +1267,11 @@ object VideoDownloadManager { while (true) { if (!isActive) return@launch fileMutex.withLock { - if (metadata.type == DownloadType.IsStopped) return@launch + if (metadata.type == DownloadType.IsStopped + || metadata.type == DownloadType.IsFailed) return@launch } - // just in case, we never want this to fail due to multithreading + // mutex just in case, we never want this to fail due to multithreading val index = currentMutex.withLock { if (!current.hasNext()) return@launch current.nextInt() @@ -1253,68 +1293,14 @@ object VideoDownloadManager { // fast stop as the jobs may be in a slow request metadata.setOnStop { - jobs.forEach { job -> - try { - job.cancel() - } catch (t: Throwable) { - logError(t) - } - } + jobs.cancel() } - jobs.forEach { it.join() } + jobs.join() // jobs are finished so we don't want to stop them anymore metadata.removeStopListener() - // set up a connection - //val request = app.get( - // link.url.replace(" ", "%20"), - // headers = link.headers.appendAndDontOverride( - // mapOf( - // "Accept-Encoding" to "identity", - // "accept" to "*/*", - // "user-agent" to USER_AGENT, - // "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", - // "sec-fetch-mode" to "navigate", - // "sec-fetch-dest" to "video", - // "sec-fetch-user" to "?1", - // "sec-ch-ua-mobile" to "?0", - // ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap() - // ), - // referer = link.referer, - // verify = false - //) - - // init variables - //val contentLength = request.size ?: 0 - //metadata.totalBytes = contentLength + resumeAt - //// save - //metadata.setDownloadFileInfoTemplate( - // DownloadedFileInfo( - // totalBytes = metadata.approxTotalBytes, - // relativePath = relativePath ?: "", - // displayName = displayName, - // basePath = basePath - // ) - //) - //// total length is less than 5mb, that is too short and something has gone wrong - //if (extension == "mp4" && metadata.approxTotalBytes < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION - //// read the buffer into the filestream, this is equivalent of transferTo - //requestStream = request.body.byteStream() - //metadata.type = DownloadType.IsDownloading - //val buffer = ByteArray(DEFAULT_BUFFER_SIZE) - //var read: Int - //while (requestStream.read(buffer, 0, DEFAULT_BUFFER_SIZE).also { read = it } >= 0) { - // fileStream.write(buffer, 0, read) - // // wait until not paused - // while (metadata.type == DownloadType.IsPaused) delay(100) - // // if stopped then break to delete - // if (metadata.type == DownloadType.IsStopped) break - // metadata.addBytes(read.toLong()) - //} - - if (metadata.type == DownloadType.IsFailed) { return@withContext ERROR_CONNECTION_ERROR } @@ -1342,7 +1328,6 @@ object VideoDownloadManager { // note that when failing we don't want to delete the file, // only user interaction has that power - metadata.removeStopListener() metadata.type = DownloadType.IsFailed return@withContext ERROR_CONNECTION_ERROR } finally { @@ -1352,19 +1337,6 @@ object VideoDownloadManager { } } - /** Helper function to make sure duplicate attributes don't get overriden or inserted without lowercase cmp - * example: map("a" to 1) appendAndDontOverride map("A" to 2, "a" to 3, "c" to 4) = map("a" to 1, "c" to 4) - * */ - private fun Map.appendAndDontOverride(rhs: Map): Map { - val out = this.toMutableMap() - val current = this.keys.map { it.lowercase() } - for ((key, value) in rhs) { - if (current.contains(key.lowercase())) continue - out[key] = value - } - return out - } - @Throws private suspend fun downloadHLS( context: Context, @@ -1429,28 +1401,95 @@ object VideoDownloadManager { metadata.hlsTotal = items.size metadata.type = DownloadType.IsDownloading + + val currentMutex = Mutex() + val current = (0 until items.size).iterator() + + val fileMutex = Mutex() + val pendingData: HashMap = hashMapOf() + + // see @downloadexplanation for explanation of this download strategy, + // this keeps all jobs working at all times, // does several connections in parallel instead of a regular for loop to improve // download speed - (startAt until items.size).chunked(parallelConnections).forEach { subset -> - // wait until not paused - while (metadata.type == DownloadType.IsPaused) delay(100) - // if stopped then break to delete - if (metadata.type == DownloadType.IsStopped) return@forEach + val jobs = (0 until parallelConnections).map { + launch(Dispatchers.IO) { + while (true) { + if (!isActive) return@launch + fileMutex.withLock { + if (metadata.type == DownloadType.IsStopped + || metadata.type == DownloadType.IsFailed + ) return@launch + } - subset.amap { idx -> - idx to items.resolveLinkSafe(idx)?.also { bytes -> - metadata.addSegment(bytes.size.toLong()) + // mutex just in case, we never want this to fail due to multithreading + val index = currentMutex.withLock { + if (!current.hasNext()) return@launch + current.nextInt() + } + + // in case something has gone wrong set to failed if the fail is not caused by + // user cancellation + val bytes = items.resolveLinkSafe(index) ?: run { + fileMutex.withLock { + if (metadata.type != DownloadType.IsStopped) { + metadata.type = DownloadType.IsFailed + } + } + return@launch + } + + try { + fileMutex.lock() + // user pause + while (metadata.type == DownloadType.IsPaused) delay(100) + // if stopped then break to delete + if (metadata.type == DownloadType.IsStopped || !isActive) return@launch + + // send notification, no matter the actual write order + metadata.addSegment(bytes.size.toLong()) + + // directly write the bytes if you are first + if (metadata.hlsWrittenProgress == index) { + fileStream.write(bytes) + metadata.setWrittenSegment(index) + } else { + // no need to clone as there will be no modification of this bytearray + pendingData[index] = bytes + } + + // write the cached bytes submitted by other threads + while (true) { + fileStream.write( + pendingData.remove(metadata.hlsWrittenProgress) ?: break + ) + metadata.setWrittenSegment(metadata.hlsWrittenProgress) + } + } catch (t : Throwable) { + // this is in case of write fail + if (metadata.type != DownloadType.IsStopped) { + metadata.type = DownloadType.IsFailed + } + } finally { + fileMutex.unlock() + } } - }.forEach { (idx, bytes) -> - if (bytes == null) { - metadata.type = DownloadType.IsFailed - return@withContext ERROR_CONNECTION_ERROR - } - fileStream.write(bytes) - metadata.setWrittenSegment(idx) } } + // fast stop as the jobs may be in a slow request + metadata.setOnStop { + jobs.cancel() + } + + jobs.join() + + metadata.removeStopListener() + + if (metadata.type == DownloadType.IsFailed) { + return@withContext ERROR_CONNECTION_ERROR + } + if (metadata.type == DownloadType.IsStopped) { // we need to close before delete fileStream.closeQuietly() From 4e28e5f8cc4d811184799cb3aa024665cc0aee3d Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 20 Aug 2023 03:58:31 +0200 Subject: [PATCH 188/570] fixed not downloading the last 20MiB on mp4 downloader + bump + mb/s notification --- app/build.gradle.kts | 2 +- .../utils/VideoDownloadManager.kt | 79 ++++++++++++++----- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d6515289..50125aa3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,7 +58,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.5" + versionName = "4.1.6" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 89094f3f..507abc34 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -30,7 +30,6 @@ import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall @@ -256,7 +255,8 @@ object VideoDownloadManager { total: Long, notificationCallback: (Int, Notification) -> Unit, hlsProgress: Long? = null, - hlsTotal: Long? = null + hlsTotal: Long? = null, + bytesPerSecond: Long ): Notification? { try { if (total <= 0) return null// crash, invalid data @@ -327,22 +327,29 @@ object VideoDownloadManager { val totalMbString: String val suffix: String + val mbFormat = "%.1f MB" + if (hlsProgress != null && hlsTotal != null) { progressPercentage = hlsProgress.toLong() * 100 / hlsTotal progressMbString = hlsProgress.toString() totalMbString = hlsTotal.toString() - suffix = " - %.1f MB".format(progress / 1000000f) + suffix = " - $mbFormat".format(progress / 1000000f) } else { progressPercentage = progress * 100 / total - progressMbString = "%.1f MB".format(progress / 1000000f) - totalMbString = "%.1f MB".format(total / 1000000f) + progressMbString = mbFormat.format(progress / 1000000f) + totalMbString = mbFormat.format(total / 1000000f) suffix = "" } + val mbPerSecondString = + if (state == DownloadType.IsDownloading) { + " ($mbFormat/s)".format(bytesPerSecond.toFloat() / 1000000f) + } else "" + val bigText = when (state) { DownloadType.IsDownloading, DownloadType.IsPaused -> { - (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix" + (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix$mbPerSecondString" } DownloadType.IsFailed -> { @@ -608,6 +615,7 @@ object VideoDownloadManager { val bytesTotal: Long, val hlsProgress: Long? = null, val hlsTotal: Long? = null, + val bytesPerSecond: Long ) data class StreamData( @@ -723,6 +731,7 @@ object VideoDownloadManager { // notification metadata private var lastUpdatedMs: Long = 0, + private var lastDownloadedBytes: Long = 0, private val createNotificationCallback: (CreateNotificationMetadata) -> Unit, private var internalType: DownloadType = DownloadType.IsPending, @@ -738,6 +747,12 @@ object VideoDownloadManager { // this is used for copy with metadata on how much we have downloaded for setKey private var downloadFileInfoTemplate: DownloadedFileInfo? = null ) : Closeable { + fun setResumeLength(length: Long) { + bytesDownloaded = length + bytesWritten = length + lastDownloadedBytes = length + } + val approxTotalBytes: Long get() = totalBytes ?: hlsTotal?.let { total -> (bytesDownloaded * (total / hlsProgress.toFloat())).toLong() @@ -839,6 +854,13 @@ object VideoDownloadManager { @JvmName("DownloadMetaDataNotify") private fun notify() { + // max 10 sec between notifications, min 0.1s, this is to stop div by zero + val dt = (System.currentTimeMillis() - lastUpdatedMs).coerceIn(100, 10000) + + val bytesPerSecond = + ((bytesDownloaded - lastDownloadedBytes) * 1000L) / dt + + lastDownloadedBytes = bytesDownloaded lastUpdatedMs = System.currentTimeMillis() try { val bytes = approxTotalBytes @@ -851,7 +873,8 @@ object VideoDownloadManager { bytesDownloaded, bytes, hlsTotal = hlsTotal?.toLong(), - hlsProgress = hlsProgress.toLong() + hlsProgress = hlsProgress.toLong(), + bytesPerSecond = bytesPerSecond ) ) } else { @@ -860,6 +883,7 @@ object VideoDownloadManager { internalType, bytesDownloaded, bytes, + bytesPerSecond = bytesPerSecond ) ) } @@ -1057,21 +1081,29 @@ object VideoDownloadManager { // we don't want to make a separate connection for every 1kb require(chuckSize > 1000) - val contentLength = + var contentLength = app.head(url = url, headers = headers, referer = referer, verify = false).size + if (contentLength != null && contentLength <= 0) contentLength = null var downloadLength: Long? = null var totalLength: Long? = null val ranges = if (contentLength == null) { + // is the equivalent of [startByte..EOF] as we don't know the size we can only do one + // connection LongArray(1) { startByte } } else { downloadLength = contentLength - startByte totalLength = contentLength - LongArray((downloadLength / chuckSize).toInt()) { idx -> + // div with ceiling as + // this makes the last part "unknown ending" and it will break at EOF + // so eg startByte = 0, downloadLength = 13, chuckSize = 10 + // = LongArray(2) { 0, 10 } = [0,10) + [10..EOF] + LongArray(((downloadLength + chuckSize - 1) / chuckSize).toInt()) { idx -> startByte + idx * chuckSize } } + return LazyStreamDownloadData( url = url, headers = headers, @@ -1158,8 +1190,7 @@ object VideoDownloadManager { val resume = stream.resume ?: return@withContext ERROR_UNKNOWN val fileLength = stream.fileLength ?: return@withContext ERROR_UNKNOWN val resumeAt = (if (resume) fileLength else 0) - metadata.bytesDownloaded = resumeAt - metadata.bytesWritten = resumeAt + metadata.setResumeLength(resumeAt) metadata.type = DownloadType.IsPending val items = streamLazy( @@ -1268,7 +1299,8 @@ object VideoDownloadManager { if (!isActive) return@launch fileMutex.withLock { if (metadata.type == DownloadType.IsStopped - || metadata.type == DownloadType.IsFailed) return@launch + || metadata.type == DownloadType.IsFailed + ) return@launch } // mutex just in case, we never want this to fail due to multithreading @@ -1374,7 +1406,7 @@ object VideoDownloadManager { fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN // push the metadata - metadata.bytesDownloaded = stream.fileLength ?: 0 + metadata.setResumeLength(stream.fileLength ?: 0) metadata.hlsProgress = startAt metadata.type = DownloadType.IsPending metadata.setDownloadFileInfoTemplate( @@ -1446,12 +1478,15 @@ object VideoDownloadManager { // if stopped then break to delete if (metadata.type == DownloadType.IsStopped || !isActive) return@launch + val segmentLength = bytes.size.toLong() // send notification, no matter the actual write order - metadata.addSegment(bytes.size.toLong()) + metadata.addSegment(segmentLength) // directly write the bytes if you are first if (metadata.hlsWrittenProgress == index) { fileStream.write(bytes) + + metadata.addBytesWritten(segmentLength) metadata.setWrittenSegment(index) } else { // no need to clone as there will be no modification of this bytearray @@ -1460,12 +1495,14 @@ object VideoDownloadManager { // write the cached bytes submitted by other threads while (true) { - fileStream.write( - pendingData.remove(metadata.hlsWrittenProgress) ?: break - ) + val cache = pendingData.remove(metadata.hlsWrittenProgress) ?: break + val cacheLength = cache.size.toLong() + + fileStream.write(cache) + metadata.addBytesWritten(cacheLength) metadata.setWrittenSegment(metadata.hlsWrittenProgress) } - } catch (t : Throwable) { + } catch (t: Throwable) { // this is in case of write fail if (metadata.type != DownloadType.IsStopped) { metadata.type = DownloadType.IsFailed @@ -1756,7 +1793,8 @@ object VideoDownloadManager { meta.bytesTotal, notificationCallback, meta.hlsProgress, - meta.hlsTotal + meta.hlsTotal, + meta.bytesPerSecond ) } } @@ -1785,7 +1823,8 @@ object VideoDownloadManager { meta.type, meta.bytesDownloaded, meta.bytesTotal, - notificationCallback + notificationCallback, + bytesPerSecond = meta.bytesPerSecond ) } }) From afcbdeecc86151081aa8a135b3c5900bf1ec8c7e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 22 Aug 2023 04:00:05 +0200 Subject: [PATCH 189/570] changes to downloader for stable resume --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 6 +- .../ui/settings/SettingsUpdates.kt | 11 +- .../utils/DownloadFileWorkManager.kt | 22 +- .../utils/VideoDownloadManager.kt | 532 ++++++------------ 4 files changed, 191 insertions(+), 380 deletions(-) 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 e0d50cc3..9e601fc7 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 @@ -520,10 +520,10 @@ class GeneratorPlayer : FullScreenPlayer() { if (uri == null) return@normalSafeApiCall val ctx = context ?: AcraApplication.context ?: return@normalSafeApiCall // RW perms for the path - val flags = + ctx.contentResolver.takePersistableUriPermission( + uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - ctx.contentResolver.takePersistableUriPermission(uri, flags) + ) val file = UniFile.fromUri(ctx, uri) println("Loaded subtitle file. Selected URI path: $uri - Name: ${file.name}") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index c304629a..62e46c08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -116,13 +116,14 @@ class SettingsUpdates : PreferenceFragmentCompat() { null, "txt", false - ).fileStream - fileStream?.writer()?.write(text) - } catch (e: Exception) { - logError(e) + ).openNew() + fileStream.writer().write(text) + dialog.dismissSafe(activity) + } catch (t: Throwable) { + logError(t) + showToast(t.message) } finally { fileStream?.closeQuietly() - dialog.dismissSafe(activity) } } binding.closeBtt.setOnClickListener { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt index aa424c08..421e4420 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt @@ -16,6 +16,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadCheck import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadEpisode import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadFromResume import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadStatusEvent +import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadResumePackage import kotlinx.coroutines.delay const val DOWNLOAD_CHECK = "DownloadCheck" @@ -36,15 +37,20 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo WORK_KEY_PACKAGE, key ) + if (info != null) { - downloadEpisode( - applicationContext, - info.source, - info.folder, - info.ep, - info.links, - ::handleNotification - ) + getDownloadResumePackage(applicationContext, info.ep.id)?.let { dpkg -> + downloadFromResume(applicationContext, dpkg, ::handleNotification) + } ?: run { + downloadEpisode( + applicationContext, + info.source, + info.folder, + info.ep, + info.links, + ::handleNotification + ) + } } else if (pkg != null) { downloadFromResume(applicationContext, pkg, ::handleNotification) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 507abc34..a81e4b3a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -9,9 +9,7 @@ import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.os.Environment -import android.provider.MediaStore import androidx.annotation.DrawableRes -import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri @@ -32,6 +30,7 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -44,6 +43,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -301,6 +301,8 @@ object VideoDownloadManager { if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { builder.setProgress((total / 1000).toInt(), (progress / 1000).toInt(), false) + } else if (state == DownloadType.IsPending) { + builder.setProgress(0,0,true) } val rowTwoExtra = if (ep.name != null) " - ${ep.name}\n" else "" @@ -352,6 +354,10 @@ object VideoDownloadManager { (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix$mbPerSecondString" } + DownloadType.IsPending -> { + (if (linkName == null) "" else "$linkName\n") + rowTwo + } + DownloadType.IsFailed -> { downloadFormat.format( context.getString(R.string.download_failed), @@ -363,7 +369,7 @@ object VideoDownloadManager { downloadFormat.format(context.getString(R.string.download_done), rowTwo) } - else -> { + DownloadType.IsStopped -> { downloadFormat.format( context.getString(R.string.download_canceled), rowTwo @@ -377,7 +383,7 @@ object VideoDownloadManager { } else { val txt = when (state) { - DownloadType.IsDownloading, DownloadType.IsPaused -> { + DownloadType.IsDownloading, DownloadType.IsPaused, DownloadType.IsPending -> { rowTwo } @@ -392,7 +398,7 @@ object VideoDownloadManager { downloadFormat.format(context.getString(R.string.download_done), rowTwo) } - else -> { + DownloadType.IsStopped -> { downloadFormat.format( context.getString(R.string.download_canceled), rowTwo @@ -480,54 +486,6 @@ object VideoDownloadManager { return tempName.replace(" ", " ").trim(' ') } - @RequiresApi(Build.VERSION_CODES.Q) - private fun ContentResolver.getExistingFolderStartName(relativePath: String): List>? { - try { - val projection = arrayOf( - MediaStore.MediaColumns._ID, - MediaStore.MediaColumns.DISPLAY_NAME, // unused (for verification use only) - //MediaStore.MediaColumns.RELATIVE_PATH, // unused (for verification use only) - ) - - val selection = - "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath'" - - val result = this.query( - MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), - projection, selection, null, null - ) - val list = ArrayList>() - - result.use { c -> - if (c != null && c.count >= 1) { - c.moveToFirst() - while (true) { - val id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) - val name = - c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)) - val uri = ContentUris.withAppendedId( - MediaStore.Downloads.EXTERNAL_CONTENT_URI, id - ) - list.add(Pair(name, uri)) - if (c.isLast) { - break - } - c.moveToNext() - } - - /* - val cDisplayName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)) - val cRelativePath = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH))*/ - - } - } - return list - } catch (e: Exception) { - logError(e) - return null - } - } - /** * Used for getting video player subs. * @return List of pairs for the files in this format: @@ -538,76 +496,12 @@ object VideoDownloadManager { basePath: String? ): List>? { val base = basePathToFile(context, basePath) - val folder = base?.gotoDir(relativePath, false) + val folder = base?.gotoDir(relativePath, false) ?: return null + if (!folder.isDirectory) return null - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) { - return context.contentResolver?.getExistingFolderStartName(relativePath) - } else { -// val normalPath = -// "${Environment.getExternalStorageDirectory()}${File.separatorChar}${relativePath}".replace( -// '/', -// File.separatorChar -// ) -// val folder = File(normalPath) - if (folder?.isDirectory == true) { - return folder.listFiles()?.map { Pair(it.name ?: "", it.uri) } - } - } - return null -// } + return folder.listFiles()?.map { (it.name ?: "") to it.uri } } - @RequiresApi(Build.VERSION_CODES.Q) - private fun ContentResolver.getExistingDownloadUriOrNullQ( - relativePath: String, - displayName: String - ): Uri? { - try { - val projection = arrayOf( - MediaStore.MediaColumns._ID, - //MediaStore.MediaColumns.DISPLAY_NAME, // unused (for verification use only) - //MediaStore.MediaColumns.RELATIVE_PATH, // unused (for verification use only) - ) - - val selection = - "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath' AND " + "${MediaStore.MediaColumns.DISPLAY_NAME}='$displayName'" - - val result = this.query( - MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), - projection, selection, null, null - ) - - result.use { c -> - if (c != null && c.count >= 1) { - c.moveToFirst().let { - val id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) - /* - val cDisplayName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)) - val cRelativePath = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH))*/ - - return ContentUris.withAppendedId( - MediaStore.Downloads.EXTERNAL_CONTENT_URI, id - ) - } - } - } - return null - } catch (e: Exception) { - logError(e) - return null - } - } - - @RequiresApi(Build.VERSION_CODES.Q) - fun ContentResolver.getFileLength(fileUri: Uri): Long? { - return try { - this.openFileDescriptor(fileUri, "r") - .use { it?.statSize ?: 0 } - } catch (e: Exception) { - logError(e) - null - } - } data class CreateNotificationMetadata( val type: DownloadType, @@ -619,16 +513,39 @@ object VideoDownloadManager { ) data class StreamData( - val errorCode: Int, - val resume: Boolean? = null, - val fileLength: Long? = null, - val fileStream: OutputStream? = null, - ) + private val fileLength: Long, + val file: UniFile, + //val fileStream: OutputStream, + ) { + fun open() : OutputStream { + return file.openOutputStream(resume) + } + + fun openNew() : OutputStream { + return file.openOutputStream(false) + } + + val resume: Boolean get() = fileLength > 0L + val startAt: Long get() = if (resume) fileLength else 0L + val exists: Boolean get() = file.exists() + } + + + //class ADownloadException(val id: Int) : RuntimeException(message = "Download error $id") + + fun UniFile.createFileOrThrow(displayName: String): UniFile { + return this.createFile(displayName) ?: throw IOException("Could not create file") + } + + fun UniFile.deleteOrThrow() { + if (!this.delete()) throw IOException("Could not delete file") + } /** * Sets up the appropriate file and creates a data stream from the file. * Used for initializing downloads. * */ + @Throws(IOException::class) fun setupStream( context: Context, name: String, @@ -637,88 +554,24 @@ object VideoDownloadManager { tryResume: Boolean, ): StreamData { val displayName = getDisplayName(name, extension) - val fileStream: OutputStream - val fileLength: Long - var resume = tryResume - val baseFile = context.getBasePath() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.first?.isDownloadDir() == true) { - val cr = context.contentResolver ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) + val (baseFile, _) = context.getBasePath() - val currentExistingFile = - cr.getExistingDownloadUriOrNullQ( - folder ?: "", - displayName - ) // CURRENT FILE WITH THE SAME PATH + val subDir = baseFile?.gotoDir(folder) ?: throw IOException() + val foundFile = subDir.findFile(displayName) - fileLength = - if (currentExistingFile == null || !resume) 0 else (cr.getFileLength( - currentExistingFile - ) - ?: 0)// IF NOT RESUME THEN 0, OTHERWISE THE CURRENT FILE SIZE - - if (!resume && currentExistingFile != null) { // DELETE FILE IF FILE EXITS AND NOT RESUME - val rowsDeleted = context.contentResolver.delete(currentExistingFile, null, null) - if (rowsDeleted < 1) { - println("ERROR DELETING FILE!!!") - } - } - - var appendFile = false - val newFileUri = if (resume && currentExistingFile != null) { - appendFile = true - currentExistingFile - } else { - val contentUri = - MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI - //val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - val currentMimeType = when (extension) { - - // Absolutely ridiculous, if text/vtt is used as mimetype scoped storage prevents - // downloading to /Downloads yet it works with null - - "vtt" -> null // "text/vtt" - "mp4" -> "video/mp4" - "srt" -> null // "application/x-subrip"//"text/plain" - else -> null - } - val newFile = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) - put(MediaStore.MediaColumns.TITLE, name) - if (currentMimeType != null) - put(MediaStore.MediaColumns.MIME_TYPE, currentMimeType) - put(MediaStore.MediaColumns.RELATIVE_PATH, folder) - } - - cr.insert( - contentUri, - newFile - ) ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) - } - - fileStream = cr.openOutputStream(newFileUri, "w" + (if (appendFile) "a" else "")) - ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) + val (file, fileLength) = if (foundFile == null || !foundFile.exists()) { + subDir.createFileOrThrow(displayName) to 0L } else { - val subDir = baseFile.first?.gotoDir(folder) - val rFile = subDir?.findFile(displayName) - if (rFile?.exists() != true) { - fileLength = 0 - if (subDir?.createFile(displayName) == null) return StreamData(ERROR_CREATE_FILE) + if (tryResume) { + foundFile to foundFile.size() } else { - if (resume) { - fileLength = rFile.size() - } else { - fileLength = 0 - if (!rFile.delete()) return StreamData(ERROR_DELETING_FILE) - if (subDir.createFile(displayName) == null) return StreamData(ERROR_CREATE_FILE) - } + foundFile.deleteOrThrow() + subDir.createFileOrThrow(displayName) to 0L } - fileStream = (subDir.findFile(displayName) - ?: subDir.createFile(displayName))!!.openOutputStream() -// fileStream = FileOutputStream(rFile, false) - if (fileLength == 0L) resume = false } - return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream) + + return StreamData(fileLength, file) } /** This class handles the notifications, as well as the relevant key */ @@ -938,6 +791,8 @@ object VideoDownloadManager { fun setWrittenSegment(segmentIndex: Int) { hlsWrittenProgress = segmentIndex + 1 + // in case of abort we need to save every written progress + updateFileInfo() } } @@ -1185,18 +1040,16 @@ object VideoDownloadManager { // set up the download file val stream = setupStream(context, name, relativePath, extension, tryResume) - if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN - val resume = stream.resume ?: return@withContext ERROR_UNKNOWN - val fileLength = stream.fileLength ?: return@withContext ERROR_UNKNOWN - val resumeAt = (if (resume) fileLength else 0) - metadata.setResumeLength(resumeAt) + + fileStream = stream.open() + + metadata.setResumeLength(stream.startAt) metadata.type = DownloadType.IsPending val items = streamLazy( url = link.url.replace(" ", "%20"), referer = link.referer, - startByte = resumeAt, + startByte = stream.startAt, headers = link.headers.appendAndDontOverride( mapOf( "Accept-Encoding" to "identity", @@ -1230,6 +1083,19 @@ object VideoDownloadManager { val pendingData: HashMap = hashMapOf() + val fileChecker = launch(Dispatchers.IO) { + while (isActive) { + if (stream.exists) { + delay(5000) + continue + } + fileMutex.withLock { + metadata.type = DownloadType.IsStopped + } + break + } + } + val jobs = (0 until parallelConnections).map { launch(Dispatchers.IO) { @@ -1329,9 +1195,11 @@ object VideoDownloadManager { } jobs.join() + fileChecker.cancel() // jobs are finished so we don't want to stop them anymore metadata.removeStopListener() + if (!stream.exists) metadata.type = DownloadType.IsStopped if (metadata.type == DownloadType.IsFailed) { return@withContext ERROR_CONNECTION_ERROR @@ -1341,11 +1209,8 @@ object VideoDownloadManager { // we need to close before delete fileStream.closeQuietly() metadata.onDelete() - if (deleteFile(context, baseFile, relativePath ?: "", displayName)) { - return@withContext SUCCESS_STOPPED - } else { - return@withContext ERROR_DELETING_FILE - } + deleteFile(context, baseFile, relativePath ?: "", displayName) + return@withContext SUCCESS_STOPPED } metadata.type = DownloadType.IsDone @@ -1400,13 +1265,13 @@ object VideoDownloadManager { folder ) else folder val displayName = getDisplayName(name, extension) - val stream = setupStream(context, name, relativePath, extension, startAt > 0) - if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - if (stream.resume != true) startAt = 0 - fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN + val stream = + setupStream(context, name, relativePath, extension, startAt > 0) + if (!stream.resume) startAt = 0 + fileStream = stream.open() // push the metadata - metadata.setResumeLength(stream.fileLength ?: 0) + metadata.setResumeLength(stream.startAt) metadata.hlsProgress = startAt metadata.type = DownloadType.IsPending metadata.setDownloadFileInfoTemplate( @@ -1433,13 +1298,25 @@ object VideoDownloadManager { metadata.hlsTotal = items.size metadata.type = DownloadType.IsDownloading - val currentMutex = Mutex() - val current = (0 until items.size).iterator() + val current = (startAt until items.size).iterator() val fileMutex = Mutex() val pendingData: HashMap = hashMapOf() + val fileChecker = launch(Dispatchers.IO) { + while (isActive) { + if (stream.exists) { + delay(5000) + continue + } + fileMutex.withLock { + metadata.type = DownloadType.IsStopped + } + break + } + } + // see @downloadexplanation for explanation of this download strategy, // this keeps all jobs working at all times, // does several connections in parallel instead of a regular for loop to improve @@ -1476,7 +1353,7 @@ object VideoDownloadManager { // user pause while (metadata.type == DownloadType.IsPaused) delay(100) // if stopped then break to delete - if (metadata.type == DownloadType.IsStopped || !isActive) return@launch + if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed || !isActive) return@launch val segmentLength = bytes.size.toLong() // send notification, no matter the actual write order @@ -1499,11 +1376,13 @@ object VideoDownloadManager { val cacheLength = cache.size.toLong() fileStream.write(cache) + metadata.addBytesWritten(cacheLength) metadata.setWrittenSegment(metadata.hlsWrittenProgress) } } catch (t: Throwable) { // this is in case of write fail + logError(t) if (metadata.type != DownloadType.IsStopped) { metadata.type = DownloadType.IsFailed } @@ -1520,9 +1399,12 @@ object VideoDownloadManager { } jobs.join() + fileChecker.cancel() metadata.removeStopListener() + if (!stream.exists) metadata.type = DownloadType.IsStopped + if (metadata.type == DownloadType.IsFailed) { return@withContext ERROR_CONNECTION_ERROR } @@ -1531,11 +1413,8 @@ object VideoDownloadManager { // we need to close before delete fileStream.closeQuietly() metadata.onDelete() - if (deleteFile(context, baseFile, relativePath ?: "", displayName)) { - return@withContext SUCCESS_STOPPED - } else { - return@withContext ERROR_DELETING_FILE - } + deleteFile(context, baseFile, relativePath ?: "", displayName) + return@withContext SUCCESS_STOPPED } metadata.type = DownloadType.IsDone @@ -1564,6 +1443,11 @@ object VideoDownloadManager { directoryName: String?, createMissingDirectories: Boolean = true ): UniFile? { + if(directoryName == null) return this + + return directoryName.split(File.separatorChar).filter { it.isNotBlank() }.fold(this) { file: UniFile?, directory -> + file?.createDirectory(directory) + } // May give this error on scoped storage. // W/DocumentsContract: Failed to create document @@ -1571,7 +1455,7 @@ object VideoDownloadManager { // Not present in latest testing. -// println("Going to dir $directoryName from ${this.uri} ---- ${this.filePath}") + println("Going to dir $directoryName from ${this.uri} ---- ${this.filePath}") try { // Creates itself from parent if doesn't exist. @@ -1671,49 +1555,6 @@ object VideoDownloadManager { return this != null && this.filePath == getDownloadDir()?.filePath } - /*private fun delete( - context: Context, - name: String, - folder: String?, - extension: String, - parentId: Int?, - basePath: UniFile? - ): Int { - val displayName = getDisplayName(name, extension) - - // delete all subtitle files - if (extension != "vtt" && extension != "srt") { - try { - delete(context, name, folder, "vtt", parentId, basePath) - delete(context, name, folder, "srt", parentId, basePath) - } catch (e: Exception) { - logError(e) - } - } - - // If scoped storage and using download dir (not accessible with UniFile) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.isDownloadDir()) { - val relativePath = getRelativePath(folder) - val lastContent = - context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName) ?: return ERROR_DELETING_FILE - if(context.contentResolver.delete(lastContent, null, null) <= 0) { - return ERROR_DELETING_FILE - } - } else { - val dir = basePath?.gotoDir(folder) - val file = dir?.findFile(displayName) - val success = file?.delete() - if (success != true) return ERROR_DELETING_FILE else { - // Cleans up empty directory - if (dir.listFiles()?.isEmpty() == true) dir.delete() - } - parentId?.let { - downloadDeleteEvent.invoke(parentId) - } - } - return SUCCESS_STOPPED - }*/ - fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { return getFileName(context, metadata.name, metadata.episode, metadata.season) @@ -1765,70 +1606,60 @@ object VideoDownloadManager { } } - if (link.isM3u8 || URL(link.url).path.endsWith(".m3u8")) { - val startIndex = if (tryResume) { - context.getKey( - KEY_DOWNLOAD_INFO, - ep.id.toString(), - null - )?.extraInfo?.toIntOrNull() - } else null - return suspendSafeApiCall { - downloadHLS( + val callback: (CreateNotificationMetadata) -> Unit = { meta -> + main { + createNotification( + context, + source, + link.name, + ep, + meta.type, + meta.bytesDownloaded, + meta.bytesTotal, + notificationCallback, + meta.hlsProgress, + meta.hlsTotal, + meta.bytesPerSecond + ) + } + } + + try { + if (link.isM3u8 || normalSafeApiCall { URL(link.url).path.endsWith(".m3u8") } == true) { + val startIndex = if (tryResume) { + context.getKey( + KEY_DOWNLOAD_INFO, + ep.id.toString(), + null + )?.extraInfo?.toIntOrNull() + } else null + + return downloadHLS( context, link, name, folder, ep.id, startIndex, - createNotificationCallback = { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback, - meta.hlsProgress, - meta.hlsTotal, - meta.bytesPerSecond - ) - } - } + callback ) - }.also { - extractorJob.cancel() - } ?: ERROR_UNKNOWN + } else { + return downloadThing( + context, + link, + name, + folder, + "mp4", + tryResume, + ep.id, + callback + ) + } + } catch (t: Throwable) { + return ERROR_UNKNOWN + } finally { + extractorJob.cancel() } - - return suspendSafeApiCall { - downloadThing( - context, - link, - name, - folder, - "mp4", - tryResume, - ep.id, - createNotificationCallback = { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback, - bytesPerSecond = meta.bytesPerSecond - ) - } - }) - }.also { extractorJob.cancel() } ?: ERROR_UNKNOWN } suspend fun downloadCheck( @@ -1911,26 +1742,10 @@ object VideoDownloadManager { val info = context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return null val base = basePathToFile(context, info.basePath) + val file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName) + if (file?.exists() != true) return null - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) { - val cr = context.contentResolver ?: return null - val fileUri = - cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName) - ?: return null - val fileLength = cr.getFileLength(fileUri) ?: return null - if (fileLength == 0L) return null - return DownloadedFileInfoResult(fileLength, info.totalBytes, fileUri) - } else { - - val file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName) - -// val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName) -// val dFile = File(normalPath) - - if (file?.exists() != true) return null - - return DownloadedFileInfoResult(file.size(), info.totalBytes, file.uri) - } + return DownloadedFileInfoResult(file.size(), info.totalBytes, file.uri) } catch (e: Exception) { logError(e) return null @@ -1943,6 +1758,7 @@ object VideoDownloadManager { fun UniFile.size(): Long { val len = length() return if (len <= 1) { + println("LEN:::::::>>>>>>>>>>>>>>>>>>>>>>>$len") val inputStream = this.openInputStream() return inputStream.available().toLong().also { inputStream.closeQuietly() } } else { @@ -1962,32 +1778,20 @@ object VideoDownloadManager { relativePath: String, displayName: String ): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && folder.isDownloadDir()) { - val cr = context.contentResolver ?: return false - val fileUri = - cr.getExistingDownloadUriOrNullQ(relativePath, displayName) - ?: return true // FILE NOT FOUND, ALREADY DELETED - - return cr.delete(fileUri, null, null) > 0 // IF DELETED ROWS IS OVER 0 - } else { - val file = folder?.gotoDir(relativePath)?.findFile(displayName) -// val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName) -// val dFile = File(normalPath) - if (file?.exists() != true) return true - return try { - file.delete() - } catch (e: Exception) { - logError(e) - val cr = context.contentResolver - cr.delete(file.uri, null, null) > 0 - } + val file = folder?.gotoDir(relativePath)?.findFile(displayName) ?: return false + if (!file.exists()) return true + return try { + file.delete() + } catch (e: Exception) { + logError(e) + (context.contentResolver?.delete(file.uri, null, null) ?: return false) > 0 } } private fun deleteFile(context: Context, id: Int): Boolean { val info = context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return false - downloadEvent.invoke(Pair(id, DownloadActionType.Stop)) + downloadEvent.invoke(id to DownloadActionType.Stop) downloadProgressEvent.invoke(Triple(id, 0, 0)) downloadStatusEvent.invoke(id to DownloadType.IsStopped) downloadDeleteEvent.invoke(id) From 3ea6b1a8d507899ae5a9b295e9f29ded7e0e0448 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 06:25:06 +0200 Subject: [PATCH 190/570] fixed resume download + migrated filesystem to SafeFile --- .../ui/player/DownloadedPlayerActivity.kt | 4 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 8 +- .../ui/settings/SettingsGeneral.kt | 42 +- .../cloudstream3/utils/BackupUtils.kt | 23 +- .../utils/VideoDownloadManager.kt | 378 +++++++----------- .../cloudstream3/utils/storage/MediaFile.kt | 369 +++++++++++++++++ .../cloudstream3/utils/storage/SafeFile.kt | 244 +++++++++++ .../utils/storage/UniFileWrapper.kt | 116 ++++++ 8 files changed, 924 insertions(+), 260 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 6f40e145..03405faf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -6,11 +6,11 @@ import android.os.Bundle import android.util.Log import android.view.KeyEvent import androidx.appcompat.app.AppCompatActivity -import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.storage.SafeFile const val DTAG = "PlayerActivity" @@ -50,7 +50,7 @@ class DownloadedPlayerActivity : AppCompatActivity() { } private fun playUri(uri: Uri) { - val name = UniFile.fromUri(this, uri).name + val name = SafeFile.fromUri(this, uri)?.name() this.navigate( R.id.global_to_navigation_player, GeneratorPlayer.newInstance( DownloadFileGenerator( 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 9e601fc7..341b4ad3 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 @@ -52,6 +52,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.toPx +import com.lagradost.cloudstream3.utils.storage.SafeFile import kotlinx.coroutines.Job import java.util.* import kotlin.math.abs @@ -525,10 +526,11 @@ class GeneratorPlayer : FullScreenPlayer() { Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) - val file = UniFile.fromUri(ctx, uri) - println("Loaded subtitle file. Selected URI path: $uri - Name: ${file.name}") + val file = SafeFile.fromUri(ctx, uri) + val fileName = file?.name() + println("Loaded subtitle file. Selected URI path: $uri - Name: $fileName") // DO NOT REMOVE THE FILE EXTENSION FROM NAME, IT'S NEEDED FOR MIME TYPES - val name = file.name ?: uri.toString() + val name = fileName ?: uri.toString() val subtitleData = SubtitleData( name, 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 2c81ad1f..f46aac9b 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 @@ -3,9 +3,7 @@ package com.lagradost.cloudstream3.ui.settings import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Build import android.os.Bundle -import android.os.Environment import android.view.View import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts @@ -13,7 +11,6 @@ import androidx.appcompat.app.AlertDialog import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty -import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -41,7 +38,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath -import java.io.File +import com.lagradost.cloudstream3.utils.storage.SafeFile fun getCurrentLocale(context: Context): String { val res = context.resources @@ -139,8 +136,9 @@ class SettingsGeneral : PreferenceFragmentCompat() { context.contentResolver.takePersistableUriPermission(uri, flags) - val file = UniFile.fromUri(context, uri) - println("Selected URI path: $uri - Full path: ${file.filePath}") + val file = SafeFile.fromUri(context, uri) + val filePath = file?.filePath() + println("Selected URI path: $uri - Full path: $filePath") // Stores the real URI using download_path_key // Important that the URI is stored instead of filepath due to permissions. @@ -149,7 +147,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { // From URI -> File path // File path here is purely for cosmetic purposes in settings - (file.filePath ?: uri.toString()).let { + (filePath ?: uri.toString()).let { PreferenceManager.getDefaultSharedPreferences(context) .edit().putString(getString(R.string.download_path_pref), it).apply() } @@ -306,25 +304,23 @@ class SettingsGeneral : PreferenceFragmentCompat() { } return@setOnPreferenceClickListener true } + fun getDownloadDirs(): List { return normalSafeApiCall { - val defaultDir = VideoDownloadManager.getDownloadDir()?.filePath + context?.let { ctx -> + val defaultDir = VideoDownloadManager.getDefaultDir(ctx)?.filePath() - // app_name_download_path = Cloudstream and does not change depending on release. - // DOES NOT WORK ON SCOPED STORAGE. - val secondaryDir = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) null else Environment.getExternalStorageDirectory().absolutePath + - File.separator + resources.getString(R.string.app_name_download_path) - val first = listOf(defaultDir, secondaryDir) - (try { - val currentDir = context?.getBasePath()?.let { it.first?.filePath ?: it.second } + val first = listOf(defaultDir) + (try { + val currentDir = ctx.getBasePath().let { it.first?.filePath() ?: it.second } - (first + - requireContext().getExternalFilesDirs("").mapNotNull { it.path } + - currentDir) - } catch (e: Exception) { - first - }).filterNotNull().distinct() + (first + + ctx.getExternalFilesDirs("").mapNotNull { it.path } + + currentDir) + } catch (e: Exception) { + first + }).filterNotNull().distinct() + } } ?: emptyList() } @@ -339,7 +335,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { val currentDir = settingsManager.getString(getString(R.string.download_path_pref), null) - ?: VideoDownloadManager.getDownloadDir().toString() + ?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx).toString() } activity?.showBottomDialog( dirs + listOf("Custom"), 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 5bd0cd15..2da54678 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -1,11 +1,8 @@ package com.lagradost.cloudstream3.utils import android.annotation.SuppressLint -import android.content.ContentValues import android.content.Context import android.net.Uri -import android.os.Build -import android.provider.MediaStore import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -36,9 +33,9 @@ import com.lagradost.cloudstream3.utils.DataStore.mapper import com.lagradost.cloudstream3.utils.DataStore.setKeyRaw import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.requestRW -import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath -import com.lagradost.cloudstream3.utils.VideoDownloadManager.isDownloadDir -import java.io.IOException +import com.lagradost.cloudstream3.utils.VideoDownloadManager.setupStream +import okhttp3.internal.closeQuietly +import java.io.OutputStream import java.io.PrintWriter import java.lang.System.currentTimeMillis import java.text.SimpleDateFormat @@ -147,6 +144,8 @@ object BackupUtils { @SuppressLint("SimpleDateFormat") fun FragmentActivity.backup() { + var fileStream: OutputStream? = null + var printStream: PrintWriter? = null try { if (!checkWrite()) { showToast(getString(R.string.backup_failed), Toast.LENGTH_LONG) @@ -154,13 +153,16 @@ object BackupUtils { return } - val subDir = getBasePath().first val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) val ext = "json" val displayName = "CS3_Backup_${date}" val backupFile = getBackup() + val stream = setupStream(this, displayName, null, ext, false) + fileStream = stream.openNew() + printStream = PrintWriter(fileStream) + printStream.print(mapper.writeValueAsString(backupFile)) - val steam = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + /*val steam = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && subDir?.isDownloadDir() == true ) { val cr = this.contentResolver @@ -198,7 +200,7 @@ object BackupUtils { val printStream = PrintWriter(steam) printStream.print(mapper.writeValueAsString(backupFile)) - printStream.close() + printStream.close()*/ showToast( R.string.backup_success, @@ -214,6 +216,9 @@ object BackupUtils { } catch (e: Exception) { logError(e) } + } finally { + printStream?.closeQuietly() + fileStream?.closeQuietly() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index a81e4b3a..37c02be4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -8,7 +8,6 @@ import android.content.* import android.graphics.Bitmap import android.net.Uri import android.os.Build -import android.os.Environment import androidx.annotation.DrawableRes import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -20,7 +19,6 @@ import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager import com.bumptech.glide.load.model.GlideUrl import com.fasterxml.jackson.annotation.JsonProperty -import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -31,19 +29,19 @@ import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall -import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import com.lagradost.cloudstream3.utils.storage.MediaFileContentType +import com.lagradost.cloudstream3.utils.storage.SafeFile import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel -import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -160,24 +158,33 @@ object VideoDownloadManager { @JsonProperty("pkg") val pkg: DownloadResumePackage, ) - private const val SUCCESS_DOWNLOAD_DONE = 1 - private const val SUCCESS_STREAM = 3 - private const val SUCCESS_STOPPED = 2 + data class DownloadStatus( + /** if you should retry with the same args and hope for a better result */ + val retrySame: Boolean, + /** if you should try the next mirror */ + val tryNext: Boolean, + /** if the result is what the user intended */ + val success: Boolean, + ) - // will not download the next one, but is still classified as an error - private const val ERROR_DELETING_FILE = 3 - private const val ERROR_CREATE_FILE = -2 - private const val ERROR_UNKNOWN = -10 + /** Invalid input, just skip to the next one as the same args will give the same error */ + private val DOWNLOAD_INVALID_INPUT = + DownloadStatus(retrySame = false, tryNext = true, success = false) - //private const val ERROR_OPEN_FILE = -3 - private const val ERROR_TOO_SMALL_CONNECTION = -4 + /** no need to try any other mirror as we have downloaded the file */ + private val DOWNLOAD_SUCCESS = + DownloadStatus(retrySame = false, tryNext = false, success = true) - //private const val ERROR_WRONG_CONTENT = -5 - private const val ERROR_CONNECTION_ERROR = -6 + /** the user pressed stop, so no need to download anything else */ + private val DOWNLOAD_STOPPED = + DownloadStatus(retrySame = false, tryNext = false, success = true) - //private const val ERROR_MEDIA_STORE_URI_CANT_BE_CREATED = -7 - //private const val ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM = -8 - private const val ERROR_CONTENT_RESOLVER_NOT_FOUND = -9 + /** the process failed due to some reason, so we retry and also try the next mirror */ + private val DOWNLOAD_FAILED = DownloadStatus(retrySame = true, tryNext = true, success = false) + + /** bad config, skip all mirrors as every call to download will have the same bad config */ + private val DOWNLOAD_BAD_CONFIG = + DownloadStatus(retrySame = false, tryNext = false, success = false) private const val KEY_RESUME_PACKAGES = "download_resume" const val KEY_DOWNLOAD_INFO = "download_info" @@ -209,15 +216,15 @@ object VideoDownloadManager { } } - /** Will return IsDone if not found or error */ - fun getDownloadState(id: Int): DownloadType { - return try { - downloadStatus[id] ?: DownloadType.IsDone - } catch (e: Exception) { - logError(e) - DownloadType.IsDone - } - } + ///** Will return IsDone if not found or error */ + //fun getDownloadState(id: Int): DownloadType { + // return try { + // downloadStatus[id] ?: DownloadType.IsDone + // } catch (e: Exception) { + // logError(e) + // DownloadType.IsDone + // } + //} private val cachedBitmaps = hashMapOf() fun Context.getImageBitmapFromUrl(url: String, headers: Map? = null): Bitmap? { @@ -302,7 +309,7 @@ object VideoDownloadManager { if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { builder.setProgress((total / 1000).toInt(), (progress / 1000).toInt(), false) } else if (state == DownloadType.IsPending) { - builder.setProgress(0,0,true) + builder.setProgress(0, 0, true) } val rowTwoExtra = if (ep.name != null) " - ${ep.name}\n" else "" @@ -496,10 +503,11 @@ object VideoDownloadManager { basePath: String? ): List>? { val base = basePathToFile(context, basePath) - val folder = base?.gotoDir(relativePath, false) ?: return null - if (!folder.isDirectory) return null + val folder = base?.gotoDirectory(relativePath, false) ?: return null + if (folder.isDirectory() != false) return null - return folder.listFiles()?.map { (it.name ?: "") to it.uri } + return folder.listFiles() + ?.mapNotNull { (it.name() ?: "") to (it.uri() ?: return@mapNotNull null) } } @@ -514,37 +522,29 @@ object VideoDownloadManager { data class StreamData( private val fileLength: Long, - val file: UniFile, + val file: SafeFile, //val fileStream: OutputStream, ) { - fun open() : OutputStream { - return file.openOutputStream(resume) + @Throws(IOException::class) + fun open(): OutputStream { + return file.openOutputStreamOrThrow(resume) } - fun openNew() : OutputStream { - return file.openOutputStream(false) + @Throws(IOException::class) + fun openNew(): OutputStream { + return file.openOutputStreamOrThrow(false) + } + + fun delete(): Boolean { + return file.delete() } val resume: Boolean get() = fileLength > 0L val startAt: Long get() = if (resume) fileLength else 0L - val exists: Boolean get() = file.exists() + val exists: Boolean get() = file.exists() == true } - //class ADownloadException(val id: Int) : RuntimeException(message = "Download error $id") - - fun UniFile.createFileOrThrow(displayName: String): UniFile { - return this.createFile(displayName) ?: throw IOException("Could not create file") - } - - fun UniFile.deleteOrThrow() { - if (!this.delete()) throw IOException("Could not delete file") - } - - /** - * Sets up the appropriate file and creates a data stream from the file. - * Used for initializing downloads. - * */ @Throws(IOException::class) fun setupStream( context: Context, @@ -552,19 +552,39 @@ object VideoDownloadManager { folder: String?, extension: String, tryResume: Boolean, + ): StreamData { + val (base, _) = context.getBasePath() + return setupStream( + base ?: throw IOException("Bad config"), + name, + folder, + extension, + tryResume + ) + } + + /** + * Sets up the appropriate file and creates a data stream from the file. + * Used for initializing downloads. + * */ + @Throws(IOException::class) + fun setupStream( + baseFile: SafeFile, + name: String, + folder: String?, + extension: String, + tryResume: Boolean, ): StreamData { val displayName = getDisplayName(name, extension) - val (baseFile, _) = context.getBasePath() - - val subDir = baseFile?.gotoDir(folder) ?: throw IOException() + val subDir = baseFile.gotoDirectoryOrThrow(folder) val foundFile = subDir.findFile(displayName) - val (file, fileLength) = if (foundFile == null || !foundFile.exists()) { + val (file, fileLength) = if (foundFile == null || foundFile.exists() != true) { subDir.createFileOrThrow(displayName) to 0L } else { if (tryResume) { - foundFile to foundFile.size() + foundFile to foundFile.lengthOrThrow() } else { foundFile.deleteOrThrow() subDir.createFileOrThrow(displayName) to 0L @@ -1004,21 +1024,20 @@ object VideoDownloadManager { } } - @Throws suspend fun downloadThing( context: Context, link: IDownloadableMinimum, name: String, - folder: String?, + folder: String, extension: String, tryResume: Boolean, parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, parallelConnections: Int = 3 - ): Int = withContext(Dispatchers.IO) { + ): DownloadStatus = withContext(Dispatchers.IO) { // we cant download torrents with this implementation, aria2c might be used in the future - if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { - return@withContext ERROR_UNKNOWN + if (link.url.startsWith("magnet") || link.url.endsWith(".torrent") || parallelConnections < 1) { + return@withContext DOWNLOAD_INVALID_INPUT } var fileStream: OutputStream? = null @@ -1033,13 +1052,10 @@ object VideoDownloadManager { // get the file path val (baseFile, basePath) = context.getBasePath() val displayName = getDisplayName(name, extension) - val relativePath = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.isDownloadDir()) getRelativePath( - folder - ) else folder + if (baseFile == null) return@withContext DOWNLOAD_BAD_CONFIG // set up the download file - val stream = setupStream(context, name, relativePath, extension, tryResume) + val stream = setupStream(baseFile, name, folder, extension, tryResume) fileStream = stream.open() @@ -1069,7 +1085,7 @@ object VideoDownloadManager { metadata.setDownloadFileInfoTemplate( DownloadedFileInfo( totalBytes = metadata.approxTotalBytes, - relativePath = relativePath ?: "", + relativePath = folder, displayName = displayName, basePath = basePath ) @@ -1202,19 +1218,19 @@ object VideoDownloadManager { if (!stream.exists) metadata.type = DownloadType.IsStopped if (metadata.type == DownloadType.IsFailed) { - return@withContext ERROR_CONNECTION_ERROR + return@withContext DOWNLOAD_FAILED } if (metadata.type == DownloadType.IsStopped) { // we need to close before delete fileStream.closeQuietly() metadata.onDelete() - deleteFile(context, baseFile, relativePath ?: "", displayName) - return@withContext SUCCESS_STOPPED + stream.delete() + return@withContext DOWNLOAD_STOPPED } metadata.type = DownloadType.IsDone - return@withContext SUCCESS_DOWNLOAD_DONE + return@withContext DOWNLOAD_SUCCESS } catch (e: IOException) { // some sort of IO error, this should not happened // we just rethrow it @@ -1226,7 +1242,7 @@ object VideoDownloadManager { // note that when failing we don't want to delete the file, // only user interaction has that power metadata.type = DownloadType.IsFailed - return@withContext ERROR_CONNECTION_ERROR + return@withContext DOWNLOAD_FAILED } finally { fileStream?.closeQuietly() //requestStream?.closeQuietly() @@ -1234,39 +1250,36 @@ object VideoDownloadManager { } } - @Throws private suspend fun downloadHLS( context: Context, link: ExtractorLink, name: String, - folder: String?, + folder: String, parentId: Int?, startIndex: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, parallelConnections: Int = 3 - ): Int = withContext(Dispatchers.IO) { - require(parallelConnections >= 1) + ): DownloadStatus = withContext(Dispatchers.IO) { + if (parallelConnections < 1) return@withContext DOWNLOAD_INVALID_INPUT val metadata = DownloadMetaData( createNotificationCallback = createNotificationCallback, id = parentId ) - val extension = "mp4" - var fileStream: OutputStream? = null try { + val extension = "mp4" + // the start .ts index var startAt = startIndex ?: 0 // set up the file data val (baseFile, basePath) = context.getBasePath() - val relativePath = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.isDownloadDir()) getRelativePath( - folder - ) else folder + if (baseFile == null) return@withContext DOWNLOAD_BAD_CONFIG + val displayName = getDisplayName(name, extension) val stream = - setupStream(context, name, relativePath, extension, startAt > 0) + setupStream(baseFile, name, folder, extension, startAt > 0) if (!stream.resume) startAt = 0 fileStream = stream.open() @@ -1277,7 +1290,7 @@ object VideoDownloadManager { metadata.setDownloadFileInfoTemplate( DownloadedFileInfo( totalBytes = 0, - relativePath = relativePath ?: "", + relativePath = folder, displayName = displayName, basePath = basePath ) @@ -1406,99 +1419,29 @@ object VideoDownloadManager { if (!stream.exists) metadata.type = DownloadType.IsStopped if (metadata.type == DownloadType.IsFailed) { - return@withContext ERROR_CONNECTION_ERROR + return@withContext DOWNLOAD_FAILED } if (metadata.type == DownloadType.IsStopped) { // we need to close before delete fileStream.closeQuietly() metadata.onDelete() - deleteFile(context, baseFile, relativePath ?: "", displayName) - return@withContext SUCCESS_STOPPED + stream.delete() + return@withContext DOWNLOAD_STOPPED } metadata.type = DownloadType.IsDone - return@withContext SUCCESS_DOWNLOAD_DONE + return@withContext DOWNLOAD_SUCCESS } catch (t: Throwable) { logError(t) metadata.type = DownloadType.IsFailed - return@withContext ERROR_UNKNOWN + return@withContext DOWNLOAD_FAILED } finally { fileStream?.closeQuietly() metadata.close() } } - - /** - * Guarantees a directory is present with the dir name (if createMissingDirectories is true). - * Works recursively when '/' is present. - * Will remove any file with the dir name if present and add directory. - * Will not work if the parent directory does not exist. - * - * @param directoryName if null will use the current path. - * @return UniFile / null if createMissingDirectories = false and folder is not found. - * */ - private fun UniFile.gotoDir( - directoryName: String?, - createMissingDirectories: Boolean = true - ): UniFile? { - if(directoryName == null) return this - - return directoryName.split(File.separatorChar).filter { it.isNotBlank() }.fold(this) { file: UniFile?, directory -> - file?.createDirectory(directory) - } - - // May give this error on scoped storage. - // W/DocumentsContract: Failed to create document - // java.lang.IllegalArgumentException: Parent document isn't a directory - - // Not present in latest testing. - - println("Going to dir $directoryName from ${this.uri} ---- ${this.filePath}") - - try { - // Creates itself from parent if doesn't exist. - if (!this.exists() && createMissingDirectories && !this.name.isNullOrBlank()) { - if (this.parentFile != null) { - this.parentFile?.createDirectory(this.name) - } else if (this.filePath != null) { - UniFile.fromFile(File(this.filePath!!).parentFile)?.createDirectory(this.name) - } - } - - val allDirectories = directoryName?.split("/") - return if (allDirectories?.size == 1 || allDirectories == null) { - val found = this.findFile(directoryName) - when { - directoryName.isNullOrBlank() -> this - found?.isDirectory == true -> found - - !createMissingDirectories -> null - // Below creates directories - found?.isFile == true -> { - found.delete() - this.createDirectory(directoryName) - } - - this.isDirectory -> this.createDirectory(directoryName) - else -> this.parentFile?.createDirectory(directoryName) - } - } else { - var currentDirectory = this - allDirectories.forEach { - // If the next directory is not found it returns the deepest directory possible. - val nextDir = currentDirectory.gotoDir(it, createMissingDirectories) - currentDirectory = nextDir ?: return null - } - currentDirectory - } - } catch (e: Exception) { - logError(e) - return null - } - } - private fun getDisplayName(name: String, extension: String): String { return "$name.$extension" } @@ -1510,33 +1453,22 @@ object VideoDownloadManager { * As of writing UniFile is used for everything but download directory on scoped storage. * Special ContentResolver fuckery is needed for that as UniFile doesn't work. * */ - fun getDownloadDir(): UniFile? { + fun getDefaultDir(context: Context): SafeFile? { // See https://www.py4u.net/discuss/614761 - return UniFile.fromFile( - File( - Environment.getExternalStorageDirectory().absolutePath + File.separatorChar + - Environment.DIRECTORY_DOWNLOADS - ) + return SafeFile.fromMedia( + context, MediaFileContentType.Downloads ) } - @Deprecated("TODO fix UniFile to work with download directory.") - private fun getRelativePath(folder: String?): String { - return (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace( - '/', - File.separatorChar - ).replace("${File.separatorChar}${File.separatorChar}", File.separatorChar.toString()) - } - /** * Turns a string to an UniFile. Used for stored string paths such as settings. * Should only be used to get a download path. * */ - private fun basePathToFile(context: Context, path: String?): UniFile? { + private fun basePathToFile(context: Context, path: String?): SafeFile? { return when { - path.isNullOrBlank() -> getDownloadDir() - path.startsWith("content://") -> UniFile.fromUri(context, path.toUri()) - else -> UniFile.fromFile(File(path)) + path.isNullOrBlank() -> getDefaultDir(context) + path.startsWith("content://") -> SafeFile.fromUri(context, path.toUri()) + else -> SafeFile.fromFile(context, File(path)) } } @@ -1545,17 +1477,12 @@ object VideoDownloadManager { * Returns the file and a string to be stored for future file retrieval. * UniFile.filePath is not sufficient for storage. * */ - fun Context.getBasePath(): Pair { + fun Context.getBasePath(): Pair { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val basePathSetting = settingsManager.getString(getString(R.string.download_path_key), null) return basePathToFile(this, basePathSetting) to basePathSetting } - fun UniFile?.isDownloadDir(): Boolean { - return this != null && this.filePath == getDownloadDir()?.filePath - } - - fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { return getFileName(context, metadata.name, metadata.episode, metadata.season) } @@ -1596,7 +1523,7 @@ object VideoDownloadManager { link: ExtractorLink, notificationCallback: (Int, Notification) -> Unit, tryResume: Boolean = false, - ): Int { + ): DownloadStatus { val name = getFileName(context, ep) // Make sure this is cancelled when download is done or cancelled. @@ -1638,7 +1565,7 @@ object VideoDownloadManager { context, link, name, - folder, + folder ?: "", ep.id, startIndex, callback @@ -1648,7 +1575,7 @@ object VideoDownloadManager { context, link, name, - folder, + folder ?: "", "mp4", tryResume, ep.id, @@ -1656,7 +1583,7 @@ object VideoDownloadManager { ) } } catch (t: Throwable) { - return ERROR_UNKNOWN + return DOWNLOAD_FAILED } finally { extractorJob.cancel() } @@ -1698,10 +1625,8 @@ object VideoDownloadManager { notificationCallback, resume ) - //.also { println("Single episode finished with return code: $it") } - // retry every link at least once - if (connectionResult <= 0) { + if (connectionResult.retrySame) { connectionResult = downloadSingleEpisode( context, item.source, @@ -1713,11 +1638,12 @@ object VideoDownloadManager { ) } - if (connectionResult > 0) { // SUCCESS + if (connectionResult.success) { // SUCCESS removeKey(KEY_RESUME_PACKAGES, id.toString()) break - } else if (index == item.links.lastIndex) { + } else if (!connectionResult.tryNext || index >= item.links.lastIndex) { downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) + break } } } catch (e: Exception) { @@ -1731,62 +1657,69 @@ object VideoDownloadManager { // return id } - fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? { - val res = getDownloadFileInfo(context, id) - if (res == null) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) - return res + /* fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? { + val res = getDownloadFileInfo(context, id) + if (res == null) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) + return res + } + */ + fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? = + getDownloadFileInfo(context, id, removeKeys = true) + + private fun DownloadedFileInfo.toFile(context: Context): SafeFile? { + return basePathToFile(context, this.basePath)?.gotoDirectory(relativePath) + ?.findFile(displayName) } - private fun getDownloadFileInfo(context: Context, id: Int): DownloadedFileInfoResult? { + private fun getDownloadFileInfo( + context: Context, + id: Int, + removeKeys: Boolean = false + ): DownloadedFileInfoResult? { try { val info = context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return null - val base = basePathToFile(context, info.basePath) - val file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName) - if (file?.exists() != true) return null + val file = info.toFile(context) - return DownloadedFileInfoResult(file.size(), info.totalBytes, file.uri) + // only delete the key if the file is not found + if (file == null || !file.existsOrThrow()) { + if (removeKeys) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) + return null + } + + return DownloadedFileInfoResult( + file.lengthOrThrow(), + info.totalBytes, + file.uriOrThrow() + ) } catch (e: Exception) { logError(e) return null } } - /** - * Gets the true download size as Scoped Storage sometimes wrongly returns 0. - * */ - fun UniFile.size(): Long { - val len = length() - return if (len <= 1) { - println("LEN:::::::>>>>>>>>>>>>>>>>>>>>>>>$len") - val inputStream = this.openInputStream() - return inputStream.available().toLong().also { inputStream.closeQuietly() } - } else { - len - } - } - fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { val success = deleteFile(context, id) if (success) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) return success } - private fun deleteFile( + /*private fun deleteFile( context: Context, - folder: UniFile?, + folder: SafeFile?, relativePath: String, displayName: String ): Boolean { - val file = folder?.gotoDir(relativePath)?.findFile(displayName) ?: return false - if (!file.exists()) return true + val file = folder?.gotoDirectory(relativePath)?.findFile(displayName) ?: return false + if (file.exists() == false) return true return try { file.delete() } catch (e: Exception) { logError(e) - (context.contentResolver?.delete(file.uri, null, null) ?: return false) > 0 + (context.contentResolver?.delete(file.uri() ?: return true, null, null) + ?: return false) > 0 } - } + }*/ private fun deleteFile(context: Context, id: Int): Boolean { val info = @@ -1795,8 +1728,7 @@ object VideoDownloadManager { downloadProgressEvent.invoke(Triple(id, 0, 0)) downloadStatusEvent.invoke(id to DownloadType.IsStopped) downloadDeleteEvent.invoke(id) - val base = basePathToFile(context, info.basePath) - return deleteFile(context, base, info.relativePath, info.displayName) + return info.toFile(context)?.delete() ?: false } fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt new file mode 100644 index 00000000..83c66b8b --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt @@ -0,0 +1,369 @@ +package com.lagradost.cloudstream3.utils.storage + +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import androidx.annotation.RequiresApi +import com.hippo.unifile.UniRandomAccessFile +import com.lagradost.cloudstream3.mvvm.logError +import okhttp3.internal.closeQuietly +import java.io.File +import java.io.InputStream +import java.io.OutputStream + + +enum class MediaFileContentType { + Downloads, + Audio, + Video, + Images, +} + +// https://developer.android.com/training/data-storage/shared/media +fun MediaFileContentType.toPath(): String { + return when (this) { + MediaFileContentType.Downloads -> Environment.DIRECTORY_DOWNLOADS + MediaFileContentType.Audio -> Environment.DIRECTORY_MUSIC + MediaFileContentType.Video -> Environment.DIRECTORY_MOVIES + MediaFileContentType.Images -> Environment.DIRECTORY_DCIM + } +} + +fun MediaFileContentType.defaultPrefix(): String { + return Environment.getExternalStorageDirectory().absolutePath +} + +fun MediaFileContentType.toAbsolutePath(): String { + return defaultPrefix() + File.separator + + this.toPath() +} + +fun replaceDuplicateFileSeparators(path: String): String { + return path.replace(Regex("${File.separator}+"), File.separator) +} + +@RequiresApi(Build.VERSION_CODES.Q) +fun MediaFileContentType.toUri(external: Boolean): Uri { + val volume = if (external) MediaStore.VOLUME_EXTERNAL_PRIMARY else MediaStore.VOLUME_INTERNAL + return when (this) { + MediaFileContentType.Downloads -> MediaStore.Downloads.getContentUri(volume) + MediaFileContentType.Audio -> MediaStore.Audio.Media.getContentUri(volume) + MediaFileContentType.Video -> MediaStore.Video.Media.getContentUri(volume) + MediaFileContentType.Images -> MediaStore.Images.Media.getContentUri(volume) + } +} + +@RequiresApi(Build.VERSION_CODES.Q) +class MediaFile( + private val context: Context, + private val folderType: MediaFileContentType, + private val external: Boolean = true, + absolutePath: String, +) : SafeFile { + // this is the path relative to the download directory so "/hello/text.txt" = "hello/text.txt" is in fact "Download/hello/text.txt" + private val sanitizedAbsolutePath: String = + replaceDuplicateFileSeparators(absolutePath) + + // this is only a directory if the filepath ends with a / + private val isDir: Boolean = sanitizedAbsolutePath.endsWith(File.separator) + private val isFile: Boolean = !isDir + + // this is the relative path including the Download directory, so "/hello/text.txt" => "Download/hello" + private val relativePath: String = + replaceDuplicateFileSeparators(folderType.toPath() + File.separator + sanitizedAbsolutePath).substringBeforeLast( + File.separator + ) + + // "/hello/text.txt" => "text.txt" + private val namePath: String = sanitizedAbsolutePath.substringAfterLast(File.separator) + private val baseUri = folderType.toUri(external) + private val contentResolver: ContentResolver = context.contentResolver + + init { + // some standard asserts that should always be hold or else this class wont work + assert(!relativePath.endsWith(File.separator)) + assert(!(isDir && isFile)) + assert(!relativePath.contains(File.separator + File.separator)) + assert(!namePath.contains(File.separator)) + + if (isDir) { + assert(namePath.isBlank()) + } else { + assert(namePath.isNotBlank()) + } + } + + companion object { + private fun splitFilenameExt(name: String): Pair { + val split = name.indexOfLast { it == '.' } + if (split <= 0) return name to null + val ext = name.substring(split + 1 until name.length) + if (ext.isBlank()) return name to null + + return name.substring(0 until split) to ext + } + + private fun splitFilenameMime(name: String): Pair { + val (display, ext) = splitFilenameExt(name) + val mimeType = when (ext) { + + // Absolutely ridiculous, if text/vtt is used as mimetype scoped storage prevents + // downloading to /Downloads yet it works with null + + "vtt" -> null // "text/vtt" + "mp4" -> "video/mp4" + "srt" -> null // "application/x-subrip"//"text/plain" + else -> null + } + return display to mimeType + } + } + + private fun appendRelativePath(path: String, folder: Boolean): MediaFile? { + if (isFile) return null + + // VideoDownloadManager.sanitizeFilename(path.replace(File.separator, "")) + + val newPath = + sanitizedAbsolutePath + path + if (folder) File.separator else "" + + return MediaFile( + context = context, + folderType = folderType, + external = external, + absolutePath = newPath + ) + } + + private fun createUri(displayName: String? = namePath): Uri? { + if (displayName == null) return null + if (isFile) return null + val (name, mime) = splitFilenameMime(displayName) + + val newFile = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) + put(MediaStore.MediaColumns.TITLE, name) + if (mime != null) + put(MediaStore.MediaColumns.MIME_TYPE, mime) + put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) + } + return contentResolver.insert(baseUri, newFile) + } + + override fun createFile(displayName: String?): SafeFile? { + if (isFile || displayName == null) return null + query(displayName)?.uri ?: createUri(displayName) ?: return null + return appendRelativePath(displayName, false) //SafeFile.fromUri(context, ?: return null) + } + + override fun createDirectory(directoryName: String?): SafeFile? { + if (directoryName == null) return null + // we don't create a dir here tbh, just fake create it + return appendRelativePath(directoryName, true) + } + + private data class QueryResult( + val uri: Uri, + val lastModified: Long, + val length: Long, + ) + + @RequiresApi(Build.VERSION_CODES.Q) + private fun query(displayName: String = namePath): QueryResult? { + try { + //val (name, mime) = splitFilenameMime(fullName) + + val projection = arrayOf( + MediaStore.MediaColumns._ID, + MediaStore.MediaColumns.DATE_MODIFIED, + MediaStore.MediaColumns.SIZE, + ) + + val selection = + "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}' AND ${MediaStore.MediaColumns.DISPLAY_NAME}='$displayName'" + + contentResolver.query( + baseUri, + projection, selection, null, null + )?.use { cursor -> + while (cursor.moveToNext()) { + val id = + cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) + + return QueryResult( + uri = ContentUris.withAppendedId( + baseUri, id + ), + lastModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)), + length = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)), + ) + } + } + } catch (t: Throwable) { + logError(t) + } + + return null + } + + override fun uri(): Uri? { + return query()?.uri + } + + override fun name(): String? { + if (isDir) return null + return namePath + } + + override fun type(): String? { + TODO("Not yet implemented") + } + + override fun filePath(): String { + return replaceDuplicateFileSeparators(relativePath + File.separator + namePath) + } + + override fun isDirectory(): Boolean { + return isDir + } + + override fun isFile(): Boolean { + return isFile + } + + override fun lastModified(): Long? { + if (isDir) return null + return query()?.lastModified + } + + override fun length(): Long? { + if (isDir) return null + val length = query()?.length ?: return null + if(length <= 0) { + val inputStream : InputStream = openInputStream() ?: return null + return try { + inputStream.available().toLong() + } catch (t : Throwable) { + null + } finally { + inputStream.closeQuietly() + } + } + return length + } + + override fun canRead(): Boolean { + TODO("Not yet implemented") + } + + override fun canWrite(): Boolean { + TODO("Not yet implemented") + } + + private fun delete(uri: Uri): Boolean { + return contentResolver.delete(uri, null, null) > 0 + } + + override fun delete(): Boolean { + return if (isDir) { + (listFiles() ?: return false).all { + it.delete() + } + } else { + delete(uri() ?: return false) + } + } + + override fun exists(): Boolean { + if (isDir) return true + return query() != null + } + + override fun listFiles(): List? { + if (isFile) return null + try { + val projection = arrayOf( + MediaStore.MediaColumns.DISPLAY_NAME + ) + + val selection = + "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}'" + contentResolver.query( + baseUri, + projection, selection, null, null + )?.use { cursor -> + val out = ArrayList(cursor.count) + while (cursor.moveToNext()) { + val nameIdx = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) + if (nameIdx == -1) continue + val name = cursor.getString(nameIdx) + + appendRelativePath(name, false)?.let { new -> + out.add(new) + } + } + + out + } + } catch (t: Throwable) { + logError(t) + } + return null + } + + override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? { + if (isFile || displayName == null) return null + + val new = appendRelativePath(displayName, false) ?: return null + if (new.exists()) { + return new + } + + return null//SafeFile.fromUri(context, query(displayName ?: return null)?.uri ?: return null) + } + + override fun renameTo(name: String?): Boolean { + TODO("Not yet implemented") + } + + override fun openOutputStream(append: Boolean): OutputStream? { + try { + // use current file + uri()?.let { + return contentResolver.openOutputStream( + it, + if (append) "wa" else "wt" + ) + } + + // create a new file if current is not found, + // as we know it is new only write access is needed + createUri()?.let { + return contentResolver.openOutputStream( + it, + "w" + ) + } + return null + } catch (t: Throwable) { + return null + } + } + + override fun openInputStream(): InputStream? { + try { + return contentResolver.openInputStream(uri() ?: return null) + } catch (t: Throwable) { + return null + } + } + + override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt new file mode 100644 index 00000000..9ba0ef88 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt @@ -0,0 +1,244 @@ +package com.lagradost.cloudstream3.utils.storage + +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import com.hippo.unifile.UniFile +import com.hippo.unifile.UniRandomAccessFile + +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +interface SafeFile { + companion object { + fun fromUri(context: Context, uri: Uri): SafeFile? { + return UniFileWrapper(UniFile.fromUri(context, uri) ?: return null) + } + + fun fromFile(context: Context, file: File?): SafeFile? { + if (file == null) return null + // because UniFile sucks balls on Media we have to do this + val absPath = file.absolutePath.removePrefix(File.separator) + for (value in MediaFileContentType.values()) { + val prefixes = listOf(value.toAbsolutePath(), value.toPath()) + for (prefix in prefixes) { + if (!absPath.startsWith(prefix)) continue + return fromMedia( + context, + value, + absPath.removePrefix(prefix).ifBlank { File.separator } + ) + } + } + + return UniFileWrapper(UniFile.fromFile(file) ?: return null) + } + + fun fromAsset( + context: Context, + filename: String? + ): SafeFile? { + return UniFileWrapper( + UniFile.fromAsset(context.assets, filename ?: return null) ?: return null + ) + } + + fun fromResource( + context: Context, + id: Int + ): SafeFile? { + return UniFileWrapper( + UniFile.fromResource(context, id) ?: return null + ) + } + + fun fromMedia( + context: Context, + folderType: MediaFileContentType, + path: String = File.separator, + external: Boolean = true, + ): SafeFile? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + //fromUri(context, folderType.toUri(external))?.findFile(folderType.toPath())?.gotoDirectory(path) + + return MediaFile( + context = context, + folderType = folderType, + external = external, + absolutePath = path + ) + } else { + fromFile( + context, + File( + (Environment.getExternalStorageDirectory().absolutePath + File.separator + + folderType.toPath() + File.separator + folderType).replace( + File.separator + File.separator, + File.separator + ) + ) + ) + } + + } + } + + /*val uri: Uri? get() = getUri() + val name: String? get() = getName() + val type: String? get() = getType() + val filePath: String? get() = getFilePath() + val isFile: Boolean? get() = isFile() + val isDirectory: Boolean? get() = isDirectory() + val length: Long? get() = length() + val canRead: Boolean get() = canRead() + val canWrite: Boolean get() = canWrite() + val lastModified: Long? get() = lastModified()*/ + + @Throws(IOException::class) + fun isFileOrThrow(): Boolean { + return isFile() ?: throw IOException("Unable to get if file is a file or directory") + } + + @Throws(IOException::class) + fun lengthOrThrow(): Long { + return length() ?: throw IOException("Unable to get file length") + } + + @Throws(IOException::class) + fun isDirectoryOrThrow(): Boolean { + return isDirectory() ?: throw IOException("Unable to get if file is a directory") + } + + @Throws(IOException::class) + fun filePathOrThrow(): String { + return filePath() ?: throw IOException("Unable to get file path") + } + + @Throws(IOException::class) + fun uriOrThrow(): Uri { + return uri() ?: throw IOException("Unable to get uri") + } + + @Throws(IOException::class) + fun renameOrThrow(name: String?) { + if (!renameTo(name)) { + throw IOException("Unable to rename to $name") + } + } + + @Throws(IOException::class) + fun openOutputStreamOrThrow(append: Boolean = false): OutputStream { + return openOutputStream(append) ?: throw IOException("Unable to open output stream") + } + + @Throws(IOException::class) + fun openInputStreamOrThrow(): InputStream { + return openInputStream() ?: throw IOException("Unable to open input stream") + } + + @Throws(IOException::class) + fun existsOrThrow(): Boolean { + return exists() ?: throw IOException("Unable get if file exists") + } + + @Throws(IOException::class) + fun findFileOrThrow(displayName: String?, ignoreCase: Boolean = false): SafeFile { + return findFile(displayName, ignoreCase) ?: throw IOException("Unable find file") + } + + @Throws(IOException::class) + fun gotoDirectoryOrThrow( + directoryName: String?, + createMissingDirectories: Boolean = true + ): SafeFile { + return gotoDirectory(directoryName, createMissingDirectories) + ?: throw IOException("Unable to go to directory $directoryName") + } + + @Throws(IOException::class) + fun listFilesOrThrow(): List { + return listFiles() ?: throw IOException("Unable to get files") + } + + + @Throws(IOException::class) + fun createFileOrThrow(displayName: String?): SafeFile { + return createFile(displayName) ?: throw IOException("Unable to create file $displayName") + } + + @Throws(IOException::class) + fun createDirectoryOrThrow(directoryName: String?): SafeFile { + return createDirectory( + directoryName ?: throw IOException("Unable to create file with invalid name") + ) + ?: throw IOException("Unable to create directory $directoryName") + } + + @Throws(IOException::class) + fun deleteOrThrow() { + if (!delete()) { + throw IOException("Unable to delete file") + } + } + + /** file.gotoDirectory("a/b/c") -> "file/a/b/c/" where a null or blank directoryName + * returns itself. createMissingDirectories specifies if the dirs should be created + * when travelling or break at a dir not found */ + fun gotoDirectory( + directoryName: String?, + createMissingDirectories: Boolean = true + ): SafeFile? { + if (directoryName == null) return this + + return directoryName.split(File.separatorChar).filter { it.isNotBlank() } + .fold(this) { file: SafeFile?, directory -> + // as MediaFile does not actually create a directory we can do this + if (createMissingDirectories || this is MediaFile) { + file?.createDirectory(directory) + } else { + val next = file?.findFile(directory) + + // we require the file to be a directory + if (next?.isDirectory() != true) { + null + } else { + next + } + } + } + } + + + fun createFile(displayName: String?): SafeFile? + fun createDirectory(directoryName: String?): SafeFile? + fun uri(): Uri? + fun name(): String? + fun type(): String? + fun filePath(): String? + fun isDirectory(): Boolean? + fun isFile(): Boolean? + fun lastModified(): Long? + fun length(): Long? + fun canRead(): Boolean + fun canWrite(): Boolean + fun delete(): Boolean + fun exists(): Boolean? + fun listFiles(): List? + + // fun listFiles(filter: FilenameFilter?): Array? + fun findFile(displayName: String?, ignoreCase: Boolean = false): SafeFile? + + fun renameTo(name: String?): Boolean + + /** Open a stream on to the content associated with the file */ + fun openOutputStream(append: Boolean = false): OutputStream? + + /** Open a stream on to the content associated with the file */ + fun openInputStream(): InputStream? + + /** Get a random access stuff of the UniFile, "r" or "rw" */ + fun createRandomAccessFile(mode: String?): UniRandomAccessFile? +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt new file mode 100644 index 00000000..f1592169 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt @@ -0,0 +1,116 @@ +package com.lagradost.cloudstream3.utils.storage + +import android.net.Uri +import com.hippo.unifile.UniFile +import com.hippo.unifile.UniRandomAccessFile +import com.lagradost.cloudstream3.mvvm.logError +import okhttp3.internal.closeQuietly +import java.io.InputStream +import java.io.OutputStream + +private fun UniFile.toFile(): SafeFile { + return UniFileWrapper(this) +} + +fun safe(apiCall: () -> T): T? { + return try { + apiCall.invoke() + } catch (throwable: Throwable) { + logError(throwable) + null + } +} + +class UniFileWrapper(val file: UniFile) : SafeFile { + override fun createFile(displayName: String?): SafeFile? { + return file.createFile(displayName)?.toFile() + } + + override fun createDirectory(directoryName: String?): SafeFile? { + return file.createDirectory(directoryName)?.toFile() + } + + override fun uri(): Uri? { + return safe { file.uri } + } + + override fun name(): String? { + return safe { file.name } + } + + override fun type(): String? { + return safe { file.type } + } + + override fun filePath(): String? { + return safe { file.filePath } + } + + override fun isDirectory(): Boolean? { + return safe { file.isDirectory } + } + + override fun isFile(): Boolean? { + return safe { file.isFile } + } + + override fun lastModified(): Long? { + return safe { file.lastModified() } + } + + override fun length(): Long? { + return safe { + val len = file.length() + if (len <= 1) { + val inputStream = this.openInputStream() ?: return@safe null + try { + inputStream.available().toLong() + } finally { + inputStream.closeQuietly() + } + } else { + len + } + } + } + + override fun canRead(): Boolean { + return safe { file.canRead() } ?: false + } + + override fun canWrite(): Boolean { + return safe { file.canWrite() } ?: false + } + + override fun delete(): Boolean { + return safe { file.delete() } ?: false + } + + override fun exists(): Boolean? { + return safe { file.exists() } + } + + override fun listFiles(): List? { + return safe { file.listFiles()?.mapNotNull { it?.toFile() } } + } + + override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? { + return safe { file.findFile(displayName, ignoreCase)?.toFile() } + } + + override fun renameTo(name: String?): Boolean { + return safe { file.renameTo(name) } ?: return false + } + + override fun openOutputStream(append: Boolean): OutputStream? { + return safe { file.openOutputStream(append) } + } + + override fun openInputStream(): InputStream? { + return safe { file.openInputStream() } + } + + override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? { + return safe { file.createRandomAccessFile(mode) } + } +} \ No newline at end of file From d436171a2f89f58107d5bc48d2a6be2fbb8845eb Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 06:36:43 +0200 Subject: [PATCH 191/570] removed possible duplicate download queue --- .../lagradost/cloudstream3/utils/VideoDownloadManager.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 37c02be4..442fa32f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -1598,9 +1598,8 @@ object VideoDownloadManager { val item = pkg.item val id = item.ep.id if (currentDownloads.contains(id)) { // IF IT IS ALREADY DOWNLOADING, RESUME IT - downloadEvent.invoke(Pair(id, DownloadActionType.Resume)) - /** ID needs to be returned to the work-manager to properly await notification */ - // return id + downloadEvent.invoke(id to DownloadActionType.Resume) + return } currentDownloads.add(id) @@ -1741,14 +1740,14 @@ object VideoDownloadManager { notificationCallback: (Int, Notification) -> Unit, setKey: Boolean = true ) { - if (!currentDownloads.any { it == pkg.item.ep.id }) { + if (!currentDownloads.any { it == pkg.item.ep.id } && !downloadQueue.any { it.item.ep.id == pkg.item.ep.id }) { downloadQueue.addLast(pkg) downloadCheck(context, notificationCallback) if (setKey) saveQueue() //ret } else { downloadEvent( - Pair(pkg.item.ep.id, DownloadActionType.Resume) + pkg.item.ep.id to DownloadActionType.Resume ) //null } From bac2ee980551d194b70866fc1580ecf40455e024 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 17:08:26 +0200 Subject: [PATCH 192/570] fixed div by zero --- .../com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index eb8cb9b3..91e97dfc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -44,6 +44,8 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { private fun fixPlayerSize() { playerWidthHeight?.let { (w, h) -> + if(w <= 0 || h <= 0) return@let + val orientation = context?.resources?.configuration?.orientation ?: return val sw = if (orientation == Configuration.ORIENTATION_LANDSCAPE) { From e2502de02cdf226ab4c37cbc71743c68896f5745 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 18:43:55 +0200 Subject: [PATCH 193/570] bump acra --- app/build.gradle.kts | 2 +- .../main/java/com/lagradost/cloudstream3/AcraApplication.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 50125aa3..178b49c2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,7 +58,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.6" + versionName = "4.1.7" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 32702657..c14780d8 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/d/e/1FAIpQLSdOlbgCx7NeaxjvEGyEQlqdh2nCvwjm2vwpP1VwW7REj9Ri3Q/formResponse" + "https://docs.google.com/forms/d/e/1FAIpQLSfO4r353BJ79TTY_-t5KWSIJT2xfqcQWY81xjAA1-1N0U2eSg/formResponse" val data = mapOf( - "entry.753293084" to errorContent.toJSON() + "entry.1993829403" to errorContent.toJSON() ) thread { // to not run it on main thread From 5bad6aca352506c722749fa6c1b5123ae722a071 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 23:57:54 +0200 Subject: [PATCH 194/570] fixed native crash handle --- .../com/lagradost/cloudstream3/AcraApplication.kt | 11 ++++++++--- .../com/lagradost/cloudstream3/NativeCrashHandler.kt | 11 ++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index c14780d8..4b4747ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -104,13 +104,17 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) : } class AcraApplication : Application() { + override fun onCreate() { super.onCreate() NativeCrashHandler.initCrashHandler() - Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) { + ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) - }) + }.also { + exceptionHandler = it + Thread.setDefaultUncaughtExceptionHandler(it) + } } override fun attachBaseContext(base: Context?) { @@ -138,6 +142,8 @@ class AcraApplication : Application() { } companion object { + var exceptionHandler: ExceptionHandler? = null + /** Use to get activity from Context */ tailrec fun Context.getActivity(): Activity? = this as? Activity ?: (this as? ContextWrapper)?.baseContext?.getActivity() @@ -212,6 +218,5 @@ class AcraApplication : Application() { activity?.supportFragmentManager?.fragments?.lastOrNull() ) } - } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt index e5cb2702..1fe00748 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt @@ -14,6 +14,12 @@ object NativeCrashHandler { private external fun getSignalStatus(): Int private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch { + + //launch { + // delay(10000) + // triggerNativeCrash() + //} + while (true) { delay(10_000) val signal = getSignalStatus() @@ -24,7 +30,10 @@ object NativeCrashHandler { if (lastError != null) continue if (checkSafeModeFile()) continue - throw RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n") + AcraApplication.exceptionHandler?.uncaughtException( + Thread.currentThread(), + RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n") + ) } } From 823ffd87080f12791a72d54508bb2393f4444d3c Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 24 Aug 2023 00:25:05 +0200 Subject: [PATCH 195/570] reverted low api crash handle crashing --- app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 4b4747ae..5f3162b4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -107,7 +107,7 @@ class AcraApplication : Application() { override fun onCreate() { super.onCreate() - NativeCrashHandler.initCrashHandler() + //NativeCrashHandler.initCrashHandler() ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) From 9a1358e295cf9761d3e8c9bba04ef214dcfc96ca Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 24 Aug 2023 14:16:33 +0000 Subject: [PATCH 196/570] Lower targetSdk to get all installed packages (#571) --- app/build.gradle.kts | 2 +- app/src/main/AndroidManifest.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 178b49c2..dfd2c173 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -55,7 +55,7 @@ android { defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 33 + targetSdk = 29 versionCode = 59 versionName = "4.1.7" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 563c82f8..0e716034 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ - + Date: Thu, 24 Aug 2023 16:39:50 +0200 Subject: [PATCH 197/570] fixed removal of predownloaded files --- .../java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt index 9ba0ef88..85a74963 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt @@ -23,7 +23,7 @@ interface SafeFile { // because UniFile sucks balls on Media we have to do this val absPath = file.absolutePath.removePrefix(File.separator) for (value in MediaFileContentType.values()) { - val prefixes = listOf(value.toAbsolutePath(), value.toPath()) + val prefixes = listOf(value.toAbsolutePath(), value.toPath()).map { it.removePrefix(File.separator) } for (prefix in prefixes) { if (!absPath.startsWith(prefix)) continue return fromMedia( From c92ac3e8b3502f26ed812647134dc0977218831e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:13:42 +0200 Subject: [PATCH 198/570] fixed removal of predownloaded files 2 + permission --- app/src/main/AndroidManifest.xml | 2 +- .../java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0e716034..15767d7b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ - + Download + if(relativePath == path) return this + val newPath = sanitizedAbsolutePath + path + if (folder) File.separator else "" From 9b4701fe91858c71fa32ecec98c3fb05451dfc6c Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:14:54 +0200 Subject: [PATCH 199/570] dont remove keys while this is tested --- .../com/lagradost/cloudstream3/utils/VideoDownloadManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 442fa32f..948d7b8a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -1682,7 +1682,7 @@ object VideoDownloadManager { // only delete the key if the file is not found if (file == null || !file.existsOrThrow()) { - if (removeKeys) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) + //if (removeKeys) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) // TODO READD return null } From 1a4cbcaea048e9d057f9c70e37a7a46330a0203c Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 24 Aug 2023 21:17:42 +0200 Subject: [PATCH 200/570] small fix --- .../ui/result/ResultViewModel2.kt | 4 +-- .../cloudstream3/utils/storage/MediaFile.kt | 27 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) 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 bdd27091..82d9a8fe 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 @@ -591,7 +591,7 @@ class ResultViewModel2 : ViewModel() { link, "$fileName ${link.name}", folder, - if (link.url.contains(".srt")) ".srt" else "vtt", + if (link.url.contains(".srt")) "srt" else "vtt", false, null, createNotificationCallback = {} ) @@ -719,7 +719,7 @@ class ResultViewModel2 : ViewModel() { ) ) } - .map { ExtractorSubtitleLink(it.name, it.url, "") } + .map { ExtractorSubtitleLink(it.name, it.url, "") }.take(3) .forEach { link -> val fileName = VideoDownloadManager.getFileName(context, meta) downloadSubtitle(context, link, fileName, folder) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt index 526d31ca..51b8adfe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt @@ -13,6 +13,7 @@ import com.hippo.unifile.UniRandomAccessFile import com.lagradost.cloudstream3.mvvm.logError import okhttp3.internal.closeQuietly import java.io.File +import java.io.FileNotFoundException import java.io.InputStream import java.io.OutputStream @@ -65,6 +66,10 @@ class MediaFile( private val external: Boolean = true, absolutePath: String, ) : SafeFile { + override fun toString(): String { + return sanitizedAbsolutePath + } + // this is the path relative to the download directory so "/hello/text.txt" = "hello/text.txt" is in fact "Download/hello/text.txt" private val sanitizedAbsolutePath: String = replaceDuplicateFileSeparators(absolutePath) @@ -130,7 +135,7 @@ class MediaFile( // VideoDownloadManager.sanitizeFilename(path.replace(File.separator, "")) // in case of duplicate path, aka Download -> Download - if(relativePath == path) return this + if (relativePath == path) return this val newPath = sanitizedAbsolutePath + path + if (folder) File.separator else "" @@ -246,12 +251,24 @@ class MediaFile( override fun length(): Long? { if (isDir) return null - val length = query()?.length ?: return null - if(length <= 0) { - val inputStream : InputStream = openInputStream() ?: return null + val query = query() + val length = query?.length ?: return null + if (length <= 0) { + try { + contentResolver.openFileDescriptor(query.uri, "r") + .use { + it?.statSize + }?.let { + return it + } + } catch (e: FileNotFoundException) { + return null + } + + val inputStream: InputStream = openInputStream() ?: return null return try { inputStream.available().toLong() - } catch (t : Throwable) { + } catch (t: Throwable) { null } finally { inputStream.closeQuietly() From b38a9b1ff5d6e1c5de21851af089232fca7c985b Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 24 Aug 2023 21:39:05 +0200 Subject: [PATCH 201/570] fuck android --- .../com/lagradost/cloudstream3/utils/VideoDownloadManager.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 948d7b8a..7bd863ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -62,6 +62,7 @@ const val DOWNLOAD_CHANNEL_DESCRIPT = "The download notification channel" object VideoDownloadManager { var maxConcurrentDownloads = 3 + var maxConcurrentConnections = 3 private var currentDownloads = mutableListOf() private const val USER_AGENT = @@ -1568,7 +1569,7 @@ object VideoDownloadManager { folder ?: "", ep.id, startIndex, - callback + callback, parallelConnections = maxConcurrentConnections ) } else { return downloadThing( @@ -1579,7 +1580,7 @@ object VideoDownloadManager { "mp4", tryResume, ep.id, - callback + callback, parallelConnections = maxConcurrentConnections ) } } catch (t: Throwable) { From 2d82480398c2dd5b0dfa5b0c0db7c626658aafd2 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Fri, 25 Aug 2023 15:58:58 +0700 Subject: [PATCH 202/570] fix Rabbitstream (#573) Co-authored-by: Sofie99 --- .../com/lagradost/cloudstream3/extractors/Rabbitstream.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt index b686f7d8..0154b4e8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt @@ -36,7 +36,6 @@ open class Rabbitstream : ExtractorApi() { override val requiresReferer = false open val embed = "ajax/embed-4" open val key = "https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt" - private var rawKey: String? = null override suspend fun getUrl( url: String, @@ -82,9 +81,10 @@ open class Rabbitstream : ExtractorApi() { ) } + } - private suspend fun getRawKey(): String = rawKey ?: app.get(key).text.also { rawKey = it } + private suspend fun getRawKey(): String = app.get(key).text private fun extractRealKey(originalString: String?, stops: String): Pair { val table = parseJson>>(stops) From d0c03321b90b13142dce2ab5931c13db48e4a90d Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Fri, 25 Aug 2023 10:59:18 +0200 Subject: [PATCH 203/570] Translations update from Hosted Weblate (#568) Co-authored-by: Carlos Luiz Co-authored-by: Joel Brink Co-authored-by: Julian Co-authored-by: Mubarek Seyd Juhar Co-authored-by: Sam Cooper Co-authored-by: Skrripy Co-authored-by: mbottari Co-authored-by: tabtomi8 --- app/src/main/res/values-ajp/strings.xml | 2 + app/src/main/res/values-am/strings.xml | 5 + app/src/main/res/values-ars/strings.xml | 203 +++++++++++++++++- app/src/main/res/values-bp/strings.xml | 69 +++++- app/src/main/res/values-de/strings.xml | 12 +- app/src/main/res/values-hu/strings.xml | 38 ++-- app/src/main/res/values-ti/strings.xml | 6 + app/src/main/res/values-uk/strings.xml | 4 +- .../metadata/android/ar-SA/changelogs/2.txt | 1 + .../android/ar-SA/full_description.txt | 10 +- .../android/ar-SA/short_description.txt | 2 +- fastlane/metadata/android/ar-SA/title.txt | 2 +- .../android/de-DE/full_description.txt | 6 +- 13 files changed, 321 insertions(+), 39 deletions(-) create mode 100644 app/src/main/res/values-ajp/strings.xml create mode 100644 app/src/main/res/values-am/strings.xml create mode 100644 app/src/main/res/values-ti/strings.xml create mode 100644 fastlane/metadata/android/ar-SA/changelogs/2.txt diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-ajp/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml new file mode 100644 index 00000000..98eb0e0d --- /dev/null +++ b/app/src/main/res/values-am/strings.xml @@ -0,0 +1,5 @@ + + + %s ክፍል %d + ተዋናዮች: %s + \ No newline at end of file diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index 42eba3cc..12d558ad 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -1,2 +1,203 @@ - + + لافتة + تغيير مزود + جارى التحميل + بث%s + ملء + تخطي التحميل + تحميل… + ترجمات + إعادة محاولة الاتصال … + %sييبي%d + الحلقة%dسيتم نشرها في + %dي%dس%dد + %dس%dد + %dد + لافتة الحلقة + اللافتة الاساسية + اذهب للخالف + معاينة الخلفية + سرعة(%.2fx) + فتح مع كلاودستريم + الصفحة الاساسية + ...%sابحث + لايوجد بيانات + المزيد من الخيارات + فتح في المتصفح + المتصفح + شاهد الفلم + دفق التورنت + بدأ التنزيل + عشوائي قادم + تشغيل المقطع الدعائي + الأنواع + توقف التنزيل + خطط للمشاهدة + لا يوجد + إعادة المشاهدة + !تم العثور على تحديث جديد +\n%s->%s + %.1f:قدر + %dاقل + كلاودستريم + بحث + التحميلات + اعدادات + ...بحث + الحلقة القادمة + شارك + مشاهدة + في التوقف + مكتمل + توقف + تشغيل البث المباشر + مصادر + تشغيل الحلقة + تم إلغاء التنزيل + تم التنزيل + تنززل + تحميل + عُد + التحميل فشل + استخدم سطوع النظام في مشغل التطبيق بدلاً من التراكب الداكن + تم تحميل ملف النسخ الاحتياطي + البحث المتقدم + إزالة الحدود السوداء + ترجمات + يضيف خيار السرعة في المشغل + انقر نقرا مزدوجا للبحث + انقر نقرًا مزدوجًا للإيقاف المؤقت + اللاعب يبحث عن المبلغ (بالثواني) + اسحب من جانب إلى آخر للتحكم بموقعك في الفيديو + ابدأ الحلقة التالية عندما تنتهي الحلقة الحالية + استخدام سطوع النظام + تحديث مراقبة التقدم + قم بمزامنة تقدم الحلقة الحالية تلقائيًا + اسحب لتغيير الإعدادات + استعادة البيانات من النسخة الاحتياطية + فشل في استعادة البيانات من الملف %s + انقر مرتين على الجانب الأيمن أو الأيسر للبحث للأمام أو للخلف + البيانات المخزنة + اضغط مرتين في المنتصف للتوقف مؤقتًا + أذونات التخزين مفقودة. حاول مرة اخرى. + حدث خطأ أثناء النسخ الاحتياطي %s + بحث + مكتبة + معلومات + التحديثات والنسخ الاحتياطي + يعطيك نتائج البحث مفصولة حسب المزود + يرسل فقط البيانات عن الأعطال + عرض المقطورات + عرض الملصقات من كيتسو + حسابات + لا يرسل أي بيانات + عرض حلقة حشو للأنمي + إخفاء جودة الفيديو المحددة في نتائج البحث + تحديثات البرنامج المساعد التلقائي + البحث تلقائيًا عن التحديثات الجديدة بعد بدء تشغيل التطبيق. + التحديث إلى الإصداراالمسبق + تنزيل المكونات الإضافية تلقائيًا + إعادة عملية الإعداد + ابحث عن تحديثات الإصدار التجريبي بدلاً من الإصدارات الكاملة فقط + حدد الوضع لتصفية تنزيل المكونات الإضافية + قم تلقائيًا بتثبيت جميع المكونات الإضافية التي لم يتم تثبيتها بعد من المستودعات المضافة. + إعدادات ترجمات كرومكاست + وضع إيجينجرافي + انتقد للبحث + نسخ إحتياطي للبيانات + إظهار تحديثات التطبيق + إعدادات ترجمات المشغل + ترجمات كرومكاست + قم بالتمرير لأعلى أو لأسفل على الجانب الأيسر أو الأيمن لتغيير السطوع أو مستوى الصوت + التشغيل التلقائي للحلقة القادمة + تطبيق رواية خفيفة من نفس المطورين + أعط بينيني للمطورين + جيتهب + تطبيق انيمي من نفس المطورين + لغة التطبيق + انضم إلى الديسكورد + بنيني معطا + بعض الهواتف لا تدعم مثبت الحزمة الجديد. جرب الخيار القديم إذا لم يتم تثبيت التحديثات. + مثبت تتبيق + اجتاز + الحلقات + موسم + تم نسخ الرابط إلى الحافظة + مسح + وقف + جارٍ تنزيل تحديث التطبيق… + إعادة التعيين إلى القيمة العادية + س + %d%s + لا يتمتع هذا المزود بدعم كرومكاست + لم يتم العثور على أي روابط + تشغيل الحلقة + عذرًا، تعطل التطبيق. سيتم إرسال تقرير خطأ مجهول إلى المطورين + %s%d%s + لا يوجد موسم + حلقة + %d-%d + يي + امسح التاريخ + جارٍ تثبيت تحديث التطبيق… + بدأ + لم يتم العثور على أي حلقات + إظهار تخطي النوافذ المنبثقة للفتح/الإنهاء + الكثير من النص. غير قادر على الحفظ في الحافظة. + وضع علامة كما شاهدت + إزالة من شاهد + حذف ملف + فشل + اكتمل + -30 + +30 + تاريخ + هل أنت متأكد أنك تريد الخروج؟ + نعم + لا + تعذر تثبيت الإصدار الجديد من التطبيق + إرث + منزل المجموعة + التقييم (من الأقل إلى الأعلى) + تم التحديث (من الجديد إلى القديم) + تم التحديث (القديم إلى الجديد) + أبجديًا (من الألف إلى الياء) + مكتبتك فارغة :( +\nقم بتسجيل الدخول إلى حساب المكتبة أو قم بإضافة العروض إلى مكتبتك المحلية. + !تم العثور على ملف الوضع الآمن +\n.عدم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف + ارجع + تحديث العروض المشتركة + الوضع العادي + حرر + ملفات تعريفية + مساعدة + .هنا يمكنك تغيير كيفية ترتيب المصادر. إذا كان للفيديو أولوية أعلى، فسيظهر في مكان أعلى في تحديد المصدر. مجموع أولوية المصدر وأولوية الجودة هو أولوية الفيديو +\n +\nالمصدر أ: 3 +\nالجودة ب: 7 +\nستكون أولوية الفيديو المدمجة .10 +\n +\n!ملاحظة: إذا كان المجموع 10 أو أكثر، فسيقوم اللاعب تلقائيًا بتخطي التحميل عند تحميل هذا الرابط + لقد صوت بالفعل + أبجديًا (ياء إلى ألف) + ترتيب حسب + مشترك + سيتم تحديث التطبيق عند الخروج + رتب + التقييم (من الأعلى إلى الأقل) + حدد المكتبة + افتع مع + .هذه القائمة فارغة. حاول التبديل إلى واحد آخر + %sتم الاشتراك في + %sتم إلغاء الاشتراك من + !%dتم إصدار الحلقة + خلفية الملف الشخصي + %dملف التعريف + واي فاي + بيانات الجوال + استخدم + %sتعذر إنشاء واجهة المستخدم بشكل صحيح، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور + الصفات + \ 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 425293e4..df95d69f 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -156,7 +156,7 @@ Não enviar nenhum dado Mostrar episódios de Filler em anime Mostrar trailers - Mostrar posters do kitsu + Mostrar posters do Kitsu Esconder qualidades de vídeo selecionadas nos resultados da Pesquisa Atualizações de plugin automáticas Mostrar atualizações do app @@ -183,7 +183,7 @@ S E Nenhum Episódio encontrado - Deletar Arquivo + Apagar Arquivo Deletar Pausar Retomar @@ -410,15 +410,19 @@ Transferido %d %s com sucesso Tudo %s já transferido Transferência em batch - plugin - plugins + Plugin + Plugins Isto irá apagar todos os repositórios de plugins Apagar repositório Transferir lista de sites a usar Transferido: %d Desativado: %d Não transferido: %d - Adicionar um repositório para instalar extensões de sites + CloudStream não tem fontes instaladas por padrão. Você precisa instalar um site de repositórios. +\n +\nPor causa das limitações do DMCA (Digital Millennium Copyright Act ) feito em nome de Sky UK Limited 🤮nós não podemos adicionar site de repositórios no app. +\n +\nEntre no nosso Discord ou pesquise online. Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas @@ -455,7 +459,7 @@ Editar Perfis Exibindo Player - procure na Barra de Progresso - remover dos assitidos + Remover dos assistidos Extensões Alfabética(A => Z) Abrir com @@ -468,7 +472,7 @@ Biblioteca Não Trilhas Sonoras - Votação(Baixa para Alta) + Votação (Baixa para Alta) Atualização iniciada Conteúdo +18 Ajuda @@ -476,7 +480,7 @@ Não pudemos instalar a nova versão do App instalador de pacotes Organizar por - Votação(Alta para Baixa) + Votação (Alta para Baixa) Alfabética(Z => A) Qualidade Perfil de plano de fundo @@ -499,4 +503,51 @@ Atualizando shows inscritos Player oculto - Procure na barra de progresso Conteúdo +18 - + Reiniciar + Parar + Marcar como assistido + Aplicativo precisa ser fechado para atualizar + Mostrar popups pulados para abertura e finalização + %d-%d + Player interno + Tamanho + Abrindo + %s %d%s + %d plugins atualizados + Todos as extensões serão desligadas para ajuda se talvez estejam causando algum bug. + Aplicativo não encontrado + Recapitular + Todas as linguagens + Pula %s + Mistura terminada + Modo seguro ligado + Ranquear: %s + Linguagem + Lista de reprodução HLS + Terminando + %d %s + Adicionado em (antigo para novo) + Introdução + plug-ins não foram encontrados no repositório + Repositório não encontrado, verifique o URL e tente usa uma VPN + Descrição + Versão + Autores + Instale a extensão primeiro + Créditos + Historico + Limpar historico + Tem Muito texto. Não é possível salvar no clipboard. + Player de vídeo preferido + Começar + Suportado + Status + MPV + Abrindo mistura + VLC + Aplicar quando reiniciar + Visualização info de crash + Faixas de áudio + Adicionado em (novo para antigo) + Faixas de video + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 45a6a66c..6892c8fd 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -92,7 +92,7 @@ Abbrechen Kopieren Schließen - Löschen + Leeren Speichern Player-Geschwindigkeit Untertiteleinstellungen @@ -390,7 +390,7 @@ Einrichtung überspringen Aussehen der App passend zu dem des Geräts ändern Absturzmeldung - Was möchtest du anschauen\? + Was möchten Sie sehen\? Fertig Erweiterungen Repository hinzufügen @@ -546,4 +546,10 @@ \nWerden eine kombinierte Videopriorität von 10 haben. \n \nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird! - + Filtermodus für Plugin-Downloads auswählen + Es wurde bereits abgestimmt + Keine Plugins im Repository gefunden + Repository nicht gefunden, überprüfe die URL und probiere eine VPN + Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein schwerwiegender Fehler und sollte sofort gemeldet werden. %s + Deaktivieren + \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 46407f76..ac817db0 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -190,7 +190,7 @@ Adatok eltárolva Hiba a biztonsági mentés során %s Fiókok - Szolgáltatás szerinti keresés eredmények + Szolgáltató szerint elkülönítve adja meg a keresési eredményeket Nem küld adatokat Poszterek megjelenítése Kitsu-ról Kiválasztott videóminőségek elrejtése keresési eredményekbe @@ -198,7 +198,7 @@ Bővítmények automatikus letöltése Automatikusan telepíti az összes még nem telepített bővítményt a hozzáadott tárolókból. Alkalmazás frissítések megjelenítése - Automatikusan keressen új frissítéseket indításkor + Automatikusan keressen új frissítéseket indításkor. Frissítés az előzetes kiadásokhoz (prerelease) Csak előzetesen kiadott frissítések (prerelease) keresése a teljes kiadások helyett Github @@ -232,30 +232,30 @@ Lejátszás böngészőben Feliratok letöltése Újracsatlakozás… - Swipe balra vagy jobbra a videólejátszóban az idő vezérléséhez + Húzd balra vagy jobbra a videólejátszóban az idő vezérléséhez Csúsztassa ujját a beállítások módosításához - Csúsztassa az újját bal vagy jobb oldalon a fényerő vagy hangerő megváltoztatásához + Csúsztassa felfelé vagy lefelé a bal vagy jobb oldalon a fényerő vagy a hangerő megváltoztatásához Biztonsági mentés 0 Banán a fejlesztőknek - Swipe to seek + Húzás a kereséshez Következő epizód automatikus lejátszása Következő epizód lejátszása amikor az aktuális epizód véget ér - Dupla koppintás to seek + Dupla koppintás a kereséshez Dupla koppintás a szüneteltetéshez - Player seek amount + Lejátszó keresési értéke (Másodpercben) Koppintson kétszer a jobb vagy bal oldalra az előre vagy hátra ugráshoz - Koppintson középre a szüneteltetéshez + Koppintson kétszer középen a szüneteltetéshez Rendszer fényerejének használata Rendszer fényerejének használata az appban a sötét átfedés helyett Előrehaladás frissítése Automatikusan szinkronizálja az aktuális epizód előrehaladását - Adatok visszaállítása a biztonsági mentésből + Adatok visszaállítása biztonsági mentésből Biztonsági mentés betöltve Információ Folytatás -30 Frissítés elkezdődött - Nem sikerült visszaállítani az adatok a fájlból %s + Nem sikerült visszaállítani az adatokat a %s fájlból Tárolási engedélyek hiányoznak. Kérjük próbálja újra. Csak összeomlásokról küld adatokat APK Telepítő @@ -280,7 +280,7 @@ DNS HTTPS-en keresztül Böngésző Android TV - kézmozdulatok + Kézmozdulatok frissítés kihagyása Alkalmazásfrissítések Szolgáltatók @@ -496,4 +496,18 @@ HQ %d letöltve Start - + Emulátor elrendezés + Nyomkövetés hozzáadása + Telefon elrendezés + Poszter cím helye + Tegye a címet a poszter alá + Az átugrás mértéke, amikor a lejátszó el van rejtve + Jogi nyilatkozat + Lejátszó megjelenítve - Ugrási Érték + Lejátszó elrejtve - Ugrási Érték + Klónozott oldal + Egy meglévő webhely klónjának hozzáadása, más URL-címmel + TV elrendezés + Automatikus + Az átugrás mértéke, amikor a lejátszó látható + \ No newline at end of file diff --git a/app/src/main/res/values-ti/strings.xml b/app/src/main/res/values-ti/strings.xml new file mode 100644 index 00000000..0f64858d --- /dev/null +++ b/app/src/main/res/values-ti/strings.xml @@ -0,0 +1,6 @@ + + + %s ክፋል %d + ክፋል %d በ ላይ ይወጣል + ተዋሳእቲ፡ %s + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index e0db1c0e..8b1b6c39 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -418,7 +418,7 @@ Почалося завантаження %d %s… Завантажено %d %s Всі %s вже завантажено - Пакетне завантаження + Завантажити пакети плагін плагіни Видалити репозиторій @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - + \ No newline at end of file diff --git a/fastlane/metadata/android/ar-SA/changelogs/2.txt b/fastlane/metadata/android/ar-SA/changelogs/2.txt new file mode 100644 index 00000000..cc43acf1 --- /dev/null +++ b/fastlane/metadata/android/ar-SA/changelogs/2.txt @@ -0,0 +1 @@ +تمت إضافة سجل التغيير! diff --git a/fastlane/metadata/android/ar-SA/full_description.txt b/fastlane/metadata/android/ar-SA/full_description.txt index 2107b338..9668a9b1 100644 --- a/fastlane/metadata/android/ar-SA/full_description.txt +++ b/fastlane/metadata/android/ar-SA/full_description.txt @@ -1,14 +1,10 @@ -يتيح لك كلاود ستريم -3 بث وتنزيل الأفلام والمسلسلات التلفزيونية والأنيمي. يأتي التطبيق بدون أي إعلانات وتحليلات. و يدعم العديد من مواقع البث الاولي(التريلر) والأفلام والمزيد. وتشمل الميزات: - +يسمح لك كلاود ستريم -3 ببث وتنزيل الأفلام, المسلسلات التلفزيونية, والأنيمي. +يأتي التطبيق بدون أي إعلانات وتحليلات و + يدعم العديد من مواقع البث الاولي(التريلر) ,والأفلام, والمزيد. إشارات مرجعية - -قم بتنزيل ودفق الأفلام والبرامج التلفزيونية والأنيمي - - تنزيلات الترجمة - دعم كروم كاست diff --git a/fastlane/metadata/android/ar-SA/short_description.txt b/fastlane/metadata/android/ar-SA/short_description.txt index f396ff81..7ccd9743 100644 --- a/fastlane/metadata/android/ar-SA/short_description.txt +++ b/fastlane/metadata/android/ar-SA/short_description.txt @@ -1 +1 @@ -بث وتحميل الأفلام والأنمي والمسلسلات التلفزيونية. +بث وتحميل الأفلام, الأنمي, والمسلسلات التلفزيونية. diff --git a/fastlane/metadata/android/ar-SA/title.txt b/fastlane/metadata/android/ar-SA/title.txt index 635e1390..7977b290 100644 --- a/fastlane/metadata/android/ar-SA/title.txt +++ b/fastlane/metadata/android/ar-SA/title.txt @@ -1 +1 @@ -كلاود ستريم +كلاودستريم diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt index df314372..ea2a8750 100644 --- a/fastlane/metadata/android/de-DE/full_description.txt +++ b/fastlane/metadata/android/de-DE/full_description.txt @@ -1,11 +1,11 @@ -Mit CloudStream-3 kannst du Filme, TV-Serien und Anime streamen und herunterladen. +Mit CloudStream-3 kannst du Filme, TV-Serien und Anime streamen und herunterladen. Die App kommt ganz ohne Werbung und Analytik aus. -Sie unterstützt mehrere Trailer-, Filmseiten und vieles mehr. Integrierte Features: +Sie unterstützt zahlreiche Trailer, Filmseiten und vieles mehr, unter anderem: Lesezeichen -Herunterladen und Streamen von Filmen, Fernsehsendungen und Animes +Herunterladen und Streaming von Filmen, Fernsehsendungen und Animes Downloads von Untertiteln From 557003895b65a7b6e1631489523e1225d56cdc34 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Fri, 25 Aug 2023 08:59:37 +0000 Subject: [PATCH 204/570] chore(locales): fix locale issues --- .../com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 3 +++ app/src/main/res/values-ajp/strings.xml | 2 +- app/src/main/res/values-am/strings.xml | 2 +- app/src/main/res/values-ars/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-ti/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- 9 files changed, 11 insertions(+), 8 deletions(-) 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 f46aac9b..1bd9778e 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 @@ -54,6 +54,8 @@ 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("", "ajp", "ajp"), + Triple("", "አማርኛ", "am"), Triple("", "العربية", "ar"), Triple("", "ars", "ars"), Triple("", "български", "bg"), @@ -96,6 +98,7 @@ val appLanguages = arrayListOf( Triple("", "Soomaaliga", "so"), Triple("", "svenska", "sv"), Triple("", "தமிழ்", "ta"), + Triple("", "ትግርኛ", "ti"), Triple("", "Tagalog", "tl"), Triple("", "Türkçe", "tr"), Triple("", "українська", "uk"), diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index a6b3daec..42eba3cc 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml index 98eb0e0d..5a799eb4 100644 --- a/app/src/main/res/values-am/strings.xml +++ b/app/src/main/res/values-am/strings.xml @@ -2,4 +2,4 @@ %s ክፍል %d ተዋናዮች: %s - \ No newline at end of file + diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index 12d558ad..ea8aa05c 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -200,4 +200,4 @@ استخدم %sتعذر إنشاء واجهة المستخدم بشكل صحيح، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور الصفات - \ 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 df95d69f..b70eec12 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -550,4 +550,4 @@ Faixas de áudio Adicionado em (novo para antigo) Faixas de video - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6892c8fd..6739465a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -552,4 +552,4 @@ Repository nicht gefunden, überprüfe die URL und probiere eine VPN Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein schwerwiegender Fehler und sollte sofort gemeldet werden. %s Deaktivieren - \ No newline at end of file + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index ac817db0..05a7f0a7 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -510,4 +510,4 @@ TV elrendezés Automatikus Az átugrás mértéke, amikor a lejátszó látható - \ No newline at end of file + diff --git a/app/src/main/res/values-ti/strings.xml b/app/src/main/res/values-ti/strings.xml index 0f64858d..a9079ed5 100644 --- a/app/src/main/res/values-ti/strings.xml +++ b/app/src/main/res/values-ti/strings.xml @@ -3,4 +3,4 @@ %s ክፋል %d ክፋል %d በ ላይ ይወጣል ተዋሳእቲ፡ %s - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8b1b6c39..4866ecd4 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - \ No newline at end of file + From 8193e39b3046192dfb6970e5ff49f30d629d033a Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 25 Aug 2023 23:16:34 +0200 Subject: [PATCH 205/570] bump + refactor --- app/build.gradle.kts | 15 +- .../lagradost/cloudstream3/MainActivity.kt | 18 +- .../cloudstream3/NativeCrashHandler.kt | 4 +- .../ui/player/DownloadedPlayerActivity.kt | 8 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 6 +- .../ui/settings/SettingsGeneral.kt | 4 +- .../cloudstream3/utils/BackupUtils.kt | 1 + .../utils/VideoDownloadManager.kt | 14 +- .../cloudstream3/utils/storage/MediaFile.kt | 389 ------------------ .../cloudstream3/utils/storage/SafeFile.kt | 244 ----------- .../utils/storage/UniFileWrapper.kt | 116 ------ 11 files changed, 40 insertions(+), 779 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dfd2c173..333fbfb8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,11 +32,12 @@ android { enable = true } - externalNativeBuild { - cmake { - path("CMakeLists.txt") - } - } + // disable this for now + //externalNativeBuild { + // cmake { + // path("CMakeLists.txt") + // } + //} signingConfigs { create("prerelease") { @@ -58,7 +59,7 @@ android { targetSdk = 29 versionCode = 59 - versionName = "4.1.7" + versionName = "4.1.8" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") @@ -232,7 +233,7 @@ dependencies { // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 - implementation("com.github.tachiyomiorg:unifile:17bec43") + implementation("com.github.LagradOst:SafeFile:0.0.2") // API because cba maintaining it myself implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 15b16078..fbad4fce 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -144,6 +144,7 @@ import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser +import com.lagradost.safefile.SafeFile import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.io.File @@ -279,6 +280,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { companion object { const val TAG = "MAINACT" var lastError: String? = null + /** * Setting this will automatically enter the query in the search * next time the search fragment is opened. @@ -366,7 +368,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { nextSearchQuery = try { URLDecoder.decode(query, "UTF-8") - } catch (t : Throwable) { + } catch (t: Throwable) { logError(t) query } @@ -859,7 +861,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { RecyclerView::class.java.declaredMethods.firstOrNull { it.name == "scrollStep" }?.also { it.isAccessible = true } - } catch (t : Throwable) { + } catch (t: Throwable) { null } } @@ -906,11 +908,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (dx > 0) dx else 0 } - if(!NO_MOVE_LIST) { + if (!NO_MOVE_LIST) { parent.smoothScrollBy(rdx, 0) - }else { + } else { val smoothScroll = reflectedScroll - if(smoothScroll == null) { + if (smoothScroll == null) { parent.smoothScrollBy(rdx, 0) } else { try { @@ -920,12 +922,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val out = IntArray(2) smoothScroll.invoke(parent, rdx, 0, out) val scrolledX = out[0] - if(abs(scrolledX) <= 0) { // newFocus.measuredWidth*2 + if (abs(scrolledX) <= 0) { // newFocus.measuredWidth*2 smoothScroll.invoke(parent, -rdx, 0, out) parent.smoothScrollBy(scrolledX, 0) if (NO_MOVE_LIST) targetDx = scrolledX } - } catch (t : Throwable) { + } catch (t: Throwable) { parent.smoothScrollBy(rdx, 0) } } @@ -1131,10 +1133,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { snackbar.show() } } - } } + ioSafe { SafeFile.check(this@MainActivity) } if (PluginManager.checkSafeModeFile()) { normalSafeApiCall { diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt index 1fe00748..7be90440 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.launch object NativeCrashHandler { // external fun triggerNativeCrash() - private external fun initNativeCrashHandler() + /*private external fun initNativeCrashHandler() private external fun getSignalStatus(): Int private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch { @@ -49,5 +49,5 @@ object NativeCrashHandler { } initSignalPolling() - } + }*/ } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 03405faf..d181e175 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.player +import android.content.ContentUris import android.content.Intent import android.net.Uri import android.os.Bundle @@ -10,7 +11,7 @@ import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.cloudstream3.utils.storage.SafeFile +import com.lagradost.safefile.SafeFile const val DTAG = "PlayerActivity" @@ -57,7 +58,10 @@ class DownloadedPlayerActivity : AppCompatActivity() { listOf( ExtractorUri( uri = uri, - name = name ?: getString(R.string.downloaded_file) + name = name ?: getString(R.string.downloaded_file), + // well not the same as a normal id, but we take it as users may want to + // play downloaded files and save the location + id = kotlin.runCatching { ContentUris.parseId(uri) }.getOrNull()?.hashCode() ) ) ) 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 341b4ad3..2b9304b6 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 @@ -21,13 +21,11 @@ import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import androidx.media3.common.Format.NO_VALUE import androidx.media3.common.MimeTypes -import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding -import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding import com.lagradost.cloudstream3.mvvm.* @@ -52,7 +50,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.toPx -import com.lagradost.cloudstream3.utils.storage.SafeFile +import com.lagradost.safefile.SafeFile import kotlinx.coroutines.Job import java.util.* import kotlin.math.abs @@ -136,7 +134,7 @@ class GeneratorPlayer : FullScreenPlayer() { return durPos.position } - var currentVerifyLink: Job? = null + private var currentVerifyLink: Job? = null private fun loadExtractorJob(extractorLink: ExtractorLink?) { currentVerifyLink?.cancel() 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 f46aac9b..49f678c4 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 @@ -38,7 +38,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath -import com.lagradost.cloudstream3.utils.storage.SafeFile +import com.lagradost.safefile.SafeFile fun getCurrentLocale(context: Context): String { val res = context.resources @@ -335,7 +335,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { val currentDir = settingsManager.getString(getString(R.string.download_path_pref), null) - ?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx).toString() } + ?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx)?.filePath() } activity?.showBottomDialog( dirs + listOf("Custom"), 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 2da54678..41326eb8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -158,6 +158,7 @@ object BackupUtils { val displayName = "CS3_Backup_${date}" val backupFile = getBackup() val stream = setupStream(this, displayName, null, ext, false) + fileStream = stream.openNew() printStream = PrintWriter(fileStream) printStream.print(mapper.writeValueAsString(backupFile)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 7bd863ae..6425ba66 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -35,8 +35,8 @@ import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute -import com.lagradost.cloudstream3.utils.storage.MediaFileContentType -import com.lagradost.cloudstream3.utils.storage.SafeFile +import com.lagradost.safefile.MediaFileContentType +import com.lagradost.safefile.SafeFile import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -554,9 +554,8 @@ object VideoDownloadManager { extension: String, tryResume: Boolean, ): StreamData { - val (base, _) = context.getBasePath() return setupStream( - base ?: throw IOException("Bad config"), + context.getBasePath().first ?: getDefaultDir(context) ?: throw IOException("Bad config"), name, folder, extension, @@ -1401,7 +1400,12 @@ object VideoDownloadManager { metadata.type = DownloadType.IsFailed } } finally { - fileMutex.unlock() + try { + // may cause java.lang.IllegalStateException: Mutex is not locked because of cancelling + fileMutex.unlock() + } catch (t : Throwable) { + logError(t) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt deleted file mode 100644 index 51b8adfe..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt +++ /dev/null @@ -1,389 +0,0 @@ -package com.lagradost.cloudstream3.utils.storage - -import android.content.ContentResolver -import android.content.ContentUris -import android.content.ContentValues -import android.content.Context -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.MediaStore -import androidx.annotation.RequiresApi -import com.hippo.unifile.UniRandomAccessFile -import com.lagradost.cloudstream3.mvvm.logError -import okhttp3.internal.closeQuietly -import java.io.File -import java.io.FileNotFoundException -import java.io.InputStream -import java.io.OutputStream - - -enum class MediaFileContentType { - Downloads, - Audio, - Video, - Images, -} - -// https://developer.android.com/training/data-storage/shared/media -fun MediaFileContentType.toPath(): String { - return when (this) { - MediaFileContentType.Downloads -> Environment.DIRECTORY_DOWNLOADS - MediaFileContentType.Audio -> Environment.DIRECTORY_MUSIC - MediaFileContentType.Video -> Environment.DIRECTORY_MOVIES - MediaFileContentType.Images -> Environment.DIRECTORY_DCIM - } -} - -fun MediaFileContentType.defaultPrefix(): String { - return Environment.getExternalStorageDirectory().absolutePath -} - -fun MediaFileContentType.toAbsolutePath(): String { - return defaultPrefix() + File.separator + - this.toPath() -} - -fun replaceDuplicateFileSeparators(path: String): String { - return path.replace(Regex("${File.separator}+"), File.separator) -} - -@RequiresApi(Build.VERSION_CODES.Q) -fun MediaFileContentType.toUri(external: Boolean): Uri { - val volume = if (external) MediaStore.VOLUME_EXTERNAL_PRIMARY else MediaStore.VOLUME_INTERNAL - return when (this) { - MediaFileContentType.Downloads -> MediaStore.Downloads.getContentUri(volume) - MediaFileContentType.Audio -> MediaStore.Audio.Media.getContentUri(volume) - MediaFileContentType.Video -> MediaStore.Video.Media.getContentUri(volume) - MediaFileContentType.Images -> MediaStore.Images.Media.getContentUri(volume) - } -} - -@RequiresApi(Build.VERSION_CODES.Q) -class MediaFile( - private val context: Context, - private val folderType: MediaFileContentType, - private val external: Boolean = true, - absolutePath: String, -) : SafeFile { - override fun toString(): String { - return sanitizedAbsolutePath - } - - // this is the path relative to the download directory so "/hello/text.txt" = "hello/text.txt" is in fact "Download/hello/text.txt" - private val sanitizedAbsolutePath: String = - replaceDuplicateFileSeparators(absolutePath) - - // this is only a directory if the filepath ends with a / - private val isDir: Boolean = sanitizedAbsolutePath.endsWith(File.separator) - private val isFile: Boolean = !isDir - - // this is the relative path including the Download directory, so "/hello/text.txt" => "Download/hello" - private val relativePath: String = - replaceDuplicateFileSeparators(folderType.toPath() + File.separator + sanitizedAbsolutePath).substringBeforeLast( - File.separator - ) - - // "/hello/text.txt" => "text.txt" - private val namePath: String = sanitizedAbsolutePath.substringAfterLast(File.separator) - private val baseUri = folderType.toUri(external) - private val contentResolver: ContentResolver = context.contentResolver - - init { - // some standard asserts that should always be hold or else this class wont work - assert(!relativePath.endsWith(File.separator)) - assert(!(isDir && isFile)) - assert(!relativePath.contains(File.separator + File.separator)) - assert(!namePath.contains(File.separator)) - - if (isDir) { - assert(namePath.isBlank()) - } else { - assert(namePath.isNotBlank()) - } - } - - companion object { - private fun splitFilenameExt(name: String): Pair { - val split = name.indexOfLast { it == '.' } - if (split <= 0) return name to null - val ext = name.substring(split + 1 until name.length) - if (ext.isBlank()) return name to null - - return name.substring(0 until split) to ext - } - - private fun splitFilenameMime(name: String): Pair { - val (display, ext) = splitFilenameExt(name) - val mimeType = when (ext) { - - // Absolutely ridiculous, if text/vtt is used as mimetype scoped storage prevents - // downloading to /Downloads yet it works with null - - "vtt" -> null // "text/vtt" - "mp4" -> "video/mp4" - "srt" -> null // "application/x-subrip"//"text/plain" - else -> null - } - return display to mimeType - } - } - - private fun appendRelativePath(path: String, folder: Boolean): MediaFile? { - if (isFile) return null - - // VideoDownloadManager.sanitizeFilename(path.replace(File.separator, "")) - - // in case of duplicate path, aka Download -> Download - if (relativePath == path) return this - - val newPath = - sanitizedAbsolutePath + path + if (folder) File.separator else "" - - return MediaFile( - context = context, - folderType = folderType, - external = external, - absolutePath = newPath - ) - } - - private fun createUri(displayName: String? = namePath): Uri? { - if (displayName == null) return null - if (isFile) return null - val (name, mime) = splitFilenameMime(displayName) - - val newFile = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) - put(MediaStore.MediaColumns.TITLE, name) - if (mime != null) - put(MediaStore.MediaColumns.MIME_TYPE, mime) - put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) - } - return contentResolver.insert(baseUri, newFile) - } - - override fun createFile(displayName: String?): SafeFile? { - if (isFile || displayName == null) return null - query(displayName)?.uri ?: createUri(displayName) ?: return null - return appendRelativePath(displayName, false) //SafeFile.fromUri(context, ?: return null) - } - - override fun createDirectory(directoryName: String?): SafeFile? { - if (directoryName == null) return null - // we don't create a dir here tbh, just fake create it - return appendRelativePath(directoryName, true) - } - - private data class QueryResult( - val uri: Uri, - val lastModified: Long, - val length: Long, - ) - - @RequiresApi(Build.VERSION_CODES.Q) - private fun query(displayName: String = namePath): QueryResult? { - try { - //val (name, mime) = splitFilenameMime(fullName) - - val projection = arrayOf( - MediaStore.MediaColumns._ID, - MediaStore.MediaColumns.DATE_MODIFIED, - MediaStore.MediaColumns.SIZE, - ) - - val selection = - "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}' AND ${MediaStore.MediaColumns.DISPLAY_NAME}='$displayName'" - - contentResolver.query( - baseUri, - projection, selection, null, null - )?.use { cursor -> - while (cursor.moveToNext()) { - val id = - cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) - - return QueryResult( - uri = ContentUris.withAppendedId( - baseUri, id - ), - lastModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)), - length = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)), - ) - } - } - } catch (t: Throwable) { - logError(t) - } - - return null - } - - override fun uri(): Uri? { - return query()?.uri - } - - override fun name(): String? { - if (isDir) return null - return namePath - } - - override fun type(): String? { - TODO("Not yet implemented") - } - - override fun filePath(): String { - return replaceDuplicateFileSeparators(relativePath + File.separator + namePath) - } - - override fun isDirectory(): Boolean { - return isDir - } - - override fun isFile(): Boolean { - return isFile - } - - override fun lastModified(): Long? { - if (isDir) return null - return query()?.lastModified - } - - override fun length(): Long? { - if (isDir) return null - val query = query() - val length = query?.length ?: return null - if (length <= 0) { - try { - contentResolver.openFileDescriptor(query.uri, "r") - .use { - it?.statSize - }?.let { - return it - } - } catch (e: FileNotFoundException) { - return null - } - - val inputStream: InputStream = openInputStream() ?: return null - return try { - inputStream.available().toLong() - } catch (t: Throwable) { - null - } finally { - inputStream.closeQuietly() - } - } - return length - } - - override fun canRead(): Boolean { - TODO("Not yet implemented") - } - - override fun canWrite(): Boolean { - TODO("Not yet implemented") - } - - private fun delete(uri: Uri): Boolean { - return contentResolver.delete(uri, null, null) > 0 - } - - override fun delete(): Boolean { - return if (isDir) { - (listFiles() ?: return false).all { - it.delete() - } - } else { - delete(uri() ?: return false) - } - } - - override fun exists(): Boolean { - if (isDir) return true - return query() != null - } - - override fun listFiles(): List? { - if (isFile) return null - try { - val projection = arrayOf( - MediaStore.MediaColumns.DISPLAY_NAME - ) - - val selection = - "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}'" - contentResolver.query( - baseUri, - projection, selection, null, null - )?.use { cursor -> - val out = ArrayList(cursor.count) - while (cursor.moveToNext()) { - val nameIdx = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) - if (nameIdx == -1) continue - val name = cursor.getString(nameIdx) - - appendRelativePath(name, false)?.let { new -> - out.add(new) - } - } - - out - } - } catch (t: Throwable) { - logError(t) - } - return null - } - - override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? { - if (isFile || displayName == null) return null - - val new = appendRelativePath(displayName, false) ?: return null - if (new.exists()) { - return new - } - - return null//SafeFile.fromUri(context, query(displayName ?: return null)?.uri ?: return null) - } - - override fun renameTo(name: String?): Boolean { - TODO("Not yet implemented") - } - - override fun openOutputStream(append: Boolean): OutputStream? { - try { - // use current file - uri()?.let { - return contentResolver.openOutputStream( - it, - if (append) "wa" else "wt" - ) - } - - // create a new file if current is not found, - // as we know it is new only write access is needed - createUri()?.let { - return contentResolver.openOutputStream( - it, - "w" - ) - } - return null - } catch (t: Throwable) { - return null - } - } - - override fun openInputStream(): InputStream? { - try { - return contentResolver.openInputStream(uri() ?: return null) - } catch (t: Throwable) { - return null - } - } - - override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? { - TODO("Not yet implemented") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt deleted file mode 100644 index 85a74963..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt +++ /dev/null @@ -1,244 +0,0 @@ -package com.lagradost.cloudstream3.utils.storage - -import android.content.Context -import android.net.Uri -import android.os.Build -import android.os.Environment -import com.hippo.unifile.UniFile -import com.hippo.unifile.UniRandomAccessFile - -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream - -interface SafeFile { - companion object { - fun fromUri(context: Context, uri: Uri): SafeFile? { - return UniFileWrapper(UniFile.fromUri(context, uri) ?: return null) - } - - fun fromFile(context: Context, file: File?): SafeFile? { - if (file == null) return null - // because UniFile sucks balls on Media we have to do this - val absPath = file.absolutePath.removePrefix(File.separator) - for (value in MediaFileContentType.values()) { - val prefixes = listOf(value.toAbsolutePath(), value.toPath()).map { it.removePrefix(File.separator) } - for (prefix in prefixes) { - if (!absPath.startsWith(prefix)) continue - return fromMedia( - context, - value, - absPath.removePrefix(prefix).ifBlank { File.separator } - ) - } - } - - return UniFileWrapper(UniFile.fromFile(file) ?: return null) - } - - fun fromAsset( - context: Context, - filename: String? - ): SafeFile? { - return UniFileWrapper( - UniFile.fromAsset(context.assets, filename ?: return null) ?: return null - ) - } - - fun fromResource( - context: Context, - id: Int - ): SafeFile? { - return UniFileWrapper( - UniFile.fromResource(context, id) ?: return null - ) - } - - fun fromMedia( - context: Context, - folderType: MediaFileContentType, - path: String = File.separator, - external: Boolean = true, - ): SafeFile? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - //fromUri(context, folderType.toUri(external))?.findFile(folderType.toPath())?.gotoDirectory(path) - - return MediaFile( - context = context, - folderType = folderType, - external = external, - absolutePath = path - ) - } else { - fromFile( - context, - File( - (Environment.getExternalStorageDirectory().absolutePath + File.separator + - folderType.toPath() + File.separator + folderType).replace( - File.separator + File.separator, - File.separator - ) - ) - ) - } - - } - } - - /*val uri: Uri? get() = getUri() - val name: String? get() = getName() - val type: String? get() = getType() - val filePath: String? get() = getFilePath() - val isFile: Boolean? get() = isFile() - val isDirectory: Boolean? get() = isDirectory() - val length: Long? get() = length() - val canRead: Boolean get() = canRead() - val canWrite: Boolean get() = canWrite() - val lastModified: Long? get() = lastModified()*/ - - @Throws(IOException::class) - fun isFileOrThrow(): Boolean { - return isFile() ?: throw IOException("Unable to get if file is a file or directory") - } - - @Throws(IOException::class) - fun lengthOrThrow(): Long { - return length() ?: throw IOException("Unable to get file length") - } - - @Throws(IOException::class) - fun isDirectoryOrThrow(): Boolean { - return isDirectory() ?: throw IOException("Unable to get if file is a directory") - } - - @Throws(IOException::class) - fun filePathOrThrow(): String { - return filePath() ?: throw IOException("Unable to get file path") - } - - @Throws(IOException::class) - fun uriOrThrow(): Uri { - return uri() ?: throw IOException("Unable to get uri") - } - - @Throws(IOException::class) - fun renameOrThrow(name: String?) { - if (!renameTo(name)) { - throw IOException("Unable to rename to $name") - } - } - - @Throws(IOException::class) - fun openOutputStreamOrThrow(append: Boolean = false): OutputStream { - return openOutputStream(append) ?: throw IOException("Unable to open output stream") - } - - @Throws(IOException::class) - fun openInputStreamOrThrow(): InputStream { - return openInputStream() ?: throw IOException("Unable to open input stream") - } - - @Throws(IOException::class) - fun existsOrThrow(): Boolean { - return exists() ?: throw IOException("Unable get if file exists") - } - - @Throws(IOException::class) - fun findFileOrThrow(displayName: String?, ignoreCase: Boolean = false): SafeFile { - return findFile(displayName, ignoreCase) ?: throw IOException("Unable find file") - } - - @Throws(IOException::class) - fun gotoDirectoryOrThrow( - directoryName: String?, - createMissingDirectories: Boolean = true - ): SafeFile { - return gotoDirectory(directoryName, createMissingDirectories) - ?: throw IOException("Unable to go to directory $directoryName") - } - - @Throws(IOException::class) - fun listFilesOrThrow(): List { - return listFiles() ?: throw IOException("Unable to get files") - } - - - @Throws(IOException::class) - fun createFileOrThrow(displayName: String?): SafeFile { - return createFile(displayName) ?: throw IOException("Unable to create file $displayName") - } - - @Throws(IOException::class) - fun createDirectoryOrThrow(directoryName: String?): SafeFile { - return createDirectory( - directoryName ?: throw IOException("Unable to create file with invalid name") - ) - ?: throw IOException("Unable to create directory $directoryName") - } - - @Throws(IOException::class) - fun deleteOrThrow() { - if (!delete()) { - throw IOException("Unable to delete file") - } - } - - /** file.gotoDirectory("a/b/c") -> "file/a/b/c/" where a null or blank directoryName - * returns itself. createMissingDirectories specifies if the dirs should be created - * when travelling or break at a dir not found */ - fun gotoDirectory( - directoryName: String?, - createMissingDirectories: Boolean = true - ): SafeFile? { - if (directoryName == null) return this - - return directoryName.split(File.separatorChar).filter { it.isNotBlank() } - .fold(this) { file: SafeFile?, directory -> - // as MediaFile does not actually create a directory we can do this - if (createMissingDirectories || this is MediaFile) { - file?.createDirectory(directory) - } else { - val next = file?.findFile(directory) - - // we require the file to be a directory - if (next?.isDirectory() != true) { - null - } else { - next - } - } - } - } - - - fun createFile(displayName: String?): SafeFile? - fun createDirectory(directoryName: String?): SafeFile? - fun uri(): Uri? - fun name(): String? - fun type(): String? - fun filePath(): String? - fun isDirectory(): Boolean? - fun isFile(): Boolean? - fun lastModified(): Long? - fun length(): Long? - fun canRead(): Boolean - fun canWrite(): Boolean - fun delete(): Boolean - fun exists(): Boolean? - fun listFiles(): List? - - // fun listFiles(filter: FilenameFilter?): Array? - fun findFile(displayName: String?, ignoreCase: Boolean = false): SafeFile? - - fun renameTo(name: String?): Boolean - - /** Open a stream on to the content associated with the file */ - fun openOutputStream(append: Boolean = false): OutputStream? - - /** Open a stream on to the content associated with the file */ - fun openInputStream(): InputStream? - - /** Get a random access stuff of the UniFile, "r" or "rw" */ - fun createRandomAccessFile(mode: String?): UniRandomAccessFile? -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt deleted file mode 100644 index f1592169..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.lagradost.cloudstream3.utils.storage - -import android.net.Uri -import com.hippo.unifile.UniFile -import com.hippo.unifile.UniRandomAccessFile -import com.lagradost.cloudstream3.mvvm.logError -import okhttp3.internal.closeQuietly -import java.io.InputStream -import java.io.OutputStream - -private fun UniFile.toFile(): SafeFile { - return UniFileWrapper(this) -} - -fun safe(apiCall: () -> T): T? { - return try { - apiCall.invoke() - } catch (throwable: Throwable) { - logError(throwable) - null - } -} - -class UniFileWrapper(val file: UniFile) : SafeFile { - override fun createFile(displayName: String?): SafeFile? { - return file.createFile(displayName)?.toFile() - } - - override fun createDirectory(directoryName: String?): SafeFile? { - return file.createDirectory(directoryName)?.toFile() - } - - override fun uri(): Uri? { - return safe { file.uri } - } - - override fun name(): String? { - return safe { file.name } - } - - override fun type(): String? { - return safe { file.type } - } - - override fun filePath(): String? { - return safe { file.filePath } - } - - override fun isDirectory(): Boolean? { - return safe { file.isDirectory } - } - - override fun isFile(): Boolean? { - return safe { file.isFile } - } - - override fun lastModified(): Long? { - return safe { file.lastModified() } - } - - override fun length(): Long? { - return safe { - val len = file.length() - if (len <= 1) { - val inputStream = this.openInputStream() ?: return@safe null - try { - inputStream.available().toLong() - } finally { - inputStream.closeQuietly() - } - } else { - len - } - } - } - - override fun canRead(): Boolean { - return safe { file.canRead() } ?: false - } - - override fun canWrite(): Boolean { - return safe { file.canWrite() } ?: false - } - - override fun delete(): Boolean { - return safe { file.delete() } ?: false - } - - override fun exists(): Boolean? { - return safe { file.exists() } - } - - override fun listFiles(): List? { - return safe { file.listFiles()?.mapNotNull { it?.toFile() } } - } - - override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? { - return safe { file.findFile(displayName, ignoreCase)?.toFile() } - } - - override fun renameTo(name: String?): Boolean { - return safe { file.renameTo(name) } ?: return false - } - - override fun openOutputStream(append: Boolean): OutputStream? { - return safe { file.openOutputStream(append) } - } - - override fun openInputStream(): InputStream? { - return safe { file.openInputStream() } - } - - override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? { - return safe { file.createRandomAccessFile(mode) } - } -} \ No newline at end of file From f01820059b520912d77be56ce8a39c2530f6f886 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 27 Aug 2023 19:07:08 +0200 Subject: [PATCH 206/570] delete resume watching + delete bookmarks buttons. fixed backup crash --- .../cloudstream3/ui/home/HomeFragment.kt | 6 ++- .../ui/home/HomeParentItemAdapterPreview.kt | 31 ++++++++--- .../cloudstream3/ui/home/HomeViewModel.kt | 36 ++++++++++--- .../cloudstream3/utils/BackupUtils.kt | 53 +++---------------- .../cloudstream3/utils/DataStoreHelper.kt | 12 +++-- 5 files changed, 72 insertions(+), 66 deletions(-) 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 fa0b6dfb..b84c619e 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 @@ -658,12 +658,14 @@ class HomeFragment : Fragment() { return@observeNullable } - bottomSheetDialog = activity?.loadHomepageList(item, expandCallback = { + val (items, delete) = item + + bottomSheetDialog = activity?.loadHomepageList(items, expandCallback = { homeViewModel.expandAndReturn(it) }, dismissCallback = { homeViewModel.popup(null) bottomSheetDialog = null - }) + }, deleteCallback = delete) } homeViewModel.reloadStored() 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 13497a99..1d8e1399 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 @@ -246,7 +246,7 @@ class HomeParentItemAdapterPreview( private val previewViewpagerText: ViewGroup = itemView.findViewById(R.id.home_preview_viewpager_text) - // private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview) + // private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview) private var resumeHolder: View = itemView.findViewById(R.id.home_watch_holder) private var resumeRecyclerView: RecyclerView = itemView.findViewById(R.id.home_watch_child_recyclerview) @@ -257,7 +257,7 @@ class HomeParentItemAdapterPreview( private var homeAccount: View? = itemView.findViewById(R.id.home_preview_switch_account) - private var topPadding : View? = itemView.findViewById(R.id.home_padding) + private var topPadding: View? = itemView.findViewById(R.id.home_padding) private val homeNonePadding: View = itemView.findViewById(R.id.home_none_padding) @@ -283,7 +283,11 @@ class HomeParentItemAdapterPreview( item.plot ?: "" homePreviewText.text = item.name - populateChips(homePreviewTags,item.tags ?: emptyList(), R.style.ChipFilledSemiTransparent) + populateChips( + homePreviewTags, + item.tags ?: emptyList(), + R.style.ChipFilledSemiTransparent + ) homePreviewTags.isGone = item.tags.isNullOrEmpty() @@ -413,7 +417,7 @@ class HomeParentItemAdapterPreview( Pair(itemView.findViewById(R.id.home_plan_to_watch_btt), WatchType.PLANTOWATCH), ) - private val toggleListHolder : ChipGroup? = itemView.findViewById(R.id.home_type_holder) + private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder) init { previewViewpager.setPageTransformer(HomeScrollTransformer()) @@ -422,8 +426,14 @@ class HomeParentItemAdapterPreview( resumeRecyclerView.adapter = resumeAdapter bookmarkRecyclerView.adapter = bookmarkAdapter - resumeRecyclerView.setLinearListLayout(nextLeft = R.id.nav_rail_view, nextRight = FOCUS_SELF) - bookmarkRecyclerView.setLinearListLayout(nextLeft = R.id.nav_rail_view, nextRight = FOCUS_SELF) + resumeRecyclerView.setLinearListLayout( + nextLeft = R.id.nav_rail_view, + nextRight = FOCUS_SELF + ) + bookmarkRecyclerView.setLinearListLayout( + nextLeft = R.id.nav_rail_view, + nextRight = FOCUS_SELF + ) fixPaddingStatusbarMargin(topPadding) @@ -547,7 +557,10 @@ class HomeParentItemAdapterPreview( resumeWatching, false ), 1, false - ) + ), + deleteCallback = { + viewModel.deleteResumeWatching() + } ) } } @@ -572,7 +585,9 @@ class HomeParentItemAdapterPreview( list, false ), 1, false - ) + ), deleteCallback = { + viewModel.deleteBookmarks(list) + } ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index e8cf8863..b27223ec 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -41,6 +41,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllBookmarkedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData @@ -92,6 +94,21 @@ class HomeViewModel : ViewModel() { } } + fun deleteResumeWatching() { + deleteAllResumeStateIds() + loadResumeWatching() + } + + fun deleteBookmarks(list: List) { + list.forEach { DataStoreHelper.deleteBookmarkedData(it.id) } + loadStoredData() + } + + fun deleteBookmarks() { + deleteAllBookmarkedData() + loadStoredData() + } + var repo: APIRepository? = null private val _apiName = MutableLiveData() @@ -394,11 +411,14 @@ class HomeViewModel : ViewModel() { } - private val _popup = MutableLiveData(null) - val popup: LiveData = _popup + private val _popup = MutableLiveData Unit)?>?>(null) + val popup: LiveData Unit)?>?> = _popup - fun popup(list: ExpandableHomepageList?) { - _popup.postValue(list) + fun popup(list: ExpandableHomepageList?, deleteCallback: (() -> Unit)? = null) { + if (list == null) + _popup.postValue(null) + else + _popup.postValue(list to deleteCallback) } private fun bookmarksUpdated(unused: Boolean) { @@ -436,8 +456,7 @@ class HomeViewModel : ViewModel() { // do nothing } - fun reloadStored() { - loadResumeWatching() + fun loadStoredData() { val list = EnumSet.noneOf(WatchType::class.java) getKey(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let { list.addAll(it) @@ -445,6 +464,11 @@ class HomeViewModel : ViewModel() { loadStoredData(list) } + fun reloadStored() { + loadResumeWatching() + loadStoredData() + } + fun click(load: LoadClickCallback) { loadResult(load.response.url, load.response.apiName, load.action) } 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 41326eb8..96593769 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -25,6 +25,7 @@ import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_T 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.ui.result.txt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs @@ -143,66 +144,26 @@ object BackupUtils { } @SuppressLint("SimpleDateFormat") - fun FragmentActivity.backup() { + fun FragmentActivity.backup() = ioSafe { var fileStream: OutputStream? = null var printStream: PrintWriter? = null try { if (!checkWrite()) { - showToast(getString(R.string.backup_failed), Toast.LENGTH_LONG) + showToast(R.string.backup_failed, Toast.LENGTH_LONG) requestRW() - return + return@ioSafe } val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) - val ext = "json" + val ext = "txt" val displayName = "CS3_Backup_${date}" val backupFile = getBackup() - val stream = setupStream(this, displayName, null, ext, false) + val stream = setupStream(this@backup, displayName, null, ext, false) fileStream = stream.openNew() printStream = PrintWriter(fileStream) printStream.print(mapper.writeValueAsString(backupFile)) - /*val steam = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - && subDir?.isDownloadDir() == true - ) { - val cr = this.contentResolver - val contentUri = - MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI - //val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - - val newFile = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) - put(MediaStore.MediaColumns.TITLE, displayName) - // While it a json file we store as txt because not - // all file managers support mimetype json - put(MediaStore.MediaColumns.MIME_TYPE, "text/plain") - //put(MediaStore.MediaColumns.RELATIVE_PATH, folder) - } - - val newFileUri = cr.insert( - contentUri, - newFile - ) ?: throw IOException("Error creating file uri") - cr.openOutputStream(newFileUri, "w") - ?: throw IOException("Error opening stream") - } else { - val fileName = "$displayName.$ext" - val rFile = subDir?.findFile(fileName) - if (rFile?.exists() == true) { - rFile.delete() - } - val file = - subDir?.createFile(fileName) - ?: throw IOException("Error creating file") - if (!file.exists()) throw IOException("File does not exist") - file.openOutputStream() - } - - val printStream = PrintWriter(steam) - printStream.print(mapper.writeValueAsString(backupFile)) - printStream.close()*/ - showToast( R.string.backup_success, Toast.LENGTH_LONG @@ -211,7 +172,7 @@ object BackupUtils { logError(e) try { showToast( - getString(R.string.backup_failed_error_format).format(e.toString()), + txt(R.string.backup_failed_error_format, e.toString()), Toast.LENGTH_LONG ) } catch (e: Exception) { 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 991651dc..2eb2ab01 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -353,6 +353,12 @@ object DataStoreHelper { removeKeys(folder2) } + fun deleteBookmarkedData(id : Int?) { + if (id == null) return + removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString()) + removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) + } + fun getAllResumeStateIds(): List? { val folder = "$currentAccount/$RESULT_RESUME_WATCHING" return getKeys(folder)?.mapNotNull { @@ -519,12 +525,10 @@ object DataStoreHelper { fun setResultWatchState(id: Int?, status: Int) { if (id == null) return - val folder = "$currentAccount/$RESULT_WATCH_STATE" if (status == WatchType.NONE.internalId) { - removeKey(folder, id.toString()) - removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) + deleteBookmarkedData(id) } else { - setKey(folder, id.toString(), status) + setKey("$currentAccount/$RESULT_WATCH_STATE", id.toString(), status) } } From ce1f48978be3aada3e6b68d8726539375c3f14f1 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 28 Aug 2023 20:56:58 +0200 Subject: [PATCH 207/570] fixed download error --- app/build.gradle.kts | 2 +- .../cloudstream3/extractors/SpeedoStream.kt | 13 +++++++++---- .../lagradost/cloudstream3/utils/ExtractorApi.kt | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 333fbfb8..3927d081 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -233,7 +233,7 @@ dependencies { // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 - implementation("com.github.LagradOst:SafeFile:0.0.2") + implementation("com.github.LagradOst:SafeFile:0.0.3") // API because cba maintaining it myself implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt index 3f6fff2f..213ecdf3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt @@ -7,15 +7,22 @@ import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +class SpeedoStream2 : SpeedoStream() { + override val mainUrl = "https://speedostream.mom" +} + class SpeedoStream1 : SpeedoStream() { override val mainUrl = "https://speedostream.pm" } open class SpeedoStream : ExtractorApi() { override val name = "SpeedoStream" - override val mainUrl = "https://speedostream.mom" + override val mainUrl = "https://speedostream.bond" override val requiresReferer = true + // .bond, .pm, .mom redirect to .bond + private val hostUrl = "https://speedostream.bond" + override suspend fun getUrl(url: String, referer: String?): List { val sources = mutableListOf() app.get(url, referer = referer).document.select("script").map { script -> @@ -26,7 +33,7 @@ open class SpeedoStream : ExtractorApi() { M3u8Helper.generateM3u8( name, it.file, - "$mainUrl/", + "$hostUrl/", ).forEach { m3uData -> sources.add(m3uData) } } } @@ -37,6 +44,4 @@ open class SpeedoStream : ExtractorApi() { private data class File( @JsonProperty("file") val file: String, ) - - } 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 83c61542..ffda32d7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -389,6 +389,7 @@ val extractorApis: MutableList = arrayListOf( Acefile(), SpeedoStream(), SpeedoStream1(), + SpeedoStream2(), Zorofile(), Embedgram(), Mvidoo(), From 6089cbc48493d746a110bba8d9997c3d2209b82e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 30 Aug 2023 00:52:34 +0200 Subject: [PATCH 208/570] fixed subs on downloads --- app/build.gradle.kts | 2 +- .../ui/player/DownloadFileGenerator.kt | 73 +++++++++++-------- .../utils/VideoDownloadManager.kt | 2 +- 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3927d081..55d0f7ae 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -233,7 +233,7 @@ dependencies { // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 - implementation("com.github.LagradOst:SafeFile:0.0.3") + implementation("com.github.LagradOst:SafeFile:0.0.5") // API because cba maintaining it myself implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index baf7ed52..1b618e45 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -50,6 +50,10 @@ class DownloadFileGenerator( return null } + fun cleanDisplayName(name: String): String { + return name.substringBeforeLast('.').trim() + } + override suspend fun generateLinks( clearCache: Boolean, isCasting: Boolean, @@ -58,39 +62,48 @@ class DownloadFileGenerator( offset: Int, ): Boolean { val meta = episodes[currentIndex + offset] - callback(Pair(null, meta)) + callback(null to meta) - context?.let { ctx -> - val relative = meta.relativePath - val display = meta.displayName + val ctx = context ?: return true + val relative = meta.relativePath ?: return true + val display = meta.displayName ?: return true - if (display == null || relative == null) { - return@let + val cleanDisplay = cleanDisplayName(display) + + VideoDownloadManager.getFolder(ctx, relative, meta.basePath) + ?.forEach { (name, uri) -> + // only these files are allowed, so no videos as subtitles + if (listOf( + ".vtt", + ".srt", + ".txt", + ".ass", + ".ttml", + ".sbv", + ".dfxp" + ).none { name.contains(it, true) } + ) return@forEach + + // cant have the exact same file as a subtitle + if (name.equals(display, true)) return@forEach + + val cleanName = cleanDisplayName(name) + + // we only want files with the approx same name + if (!cleanName.startsWith(cleanDisplay, true)) return@forEach + + val realName = cleanName.removePrefix(cleanDisplay) + + subtitleCallback( + SubtitleData( + realName.ifBlank { ctx.getString(R.string.default_subtitles) }, + uri.toString(), + SubtitleOrigin.DOWNLOADED_FILE, + name.toSubtitleMimeType(), + emptyMap() + ) + ) } - VideoDownloadManager.getFolder(ctx, relative, meta.basePath) - ?.forEach { file -> - val name = display.removeSuffix(".mp4") - if (file.first != meta.displayName && file.first.startsWith(name)) { - val realName = file.first.removePrefix(name) - .removeSuffix(".vtt") - .removeSuffix(".srt") - .removeSuffix(".txt") - .trim() - .removePrefix("(") - .removeSuffix(")") - - subtitleCallback( - SubtitleData( - realName.ifBlank { ctx.getString(R.string.default_subtitles) }, - file.second.toString(), - SubtitleOrigin.DOWNLOADED_FILE, - name.toSubtitleMimeType(), - emptyMap() - ) - ) - } - } - } return true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 6425ba66..72eb002a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -505,7 +505,7 @@ object VideoDownloadManager { ): List>? { val base = basePathToFile(context, basePath) val folder = base?.gotoDirectory(relativePath, false) ?: return null - if (folder.isDirectory() != false) return null + //if (folder.isDirectory() != false) return null return folder.listFiles() ?.mapNotNull { (it.name() ?: "") to (it.uri() ?: return@mapNotNull null) } From 9c991f2abd53bdbf50f7ae52f3fe64221d341e57 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Sat, 2 Sep 2023 05:32:18 +0700 Subject: [PATCH 209/570] extractor: fix chillx (#583) * Extractor: added Rabbitstream * Extractor: added Rabbitstream * extractor: fix Chillx * comply --------- Co-authored-by: Sofie99 --- .../cloudstream3/extractors/Chillx.kt | 58 +---------- .../cloudstream3/extractors/Gdriveplayer.kt | 88 +---------------- .../extractors/helper/AesHelper.kt | 95 +++++++++++++++++++ 3 files changed, 102 insertions(+), 139 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index b4f3d897..bcf8848c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -2,15 +2,12 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.extractors.helper.* +import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities -import javax.crypto.Cipher -import javax.crypto.SecretKeyFactory -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.PBEKeySpec -import javax.crypto.spec.SecretKeySpec class Moviesapi : Chillx() { override val name = "Moviesapi" @@ -32,7 +29,7 @@ open class Chillx : ExtractorApi() { override val requiresReferer = true companion object { - private const val KEY = "11x&W5UBrcqn\$9Yl" + private const val KEY = "m4H6D9%0\$N&F6rQ&" } override suspend fun getUrl( @@ -47,8 +44,7 @@ open class Chillx : ExtractorApi() { referer = referer ).text )?.groupValues?.get(1) - val encData = AppUtils.tryParseJson(base64Decode(master ?: return)) - val decrypt = cryptoAESHandler(encData ?: return, KEY, false) + val decrypt = cryptoAESHandler(master ?: return, KEY.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1) @@ -86,52 +82,6 @@ open class Chillx : ExtractorApi() { } } - private fun cryptoAESHandler( - data: AESData, - pass: String, - encrypt: Boolean = true - ): String { - val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512") - val spec = PBEKeySpec( - pass.toCharArray(), - data.salt?.hexToByteArray(), - data.iterations?.toIntOrNull() ?: 1, - 256 - ) - val key = factory.generateSecret(spec) - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - return if (!encrypt) { - cipher.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(key.encoded, "AES"), - IvParameterSpec(data.iv?.hexToByteArray()) - ) - String(cipher.doFinal(base64DecodeArray(data.ciphertext.toString()))) - } else { - cipher.init( - Cipher.ENCRYPT_MODE, - SecretKeySpec(key.encoded, "AES"), - IvParameterSpec(data.iv?.hexToByteArray()) - ) - base64Encode(cipher.doFinal(data.ciphertext?.toByteArray())) - } - } - - private fun String.hexToByteArray(): ByteArray { - check(length % 2 == 0) { "Must have an even length" } - return chunked(2) - .map { it.toInt(16).toByte() } - - .toByteArray() - } - - data class AESData( - @JsonProperty("ciphertext") val ciphertext: String? = null, - @JsonProperty("iv") val iv: String? = null, - @JsonProperty("salt") val salt: String? = null, - @JsonProperty("iterations") val iterations: String? = null, - ) - data class Tracks( @JsonProperty("file") val file: String? = null, @JsonProperty("label") val label: String? = null, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt index df9c74a4..8d1a4d07 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt @@ -2,14 +2,10 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import org.jsoup.nodes.Element -import java.security.DigestException -import java.security.MessageDigest -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec class DatabaseGdrive2 : Gdriveplayer() { override var mainUrl = "https://databasegdriveplayer.co" @@ -65,78 +61,6 @@ open class Gdriveplayer : ExtractorApi() { ?.data()?.let { getAndUnpack(it) } } - private fun String.decodeHex(): ByteArray { - check(length % 2 == 0) { "Must have an even length" } - return chunked(2) - .map { it.toInt(16).toByte() } - .toByteArray() - } - - // https://stackoverflow.com/a/41434590/8166854 - private fun GenerateKeyAndIv( - password: ByteArray, - salt: ByteArray, - hashAlgorithm: String = "MD5", - keyLength: Int = 32, - ivLength: Int = 16, - iterations: Int = 1 - ): List? { - - val md = MessageDigest.getInstance(hashAlgorithm) - val digestLength = md.digestLength - val targetKeySize = keyLength + ivLength - val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength - val generatedData = ByteArray(requiredLength) - var generatedLength = 0 - - try { - md.reset() - - while (generatedLength < targetKeySize) { - if (generatedLength > 0) - md.update( - generatedData, - generatedLength - digestLength, - digestLength - ) - - md.update(password) - md.update(salt, 0, 8) - md.digest(generatedData, generatedLength, digestLength) - - for (i in 1 until iterations) { - md.update(generatedData, generatedLength, digestLength) - md.digest(generatedData, generatedLength, digestLength) - } - - generatedLength += digestLength - } - return listOf( - generatedData.copyOfRange(0, keyLength), - generatedData.copyOfRange(keyLength, targetKeySize) - ) - } catch (e: DigestException) { - return null - } - } - - private fun cryptoAESHandler( - data: AesData, - pass: ByteArray, - encrypt: Boolean = true - ): String? { - val (key, iv) = GenerateKeyAndIv(pass, data.s.decodeHex()) ?: return null - val cipher = Cipher.getInstance("AES/CBC/NoPadding") - return if (!encrypt) { - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) - String(cipher.doFinal(base64DecodeArray(data.ct))) - } else { - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) - base64Encode(cipher.doFinal(data.ct.toByteArray())) - - } - } - private fun Regex.first(str: String): String? { return find(str)?.groupValues?.getOrNull(1) } @@ -154,14 +78,14 @@ open class Gdriveplayer : ExtractorApi() { val document = app.get(url).document val eval = unpackJs(document)?.replace("\\", "") ?: return - val data = tryParseJson(Regex("data='(\\S+?)'").first(eval)) ?: return + val data = Regex("data='(\\S+?)'").first(eval) ?: return val password = Regex("null,['|\"](\\w+)['|\"]").first(eval) ?.split(Regex("\\D+")) ?.joinToString("") { Char(it.toInt()).toString() }.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() } ?: throw ErrorLoadingException("can't find password") - val decryptedData = cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "") + val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "") val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],") val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],") @@ -194,12 +118,6 @@ open class Gdriveplayer : ExtractorApi() { } - data class AesData( - @JsonProperty("ct") val ct: String, - @JsonProperty("iv") val iv: String, - @JsonProperty("s") val s: String - ) - data class Tracks( @JsonProperty("file") val file: String, @JsonProperty("kind") val kind: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt new file mode 100644 index 00000000..b41eae52 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt @@ -0,0 +1,95 @@ +package com.lagradost.cloudstream3.extractors.helper + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.base64DecodeArray +import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.utils.AppUtils +import java.security.DigestException +import java.security.MessageDigest +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +object AesHelper { + + private const val HASH = "AES/CBC/PKCS5PADDING" + private const val KDF = "MD5" + + fun cryptoAESHandler( + data: String, + pass: ByteArray, + encrypt: Boolean = true, + padding: String = HASH, + ): String? { + val parse = AppUtils.tryParseJson(data) ?: return null + val (key, iv) = generateKeyAndIv(pass, parse.s.hexToByteArray()) ?: return null + val cipher = Cipher.getInstance(padding) + return if (!encrypt) { + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + String(cipher.doFinal(base64DecodeArray(parse.ct))) + } else { + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + base64Encode(cipher.doFinal(parse.ct.toByteArray())) + } + } + + // https://stackoverflow.com/a/41434590/8166854 + fun generateKeyAndIv( + password: ByteArray, + salt: ByteArray, + hashAlgorithm: String = KDF, + keyLength: Int = 32, + ivLength: Int = 16, + iterations: Int = 1 + ): Pair? { + + val md = MessageDigest.getInstance(hashAlgorithm) + val digestLength = md.digestLength + val targetKeySize = keyLength + ivLength + val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength + val generatedData = ByteArray(requiredLength) + var generatedLength = 0 + + try { + md.reset() + + while (generatedLength < targetKeySize) { + if (generatedLength > 0) + md.update( + generatedData, + generatedLength - digestLength, + digestLength + ) + + md.update(password) + md.update(salt, 0, 8) + md.digest(generatedData, generatedLength, digestLength) + + for (i in 1 until iterations) { + md.update(generatedData, generatedLength, digestLength) + md.digest(generatedData, generatedLength, digestLength) + } + + generatedLength += digestLength + } + return generatedData.copyOfRange(0, keyLength) to generatedData.copyOfRange(keyLength, targetKeySize) + } catch (e: DigestException) { + return null + } + } + + fun String.hexToByteArray(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + } + + private data class AesData( + @JsonProperty("ct") val ct: String, + @JsonProperty("iv") val iv: String, + @JsonProperty("s") val s: String + ) + +} \ No newline at end of file From 6211b02e85b0dcd4ab5a2954623917e8c27ba552 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 3 Sep 2023 23:32:43 +0200 Subject: [PATCH 210/570] switched from isM3u8 to ExtractorLinkType --- .../cloudstream3/extractors/Pelisplus.kt | 3 +- .../cloudstream3/extractors/Vidstream.kt | 3 +- .../cloudstream3/extractors/WcoStream.kt | 4 +- .../cloudstream3/extractors/Wibufile.kt | 6 +- .../cloudstream3/ui/APIRepository.kt | 14 ++- .../cloudstream3/ui/ControllerActivity.kt | 4 +- .../cloudstream3/ui/player/CS3IPlayer.kt | 12 +- .../ui/player/DownloadFileGenerator.kt | 4 +- .../ui/player/ExtractorLinkGenerator.kt | 7 +- .../cloudstream3/ui/player/IGenerator.kt | 43 ++++++- .../cloudstream3/ui/player/LinkGenerator.kt | 2 +- .../ui/player/PlayerGeneratorViewModel.kt | 12 +- .../ui/player/RepoLinkGenerator.kt | 21 ++-- .../ui/result/ResultViewModel2.kt | 40 +++--- .../cloudstream3/utils/CastHelper.kt | 6 +- .../cloudstream3/utils/ExtractorApi.kt | 119 +++++++++++++++--- .../utils/VideoDownloadManager.kt | 78 +++++++----- 17 files changed, 269 insertions(+), 109 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt index 45ec4c2f..4163cd94 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt @@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.extractorApis import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.loadExtractor @@ -66,7 +67,7 @@ open class Pelisplus(val mainUrl: String) { href, page.url, getQualityFromName(qual), - element.attr("href").contains(".m3u8") + type = INFER_TYPE ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt index 7eb7fbac..c6493dbe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt @@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.argamap import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.extractorApis import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.loadExtractor @@ -70,7 +71,7 @@ class Vidstream(val mainUrl: String) { href, page.url, getQualityFromName(qual), - element.attr("href").contains(".m3u8") + type = INFER_TYPE ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt index 6cc486cd..659d7804 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt @@ -7,6 +7,7 @@ import com.lagradost.cloudstream3.extractors.helper.NineAnimeHelper.encrypt import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.Qualities class Vidstreamz : WcoStream() { @@ -126,8 +127,7 @@ open class WcoStream : ExtractorApi() { if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server") return response.parsed().data.media.sources.map { - ExtractorLink(name, it.file,it.file,host,Qualities.Unknown.value,it.file.contains(".m3u8")) + ExtractorLink(name, it.file, it.file, host, Qualities.Unknown.value, type = INFER_TYPE) } - } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt index ae1e872a..c69f0938 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt @@ -4,8 +4,8 @@ import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.Qualities -import java.net.URI open class Wibufile : ExtractorApi() { override val name: String = "Wibufile" @@ -28,10 +28,8 @@ open class Wibufile : ExtractorApi() { video ?: return, "$mainUrl/", Qualities.Unknown.value, - URI(url).path.endsWith(".m3u8") + type = INFER_TYPE ) ) - } - } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 4ab2e8e2..a075cc2e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -1,16 +1,24 @@ package com.lagradost.cloudstream3.ui -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.DubStatus +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.HomePageResponse +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent +import com.lagradost.cloudstream3.MainPageRequest +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.fixUrl import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.ExtractorLink import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope.coroutineContext import kotlinx.coroutines.async import kotlinx.coroutines.delay @@ -174,7 +182,7 @@ class APIRepository(val api: MainAPI) { data: String, isCasting: Boolean, subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit + callback: (ExtractorLink) -> Unit, ): Boolean { if (isInvalidData(data)) return false // this makes providers cleaner return try { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 46ddce09..6c0e7796 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -25,6 +25,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.sortSubs import com.lagradost.cloudstream3.sortUrls +import com.lagradost.cloudstream3.ui.player.LoadType import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultEpisode @@ -294,7 +295,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi val generator = RepoLinkGenerator(listOf(epData)) val isSuccessful = safeApiCall { - generator.generateLinks(clearCache = false, isCasting = true, + generator.generateLinks( + clearCache = false, type = LoadType.Chromecast, callback = { it.first?.let { link -> currentLinks.add(link) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 2067eb04..fd1da5ca 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -53,9 +53,11 @@ import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList +import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File +import java.lang.IllegalArgumentException import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession @@ -1257,10 +1259,12 @@ class CS3IPlayer : IPlayer { HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) } - val mime = when { - link.isM3u8 -> MimeTypes.APPLICATION_M3U8 - link.isDash -> MimeTypes.APPLICATION_MPD - else -> MimeTypes.VIDEO_MP4 + val mime = when(link.type) { + ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8 + ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD + ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4 + ExtractorLinkType.TORRENT -> throw IllegalArgumentException("No torrent support") + ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet support") } val mediaItems = if (link is ExtractorLinkPlayList) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index 1b618e45..b0223bb5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -56,10 +56,10 @@ class DownloadFileGenerator( override suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, - offset: Int, + offset: Int ): Boolean { val meta = episodes[currentIndex + offset] callback(null to meta) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt index 7c19e97d..d8d2d537 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt @@ -37,14 +37,17 @@ class ExtractorLinkGenerator( override suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, offset: Int ): Boolean { subtitles.forEach(subtitleCallback) + val allowedTypes = type.toSet() links.forEach { - callback.invoke(it to null) + if(allowedTypes.contains(it.type)) { + callback.invoke(it to null) + } } return true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index a1287e6a..af74cb57 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -1,8 +1,43 @@ package com.lagradost.cloudstream3.ui.player import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.ExtractorUri +enum class LoadType { + Unknown, + InApp, + InAppDownload, + ExternalApp, + Browser, + Chromecast +} + +fun LoadType.toSet() : Set { + return when(this) { + LoadType.InApp -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) + LoadType.Browser -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) + LoadType.InAppDownload -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.M3U8 + ) + LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet() + LoadType.Chromecast -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) + } +} + interface IGenerator { val hasCache: Boolean @@ -13,15 +48,15 @@ interface IGenerator { fun goto(index: Int) fun getCurrentId(): Int? // this is used to save data or read data about this id - fun getCurrent(offset : Int = 0): Any? // this is used to get metadata about the current playing, can return null - fun getAll() : List? // this us used to get the metadata about all entries, not needed + fun getCurrent(offset: Int = 0): Any? // this is used to get metadata about the current playing, can return null + fun getAll(): List? // this us used to get the metadata about all entries, not needed /* not safe, must use try catch */ suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, - offset : Int = 0, + offset: Int = 0, ): Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index 0b560857..ba2cdb40 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -48,7 +48,7 @@ class LinkGenerator( override suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, offset: Int diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 1b13b519..42659f8d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -78,10 +78,10 @@ class PlayerGeneratorViewModel : ViewModel() { if (generator?.hasCache == true && generator?.hasNext() == true) { safeApiCall { generator?.generateLinks( + type = LoadType.InApp, clearCache = false, - isCasting = false, - {}, - {}, + callback = {}, + subtitleCallback = {}, offset = 1 ) } @@ -147,7 +147,7 @@ class PlayerGeneratorViewModel : ViewModel() { } } - fun loadLinks(clearCache: Boolean = false, isCasting: Boolean = false) { + fun loadLinks(clearCache: Boolean = false, type: LoadType = LoadType.InApp) { Log.i(TAG, "loadLinks") currentJob?.cancel() @@ -162,14 +162,14 @@ class PlayerGeneratorViewModel : ViewModel() { // load more data _loadingLinks.postValue(Resource.Loading()) val loadingState = safeApiCall { - generator?.generateLinks(clearCache = clearCache, isCasting = isCasting, { + generator?.generateLinks(type = type,clearCache = clearCache, callback = { currentLinks.add(it) // Clone to prevent ConcurrentModificationException normalSafeApiCall { // Extra normalSafeApiCall since .toSet() iterates. _currentLinks.postValue(currentLinks.toSet()) } - }, { + }, subtitleCallback = { currentSubs.add(it) normalSafeApiCall { _currentSubs.postValue(currentSubs.toSet()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt index 2ce53ea5..d55da57c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt @@ -67,18 +67,19 @@ class RepoLinkGenerator( override suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, - offset: Int, + offset: Int ): Boolean { + val allowedTypes = type.toSet() val index = currentIndex val current = episodes.getOrNull(index + offset) ?: return false val (currentLinkCache, currentSubsCache) = if (clearCache) { Pair(mutableSetOf(), mutableSetOf()) } else { - cache[Pair(current.apiName, current.id)] ?: Pair(mutableSetOf(), mutableSetOf()) + cache[current.apiName to current.id] ?: Pair(mutableSetOf(), mutableSetOf()) } //val currentLinkCache = if (clearCache) mutableSetOf() else linkCache[index].toMutableSet() @@ -88,9 +89,9 @@ class RepoLinkGenerator( val currentSubsUrls = mutableSetOf() // makes all subs urls unique val currentSubsNames = mutableSetOf() // makes all subs names unique - currentLinkCache.forEach { link -> + currentLinkCache.filter { allowedTypes.contains(it.type) }.forEach { link -> currentLinks.add(link.url) - callback(Pair(link, null)) + callback(link to null) } currentSubsCache.forEach { sub -> @@ -108,8 +109,8 @@ class RepoLinkGenerator( val result = APIRepository( getApiFromNameNull(current.apiName) ?: throw Exception("This provider does not exist") ).loadLinks(current.data, - isCasting, - { file -> + isCasting = LoadType.Chromecast == type, + subtitleCallback = { file -> val correctFile = PlayerSubtitleHelper.getSubtitleData(file) if (!currentSubsUrls.contains(correctFile.url)) { currentSubsUrls.add(correctFile.url) @@ -132,12 +133,14 @@ class RepoLinkGenerator( } } }, - { link -> + callback = { link -> Log.d(TAG, "Loaded ExtractorLink: $link") if (!currentLinks.contains(link.url)) { if (!currentLinkCache.contains(link)) { currentLinks.add(link.url) - callback(Pair(link, null)) + if (allowedTypes.contains(link.type)) { + callback(Pair(link, null)) + } currentLinkCache.add(link) //linkCache[index] = currentLinkCache } 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 82d9a8fe..b2c57137 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 @@ -36,6 +36,7 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.IGenerator +import com.lagradost.cloudstream3.ui.player.LoadType import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction @@ -745,7 +746,7 @@ class ResultViewModel2 : ViewModel() { val generator = RepoLinkGenerator(listOf(episode)) val currentLinks = mutableSetOf() val currentSubs = mutableSetOf() - generator.generateLinks(clearCache = false, isCasting = false, callback = { + generator.generateLinks(clearCache = false, LoadType.Chromecast, callback = { it.first?.let { link -> currentLinks.add(link) } @@ -825,7 +826,7 @@ class ResultViewModel2 : ViewModel() { isVisible: Boolean = true ) { if (activity == null) return - loadLinks(result, isVisible = isVisible, isCasting = true) { data -> + loadLinks(result, isVisible = isVisible, LoadType.Chromecast) { data -> startChromecast(activity, result, data.links, data.subs, 0) } } @@ -936,7 +937,7 @@ class ResultViewModel2 : ViewModel() { private fun loadLinks( result: ResultEpisode, isVisible: Boolean, - isCasting: Boolean, + type: LoadType, clearCache: Boolean = false, work: suspend (CoroutineScope.(LinkLoadingResult) -> Unit) ) { @@ -945,7 +946,7 @@ class ResultViewModel2 : ViewModel() { val links = loadLinks( result, isVisible = isVisible, - isCasting = isCasting, + type = type, clearCache = clearCache ) if (!this.isActive) return@ioSafe @@ -956,11 +957,11 @@ class ResultViewModel2 : ViewModel() { private var currentLoadLinkJob: Job? = null private fun acquireSingleLink( result: ResultEpisode, - isCasting: Boolean, + type: LoadType, text: UiText, callback: (Pair) -> Unit, ) { - loadLinks(result, isVisible = true, isCasting = isCasting) { links -> + loadLinks(result, isVisible = true, type) { links -> postPopup( text, links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { @@ -971,11 +972,10 @@ class ResultViewModel2 : ViewModel() { private fun acquireSingleSubtitle( result: ResultEpisode, - isCasting: Boolean, text: UiText, callback: (Pair) -> Unit, ) { - loadLinks(result, isVisible = true, isCasting = isCasting) { links -> + loadLinks(result, isVisible = true, type = LoadType.Unknown) { links -> postPopup( text, links.subs.map { txt(it.name) }) @@ -988,7 +988,7 @@ class ResultViewModel2 : ViewModel() { private suspend fun CoroutineScope.loadLinks( result: ResultEpisode, isVisible: Boolean, - isCasting: Boolean, + type: LoadType, clearCache: Boolean = false, ): LinkLoadingResult { val tempGenerator = RepoLinkGenerator(listOf(result)) @@ -1002,7 +1002,7 @@ class ResultViewModel2 : ViewModel() { } try { updatePage() - tempGenerator.generateLinks(clearCache, isCasting, { (link, _) -> + tempGenerator.generateLinks(clearCache, type, { (link, _) -> if (link != null) { links += link updatePage() @@ -1272,7 +1272,6 @@ class ResultViewModel2 : ViewModel() { acquireSingleSubtitle( click.data, - false, txt(R.string.episode_action_download_subtitle) ) { (links, index) -> downloadSubtitle( @@ -1317,7 +1316,7 @@ class ResultViewModel2 : ViewModel() { val response = currentResponse ?: return acquireSingleLink( click.data, - false, + LoadType.InAppDownload, txt(R.string.episode_action_download_mirror) ) { (result, index) -> ioSafe { @@ -1347,7 +1346,7 @@ class ResultViewModel2 : ViewModel() { loadLinks( click.data, isVisible = false, - isCasting = false, + type = LoadType.InApp, clearCache = true ) } @@ -1356,7 +1355,7 @@ class ResultViewModel2 : ViewModel() { ACTION_CHROME_CAST_MIRROR -> { acquireSingleLink( click.data, - isCasting = true, + LoadType.Chromecast, txt(R.string.episode_action_chromecast_mirror) ) { (result, index) -> startChromecast(activity, click.data, result.links, result.subs, index) @@ -1365,7 +1364,7 @@ class ResultViewModel2 : ViewModel() { ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( click.data, - isCasting = true, + LoadType.Browser, txt(R.string.episode_action_play_in_browser) ) { (result, index) -> try { @@ -1380,7 +1379,7 @@ class ResultViewModel2 : ViewModel() { ACTION_COPY_LINK -> { acquireSingleLink( click.data, - isCasting = true, + LoadType.ExternalApp, txt(R.string.episode_action_copy_link) ) { (result, index) -> val act = activity ?: return@acquireSingleLink @@ -1399,7 +1398,7 @@ class ResultViewModel2 : ViewModel() { } ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> { - loadLinks(click.data, isVisible = true, isCasting = true) { links -> + loadLinks(click.data, isVisible = true, LoadType.ExternalApp) { links -> if (links.links.isEmpty()) { showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT) return@loadLinks @@ -1415,7 +1414,7 @@ class ResultViewModel2 : ViewModel() { ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink( click.data, - isCasting = true, + LoadType.Chromecast, txt( R.string.episode_action_play_in_format, txt(R.string.player_settings_play_in_web) @@ -1432,7 +1431,7 @@ class ResultViewModel2 : ViewModel() { ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink( click.data, - isCasting = true, + LoadType.Chromecast, txt( R.string.episode_action_play_in_format, txt(R.string.player_settings_play_in_mpv) @@ -1461,7 +1460,6 @@ class ResultViewModel2 : ViewModel() { if (index >= 0) it.goto(index) } - } ?: return, list ) ) @@ -2173,7 +2171,7 @@ class ResultViewModel2 : ViewModel() { trailerData.extractorUrl, trailerData.referer ?: "", Qualities.Unknown.value, - trailerData.extractorUrl.contains(".m3u8") + type = INFER_TYPE ) ) to arrayListOf() } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt index 6b5e9ec2..d8373165 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt @@ -55,7 +55,11 @@ object CastHelper { val builder = MediaInfo.Builder(link.url) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setContentType(if (link.isM3u8) MimeTypes.APPLICATION_M3U8 else MimeTypes.VIDEO_MP4) + .setContentType(when(link.type) { + ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8 + ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD + else -> MimeTypes.VIDEO_MP4 + }) .setMetadata(movieMetadata) .setMediaTracks(tracks) data?.let { 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 ffda32d7..2a539f0d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -4,8 +4,10 @@ import android.net.Uri import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.extractors.* +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay import org.jsoup.Jsoup +import java.net.URL import kotlin.collections.MutableList /** @@ -35,35 +37,101 @@ data class ExtractorLinkPlayList( val playlist: List, override val referer: String, override val quality: Int, - override val isM3u8: Boolean = false, + val isM3u8: Boolean = false, override val headers: Map = mapOf(), /** Used for getExtractorVerifierJob() */ override val extractorData: String? = null, + override val type: ExtractorLinkType, ) : ExtractorLink( - source, - name, - // Blank as un-used - "", - referer, - quality, - isM3u8, - headers, - extractorData -) + source = source, + name = name, + url = "", + referer = referer, + quality = quality, + headers = headers, + extractorData = extractorData, + type = type +) { + constructor( + source: String, + name: String, + playlist: List, + referer: String, + quality: Int, + isM3u8: Boolean = false, + headers: Map = mapOf(), + extractorData: String? = null, + ) : this( + source = source, + name = name, + playlist = playlist, + referer = referer, + quality = quality, + type = if (isM3u8) ExtractorLinkType.M3U8 else ExtractorLinkType.VIDEO, + headers = headers, + extractorData = extractorData, + ) +} +/** Metadata about the file type used for downloads and exoplayer hint, + * if you respond with the wrong one the file will fail to download or be played */ +enum class ExtractorLinkType { + /** Single stream of bytes no matter the actual file type */ + VIDEO, + /** Split into several .ts files, has support for encrypted m3u8s */ + M3U8, + /** Like m3u8 but uses xml, currently no download support */ + DASH, + /** No support at the moment */ + TORRENT, + /** No support at the moment */ + MAGNET, +} +private fun inferTypeFromUrl(url: String): ExtractorLinkType { + val path = normalSafeApiCall { URL(url).path } + return when { + path?.endsWith(".m3u8") == true -> ExtractorLinkType.M3U8 + path?.endsWith(".mpd") == true -> ExtractorLinkType.DASH + path?.endsWith(".torrent") == true -> ExtractorLinkType.TORRENT + url.startsWith("magnet:") -> ExtractorLinkType.MAGNET + else -> ExtractorLinkType.VIDEO + } +} +val INFER_TYPE : ExtractorLinkType? = null open class ExtractorLink constructor( open val source: String, open val name: String, override val url: String, override val referer: String, open val quality: Int, - open val isM3u8: Boolean = false, override val headers: Map = mapOf(), /** Used for getExtractorVerifierJob() */ open val extractorData: String? = null, - open val isDash: Boolean = false, + open val type: ExtractorLinkType, ) : VideoDownloadManager.IDownloadableMinimum { + constructor( + source: String, + name: String, + url: String, + referer: String, + quality: Int, + /** the type of the media, use INFER_TYPE if you want to auto infer the type from the url */ + type: ExtractorLinkType?, + headers: Map = mapOf(), + /** Used for getExtractorVerifierJob() */ + extractorData: String? = null, + ) : this( + source = source, + name = name, + url = url, + referer = referer, + quality = quality, + headers = headers, + extractorData = extractorData, + type = type ?: inferTypeFromUrl(url) + ) + /** * Old constructor without isDash, allows for backwards compatibility with extensions. * Should be removed after all extensions have updated their cloudstream.jar @@ -80,8 +148,30 @@ open class ExtractorLink constructor( extractorData: String? = null ) : this(source, name, url, referer, quality, isM3u8, headers, extractorData, false) + constructor( + source: String, + name: String, + url: String, + referer: String, + quality: Int, + isM3u8: Boolean = false, + headers: Map = mapOf(), + /** Used for getExtractorVerifierJob() */ + extractorData: String? = null, + isDash: Boolean, + ) : this( + source = source, + name = name, + url = url, + referer = referer, + quality = quality, + headers = headers, + extractorData = extractorData, + type = if (isDash) ExtractorLinkType.DASH else if (isM3u8) ExtractorLinkType.M3U8 else ExtractorLinkType.VIDEO + ) + override fun toString(): String { - return "ExtractorLink(name=$name, url=$url, referer=$referer, isM3u8=$isM3u8)" + return "ExtractorLink(name=$name, url=$url, referer=$referer, type=$type)" } } @@ -135,6 +225,7 @@ enum class Qualities(var value: Int, val defaultPriority: Int) { else -> "${qual}p" } } + fun getStringByIntFull(quality: Int): String { return when (quality) { 0 -> "Auto" diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 72eb002a..d108daed 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -53,7 +53,7 @@ import java.io.Closeable import java.io.File import java.io.IOException import java.io.OutputStream -import java.net.URL +import java.lang.IllegalArgumentException import java.util.* const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general" @@ -951,7 +951,10 @@ object VideoDownloadManager { /** how many bytes every connection should be, by default it is 10 MiB */ chuckSize: Long = (1 shl 20) * 10, /** maximum bytes in the buffer that responds */ - bufferSize: Int = DEFAULT_BUFFER_SIZE + bufferSize: Int = DEFAULT_BUFFER_SIZE, + /** how many bytes bytes it should require to use the parallel downloader instead, + * if we download a very small file we don't want it parallel */ + maximumSmallSize : Long = chuckSize * 2 ): LazyStreamDownloadData { // we don't want to make a separate connection for every 1kb require(chuckSize > 1000) @@ -963,7 +966,7 @@ object VideoDownloadManager { var downloadLength: Long? = null var totalLength: Long? = null - val ranges = if (contentLength == null) { + val ranges = if (contentLength == null || contentLength < maximumSmallSize) { // is the equivalent of [startByte..EOF] as we don't know the size we can only do one // connection LongArray(1) { startByte } @@ -1024,6 +1027,7 @@ object VideoDownloadManager { } } + /** download a file that consist of a single stream of data*/ suspend fun downloadThing( context: Context, link: IDownloadableMinimum, @@ -1035,8 +1039,7 @@ object VideoDownloadManager { createNotificationCallback: (CreateNotificationMetadata) -> Unit, parallelConnections: Int = 3 ): DownloadStatus = withContext(Dispatchers.IO) { - // we cant download torrents with this implementation, aria2c might be used in the future - if (link.url.startsWith("magnet") || link.url.endsWith(".torrent") || parallelConnections < 1) { + if (parallelConnections < 1) { return@withContext DOWNLOAD_INVALID_INPUT } @@ -1529,6 +1532,11 @@ object VideoDownloadManager { notificationCallback: (Int, Notification) -> Unit, tryResume: Boolean = false, ): DownloadStatus { + // no support for these file formats + if(link.type == ExtractorLinkType.MAGNET || link.type == ExtractorLinkType.TORRENT || link.type == ExtractorLinkType.DASH) { + return DOWNLOAD_INVALID_INPUT + } + val name = getFileName(context, ep) // Make sure this is cancelled when download is done or cancelled. @@ -1557,35 +1565,39 @@ object VideoDownloadManager { } try { - if (link.isM3u8 || normalSafeApiCall { URL(link.url).path.endsWith(".m3u8") } == true) { - val startIndex = if (tryResume) { - context.getKey( - KEY_DOWNLOAD_INFO, - ep.id.toString(), - null - )?.extraInfo?.toIntOrNull() - } else null + when(link.type) { + ExtractorLinkType.M3U8 -> { + val startIndex = if (tryResume) { + context.getKey( + KEY_DOWNLOAD_INFO, + ep.id.toString(), + null + )?.extraInfo?.toIntOrNull() + } else null - return downloadHLS( - context, - link, - name, - folder ?: "", - ep.id, - startIndex, - callback, parallelConnections = maxConcurrentConnections - ) - } else { - return downloadThing( - context, - link, - name, - folder ?: "", - "mp4", - tryResume, - ep.id, - callback, parallelConnections = maxConcurrentConnections - ) + return downloadHLS( + context, + link, + name, + folder ?: "", + ep.id, + startIndex, + callback, parallelConnections = maxConcurrentConnections + ) + } + ExtractorLinkType.VIDEO -> { + return downloadThing( + context, + link, + name, + folder ?: "", + "mp4", + tryResume, + ep.id, + callback, parallelConnections = maxConcurrentConnections + ) + } + else -> throw IllegalArgumentException("unsuported download type") } } catch (t: Throwable) { return DOWNLOAD_FAILED From 0839775172db1f55b4703d189e1aa7e8f3aed3f3 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Mon, 4 Sep 2023 20:36:36 +0000 Subject: [PATCH 211/570] Upgrade SDK (#590) * Update sdk version * Let's not be too radical --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 55d0f7ae..e31de078 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,12 +51,12 @@ android { } compileSdk = 33 - buildToolsVersion = "30.0.3" + buildToolsVersion = "34.0.0" defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 29 + targetSdk = 33 versionCode = 59 versionName = "4.1.8" From 3fe247fb193ada9bbb7ff391d38e02a24630c1e0 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 6 Sep 2023 20:53:43 +0200 Subject: [PATCH 212/570] added drm player support --- .../cloudstream3/ui/player/CS3IPlayer.kt | 87 ++++++++++++++++--- .../cloudstream3/utils/ExtractorApi.kt | 55 ++++++++++++ 2 files changed, 132 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index fd1da5ca..c779943b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.player +import android.annotation.SuppressLint import android.content.Context import android.net.Uri import android.os.Handler @@ -7,6 +8,7 @@ import android.os.Looper import android.util.Log import android.util.Rational import android.widget.FrameLayout +import androidx.media3.common.C import androidx.media3.common.C.* import androidx.media3.common.Format import androidx.media3.common.MediaItem @@ -31,6 +33,10 @@ import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.SeekParameters +import androidx.media3.exoplayer.dash.DashMediaSource +import androidx.media3.exoplayer.drm.DefaultDrmSessionManager +import androidx.media3.exoplayer.drm.FrameworkMediaDrm +import androidx.media3.exoplayer.drm.LocalMediaDrmCallback import androidx.media3.exoplayer.source.ClippingMediaSource import androidx.media3.exoplayer.source.ConcatenatingMediaSource import androidx.media3.exoplayer.source.DefaultMediaSourceFactory @@ -50,6 +56,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.DrmExtractorLink import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList @@ -58,6 +65,7 @@ import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File import java.lang.IllegalArgumentException +import java.util.UUID import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession @@ -104,7 +112,16 @@ class CS3IPlayer : IPlayer { * */ data class MediaItemSlice( val mediaItem: MediaItem, - val durationUs: Long + val durationUs: Long, + val drm: DrmMetadata? = null + ) + + data class DrmMetadata( + val kid: String, + val key: String, + val uuid: UUID, + val kty: String, + val keyRequestParameters: HashMap, ) override fun getDuration(): Long? = exoPlayer?.duration @@ -340,6 +357,7 @@ class CS3IPlayer : IPlayer { }.flatten() } + @SuppressLint("UnsafeOptInUsageError") private fun Tracks.Group.getFormats(): List> { return (0 until this.mediaTrackGroup.length).mapNotNull { i -> if (this.isSupported) @@ -368,6 +386,7 @@ class CS3IPlayer : IPlayer { ) } + @SuppressLint("UnsafeOptInUsageError") override fun getVideoTracks(): CurrentTracks { val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList() val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO } @@ -387,6 +406,7 @@ class CS3IPlayer : IPlayer { /** * @return True if the player should be reloaded * */ + @SuppressLint("UnsafeOptInUsageError") override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean { Log.i(TAG, "setPreferredSubtitles init $subtitle") currentSubtitles = subtitle @@ -465,6 +485,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") override fun getAspectRatio(): Rational? { return exoPlayer?.videoFormat?.let { format -> Rational(format.width, format.height) @@ -475,6 +496,7 @@ class CS3IPlayer : IPlayer { subtitleHelper.setSubStyle(style) } + @SuppressLint("UnsafeOptInUsageError") override fun saveData() { Log.i(TAG, "saveData") updatedTime() @@ -548,6 +570,7 @@ class CS3IPlayer : IPlayer { var requestSubtitleUpdate: (() -> Unit)? = null + @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(headers: Map): HttpDataSource.Factory { val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT) return source.apply { @@ -555,6 +578,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory { val provider = getApiFromNameNull(link.source) val interceptor = provider?.getVideoInterceptor(link) @@ -632,6 +656,7 @@ class CS3IPlayer : IPlayer { return Pair(subSources, activeSubtitles) }*/ + @SuppressLint("UnsafeOptInUsageError") private fun getCache(context: Context, cacheSize: Long): SimpleCache? { return try { val databaseProvider = StandaloneDatabaseProvider(context) @@ -663,6 +688,7 @@ class CS3IPlayer : IPlayer { return getMediaItemBuilder(mimeType).setUri(url).build() } + @SuppressLint("UnsafeOptInUsageError") private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector { val trackSelector = DefaultTrackSelector(context) trackSelector.parameters = trackSelector.buildUponParameters() @@ -676,6 +702,7 @@ class CS3IPlayer : IPlayer { var currentTextRenderer: CustomTextRenderer? = null + @SuppressLint("UnsafeOptInUsageError") private fun buildExoPlayer( context: Context, mediaItemSlices: List, @@ -760,15 +787,33 @@ class CS3IPlayer : IPlayer { // If there is only one item then treat it as normal, if multiple: concatenate the items. val videoMediaSource = if (mediaItemSlices.size == 1) { - factory.createMediaSource(mediaItemSlices.first().mediaItem) + val item = mediaItemSlices.first() + + item.drm?.let { drm -> + val drmCallback = + LocalMediaDrmCallback("{\"keys\":[{\"kty\":\"${drm.kty}\",\"k\":\"${drm.key}\",\"kid\":\"${drm.kid}\"}],\"type\":\"temporary\"}".toByteArray()) + val manager = DefaultDrmSessionManager.Builder() + .setPlayClearSamplesWithoutKeys(true) + .setMultiSession(false) + .setKeyRequestParameters(drm.keyRequestParameters) + .setUuidAndExoMediaDrmProvider(drm.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER) + .build(drmCallback) + val manifestDataSourceFactory = DefaultHttpDataSource.Factory() + + DashMediaSource.Factory(manifestDataSourceFactory) + .setDrmSessionManagerProvider { manager } + .createMediaSource(item.mediaItem) + } ?: run { + factory.createMediaSource(item.mediaItem) + } } else { val source = ConcatenatingMediaSource() - mediaItemSlices.map { + mediaItemSlices.map { item -> source.addMediaSource( // The duration MUST be known for it to work properly, see https://github.com/google/ExoPlayer/issues/4727 ClippingMediaSource( - factory.createMediaSource(it.mediaItem), - it.durationUs + factory.createMediaSource(item.mediaItem), + item.durationUs ) ) } @@ -1105,6 +1150,8 @@ class CS3IPlayer : IPlayer { } private var lastTimeStamps: List = emptyList() + + @SuppressLint("UnsafeOptInUsageError") override fun addTimeStamps(timeStamps: List) { lastTimeStamps = timeStamps timeStamps.forEach { timestamp -> @@ -1122,6 +1169,7 @@ class CS3IPlayer : IPlayer { updatedTime() } + @SuppressLint("UnsafeOptInUsageError") fun onRenderFirst() { if (hasUsedFirstRender) { // this insures that we only call this once per player load return @@ -1188,6 +1236,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") private fun getSubSources( onlineSourceFactory: HttpDataSource.Factory?, offlineSourceFactory: DataSource.Factory?, @@ -1243,6 +1292,7 @@ class CS3IPlayer : IPlayer { return exoPlayer != null } + @SuppressLint("UnsafeOptInUsageError") private fun loadOnlinePlayer(context: Context, link: ExtractorLink) { Log.i(TAG, "loadOnlinePlayer $link") try { @@ -1259,7 +1309,7 @@ class CS3IPlayer : IPlayer { HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) } - val mime = when(link.type) { + val mime = when (link.type) { ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8 ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4 @@ -1267,12 +1317,29 @@ class CS3IPlayer : IPlayer { ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet support") } - val mediaItems = if (link is ExtractorLinkPlayList) { - link.playlist.map { + + val mediaItems = when (link) { + is ExtractorLinkPlayList -> link.playlist.map { MediaItemSlice(getMediaItem(mime, it.url), it.durationUs) } - } else { - listOf( + + is DrmExtractorLink -> { + listOf( + // Single sliced list with unset length + MediaItemSlice( + getMediaItem(mime, link.url), Long.MIN_VALUE, + drm = DrmMetadata( + kid = link.kid, + key = link.key, + uuid = link.uuid ?: C.CLEARKEY_UUID, + kty = link.kty, + keyRequestParameters = link.keyRequestParameters + ) + ) + ) + } + + else -> listOf( // Single sliced list with unset length MediaItemSlice(getMediaItem(mime, link.url), Long.MIN_VALUE) ) 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 2a539f0d..85e88819 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -8,6 +8,7 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay import org.jsoup.Jsoup import java.net.URL +import java.util.UUID import kotlin.collections.MutableList /** @@ -99,6 +100,60 @@ private fun inferTypeFromUrl(url: String): ExtractorLinkType { } } val INFER_TYPE : ExtractorLinkType? = null + +open class DrmExtractorLink private constructor( + override val source: String, + override val name: String, + override val url: String, + override val referer: String, + override val quality: Int, + override val headers: Map = mapOf(), + /** Used for getExtractorVerifierJob() */ + override val extractorData: String? = null, + override val type: ExtractorLinkType, + open val kid : String, + open val key : String, + /** if null then it uses the UUID for the ClearKey DRM scheme */ + open val uuid : UUID?, + open val kty : String, + + open val keyRequestParameters : HashMap +) : ExtractorLink( + source, name, url, referer, quality, type, headers, extractorData +) { + constructor( + source: String, + name: String, + url: String, + referer: String, + quality: Int, + /** the type of the media, use INFER_TYPE if you want to auto infer the type from the url */ + type: ExtractorLinkType?, + headers: Map = mapOf(), + /** Used for getExtractorVerifierJob() */ + extractorData: String? = null, + kid : String, + key : String, + uuid : UUID? = null, + kty : String = "oct", + keyRequestParameters : HashMap = hashMapOf(), + ) : this( + source = source, + name = name, + url = url, + referer = referer, + quality = quality, + headers = headers, + extractorData = extractorData, + type = type ?: inferTypeFromUrl(url), + kid = kid, + key = key, + uuid = uuid, + keyRequestParameters = keyRequestParameters, + kty = kty, + ) +} + open class ExtractorLink constructor( open val source: String, open val name: String, From 49731cd6995dc2b784fef17554faeb90a1b895c0 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 6 Sep 2023 22:42:22 +0200 Subject: [PATCH 213/570] changed drm API a bit --- .../cloudstream3/ui/player/CS3IPlayer.kt | 2 +- .../cloudstream3/utils/ExtractorApi.kt | 225 +++++++++++++++++- 2 files changed, 220 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index c779943b..4a88a2e7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -1331,7 +1331,7 @@ class CS3IPlayer : IPlayer { drm = DrmMetadata( kid = link.kid, key = link.key, - uuid = link.uuid ?: C.CLEARKEY_UUID, + uuid = link.uuid, kty = link.kty, keyRequestParameters = link.keyRequestParameters ) 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 85e88819..0a926374 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -1,15 +1,204 @@ package com.lagradost.cloudstream3.utils import android.net.Uri -import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.USER_AGENT +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.extractors.AStreamHub +import com.lagradost.cloudstream3.extractors.Acefile +import com.lagradost.cloudstream3.extractors.Ahvsh +import com.lagradost.cloudstream3.extractors.Aico +import com.lagradost.cloudstream3.extractors.AsianLoad +import com.lagradost.cloudstream3.extractors.Bestx +import com.lagradost.cloudstream3.extractors.Blogger +import com.lagradost.cloudstream3.extractors.BullStream +import com.lagradost.cloudstream3.extractors.ByteShare +import com.lagradost.cloudstream3.extractors.Cda +import com.lagradost.cloudstream3.extractors.Cdnplayer +import com.lagradost.cloudstream3.extractors.Chillx +import com.lagradost.cloudstream3.extractors.CineGrabber +import com.lagradost.cloudstream3.extractors.Cinestart +import com.lagradost.cloudstream3.extractors.DBfilm +import com.lagradost.cloudstream3.extractors.Dailymotion +import com.lagradost.cloudstream3.extractors.DatabaseGdrive +import com.lagradost.cloudstream3.extractors.DatabaseGdrive2 +import com.lagradost.cloudstream3.extractors.DesuArcg +import com.lagradost.cloudstream3.extractors.DesuDrive +import com.lagradost.cloudstream3.extractors.DesuOdchan +import com.lagradost.cloudstream3.extractors.DesuOdvip +import com.lagradost.cloudstream3.extractors.Dokicloud +import com.lagradost.cloudstream3.extractors.DoodCxExtractor +import com.lagradost.cloudstream3.extractors.DoodLaExtractor +import com.lagradost.cloudstream3.extractors.DoodPmExtractor +import com.lagradost.cloudstream3.extractors.DoodShExtractor +import com.lagradost.cloudstream3.extractors.DoodSoExtractor +import com.lagradost.cloudstream3.extractors.DoodToExtractor +import com.lagradost.cloudstream3.extractors.DoodWatchExtractor +import com.lagradost.cloudstream3.extractors.DoodWfExtractor +import com.lagradost.cloudstream3.extractors.DoodWsExtractor +import com.lagradost.cloudstream3.extractors.DoodYtExtractor +import com.lagradost.cloudstream3.extractors.Dooood +import com.lagradost.cloudstream3.extractors.Embedgram +import com.lagradost.cloudstream3.extractors.Evoload +import com.lagradost.cloudstream3.extractors.Evoload1 +import com.lagradost.cloudstream3.extractors.FEmbed +import com.lagradost.cloudstream3.extractors.FEnet +import com.lagradost.cloudstream3.extractors.Fastream +import com.lagradost.cloudstream3.extractors.FeHD +import com.lagradost.cloudstream3.extractors.Fembed9hd +import com.lagradost.cloudstream3.extractors.FileMoon +import com.lagradost.cloudstream3.extractors.FileMoonIn +import com.lagradost.cloudstream3.extractors.FileMoonSx +import com.lagradost.cloudstream3.extractors.Filesim +import com.lagradost.cloudstream3.extractors.Fplayer +import com.lagradost.cloudstream3.extractors.GMPlayer +import com.lagradost.cloudstream3.extractors.Gdriveplayer +import com.lagradost.cloudstream3.extractors.Gdriveplayerapi +import com.lagradost.cloudstream3.extractors.Gdriveplayerapp +import com.lagradost.cloudstream3.extractors.Gdriveplayerbiz +import com.lagradost.cloudstream3.extractors.Gdriveplayerco +import com.lagradost.cloudstream3.extractors.Gdriveplayerfun +import com.lagradost.cloudstream3.extractors.Gdriveplayerio +import com.lagradost.cloudstream3.extractors.Gdriveplayerme +import com.lagradost.cloudstream3.extractors.Gdriveplayerorg +import com.lagradost.cloudstream3.extractors.Gdriveplayerus +import com.lagradost.cloudstream3.extractors.Gofile +import com.lagradost.cloudstream3.extractors.GuardareStream +import com.lagradost.cloudstream3.extractors.Guccihide +import com.lagradost.cloudstream3.extractors.Hxfile +import com.lagradost.cloudstream3.extractors.JWPlayer +import com.lagradost.cloudstream3.extractors.Jawcloud +import com.lagradost.cloudstream3.extractors.Jeniusplay +import com.lagradost.cloudstream3.extractors.Keephealth +import com.lagradost.cloudstream3.extractors.KotakAnimeid +import com.lagradost.cloudstream3.extractors.Kotakajair +import com.lagradost.cloudstream3.extractors.Krakenfiles +import com.lagradost.cloudstream3.extractors.LayarKaca +import com.lagradost.cloudstream3.extractors.Linkbox +import com.lagradost.cloudstream3.extractors.Luxubu +import com.lagradost.cloudstream3.extractors.Lvturbo +import com.lagradost.cloudstream3.extractors.Maxstream +import com.lagradost.cloudstream3.extractors.Mcloud +import com.lagradost.cloudstream3.extractors.Megacloud +import com.lagradost.cloudstream3.extractors.Meownime +import com.lagradost.cloudstream3.extractors.MixDrop +import com.lagradost.cloudstream3.extractors.MixDropBz +import com.lagradost.cloudstream3.extractors.MixDropCh +import com.lagradost.cloudstream3.extractors.MixDropTo +import com.lagradost.cloudstream3.extractors.Movhide +import com.lagradost.cloudstream3.extractors.Moviehab +import com.lagradost.cloudstream3.extractors.MoviehabNet +import com.lagradost.cloudstream3.extractors.Moviesapi +import com.lagradost.cloudstream3.extractors.Moviesm4u +import com.lagradost.cloudstream3.extractors.Mp4Upload +import com.lagradost.cloudstream3.extractors.Mvidoo +import com.lagradost.cloudstream3.extractors.MwvnVizcloudInfo +import com.lagradost.cloudstream3.extractors.Neonime7n +import com.lagradost.cloudstream3.extractors.Neonime8n +import com.lagradost.cloudstream3.extractors.OkRu +import com.lagradost.cloudstream3.extractors.OkRuHttps +import com.lagradost.cloudstream3.extractors.Okrulink +import com.lagradost.cloudstream3.extractors.Pixeldrain +import com.lagradost.cloudstream3.extractors.PlayLtXyz +import com.lagradost.cloudstream3.extractors.PlayerVoxzer +import com.lagradost.cloudstream3.extractors.Rabbitstream +import com.lagradost.cloudstream3.extractors.Rasacintaku +import com.lagradost.cloudstream3.extractors.SBfull +import com.lagradost.cloudstream3.extractors.Sbasian +import com.lagradost.cloudstream3.extractors.Sbface +import com.lagradost.cloudstream3.extractors.Sbflix +import com.lagradost.cloudstream3.extractors.Sblona +import com.lagradost.cloudstream3.extractors.Sblongvu +import com.lagradost.cloudstream3.extractors.Sbnet +import com.lagradost.cloudstream3.extractors.Sbrapid +import com.lagradost.cloudstream3.extractors.Sbsonic +import com.lagradost.cloudstream3.extractors.Sbspeed +import com.lagradost.cloudstream3.extractors.Sbthe +import com.lagradost.cloudstream3.extractors.Sendvid +import com.lagradost.cloudstream3.extractors.ShaveTape +import com.lagradost.cloudstream3.extractors.Solidfiles +import com.lagradost.cloudstream3.extractors.SpeedoStream +import com.lagradost.cloudstream3.extractors.SpeedoStream1 +import com.lagradost.cloudstream3.extractors.SpeedoStream2 +import com.lagradost.cloudstream3.extractors.Ssbstream +import com.lagradost.cloudstream3.extractors.StreamM4u +import com.lagradost.cloudstream3.extractors.StreamSB +import com.lagradost.cloudstream3.extractors.StreamSB1 +import com.lagradost.cloudstream3.extractors.StreamSB10 +import com.lagradost.cloudstream3.extractors.StreamSB11 +import com.lagradost.cloudstream3.extractors.StreamSB2 +import com.lagradost.cloudstream3.extractors.StreamSB3 +import com.lagradost.cloudstream3.extractors.StreamSB4 +import com.lagradost.cloudstream3.extractors.StreamSB5 +import com.lagradost.cloudstream3.extractors.StreamSB6 +import com.lagradost.cloudstream3.extractors.StreamSB7 +import com.lagradost.cloudstream3.extractors.StreamSB8 +import com.lagradost.cloudstream3.extractors.StreamSB9 +import com.lagradost.cloudstream3.extractors.StreamTape +import com.lagradost.cloudstream3.extractors.StreamTapeNet +import com.lagradost.cloudstream3.extractors.StreamhideCom +import com.lagradost.cloudstream3.extractors.StreamhideTo +import com.lagradost.cloudstream3.extractors.Streamhub2 +import com.lagradost.cloudstream3.extractors.Streamlare +import com.lagradost.cloudstream3.extractors.StreamoUpload +import com.lagradost.cloudstream3.extractors.Streamplay +import com.lagradost.cloudstream3.extractors.Streamsss +import com.lagradost.cloudstream3.extractors.Supervideo +import com.lagradost.cloudstream3.extractors.Tantifilm +import com.lagradost.cloudstream3.extractors.Tomatomatela +import com.lagradost.cloudstream3.extractors.TomatomatelalClub +import com.lagradost.cloudstream3.extractors.Tubeless +import com.lagradost.cloudstream3.extractors.Upstream +import com.lagradost.cloudstream3.extractors.UpstreamExtractor +import com.lagradost.cloudstream3.extractors.Uqload +import com.lagradost.cloudstream3.extractors.Uqload1 +import com.lagradost.cloudstream3.extractors.Uqload2 +import com.lagradost.cloudstream3.extractors.Userload +import com.lagradost.cloudstream3.extractors.Userscloud +import com.lagradost.cloudstream3.extractors.Uservideo +import com.lagradost.cloudstream3.extractors.Vanfem +import com.lagradost.cloudstream3.extractors.Vicloud +import com.lagradost.cloudstream3.extractors.VidSrcExtractor +import com.lagradost.cloudstream3.extractors.VidSrcExtractor2 +import com.lagradost.cloudstream3.extractors.VideoVard +import com.lagradost.cloudstream3.extractors.VideovardSX +import com.lagradost.cloudstream3.extractors.Vidgomunime +import com.lagradost.cloudstream3.extractors.Vidgomunimesb +import com.lagradost.cloudstream3.extractors.Vidmoly +import com.lagradost.cloudstream3.extractors.Vidmolyme +import com.lagradost.cloudstream3.extractors.Vido +import com.lagradost.cloudstream3.extractors.Vidstreamz +import com.lagradost.cloudstream3.extractors.Vizcloud +import com.lagradost.cloudstream3.extractors.Vizcloud2 +import com.lagradost.cloudstream3.extractors.VizcloudCloud +import com.lagradost.cloudstream3.extractors.VizcloudDigital +import com.lagradost.cloudstream3.extractors.VizcloudInfo +import com.lagradost.cloudstream3.extractors.VizcloudLive +import com.lagradost.cloudstream3.extractors.VizcloudOnline +import com.lagradost.cloudstream3.extractors.VizcloudSite +import com.lagradost.cloudstream3.extractors.VizcloudXyz +import com.lagradost.cloudstream3.extractors.Voe +import com.lagradost.cloudstream3.extractors.Watchx +import com.lagradost.cloudstream3.extractors.WcoStream +import com.lagradost.cloudstream3.extractors.Wibufile +import com.lagradost.cloudstream3.extractors.XStreamCdn +import com.lagradost.cloudstream3.extractors.YourUpload +import com.lagradost.cloudstream3.extractors.YoutubeExtractor +import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor +import com.lagradost.cloudstream3.extractors.YoutubeNoCookieExtractor +import com.lagradost.cloudstream3.extractors.YoutubeShortLinkExtractor +import com.lagradost.cloudstream3.extractors.Yufiles +import com.lagradost.cloudstream3.extractors.Zorofile +import com.lagradost.cloudstream3.extractors.Zplayer +import com.lagradost.cloudstream3.extractors.ZplayerV2 +import com.lagradost.cloudstream3.extractors.Ztreamhub import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.extractors.* import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay import org.jsoup.Jsoup import java.net.URL import java.util.UUID -import kotlin.collections.MutableList /** * For use in the ConcatenatingMediaSource. @@ -101,6 +290,31 @@ private fun inferTypeFromUrl(url: String): ExtractorLinkType { } val INFER_TYPE : ExtractorLinkType? = null +/** + * UUID for the ClearKey DRM scheme. + * + * + * ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up. + */ +val CLEARKEY_UUID = UUID(-0x1d8e62a7567a4c37L, 0x781AB030AF78D30EL) + +/** + * UUID for the Widevine DRM scheme. + * + * + * Widevine is supported on Android devices running Android 4.3 (API Level 18) and up. + */ +val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L) + +/** + * UUID for the PlayReady DRM scheme. + * + * + * PlayReady is supported on all AndroidTV devices. Note that most other Android devices do not + * provide PlayReady support. + */ +val PLAYREADY_UUID = UUID(-0x65fb0f8667bfbd7aL, -0x546d19a41f77a06bL) + open class DrmExtractorLink private constructor( override val source: String, override val name: String, @@ -113,8 +327,7 @@ open class DrmExtractorLink private constructor( override val type: ExtractorLinkType, open val kid : String, open val key : String, - /** if null then it uses the UUID for the ClearKey DRM scheme */ - open val uuid : UUID?, + open val uuid : UUID, open val kty : String, open val keyRequestParameters : HashMap @@ -134,7 +347,7 @@ open class DrmExtractorLink private constructor( extractorData: String? = null, kid : String, key : String, - uuid : UUID? = null, + uuid : UUID = CLEARKEY_UUID, kty : String = "oct", keyRequestParameters : HashMap = hashMapOf(), ) : this( From 4ddd78ebb6892742543b082a6395553d9642dc8e Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:30:00 +0530 Subject: [PATCH 214/570] fook jitpack (#595) --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e31de078..f52d6e5e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -250,9 +250,9 @@ dependencies { // used for subtitle decoding https://github.com/albfernandez/juniversalchardet implementation("com.github.albfernandez:juniversalchardet:2.4.0") - // newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204 + // newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev // this should be updated frequently to avoid trailer fu*kery - implementation("com.github.TeamNewPipe:NewPipeExtractor:1f08d28") + implementation("com.github.teamnewpipe:NewPipeExtractor:1f08d28") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6") // Library/extensions searching with Levenshtein distance From f05c65cf5c62964c73b9756c4f5c2dedb7cfb919 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Fri, 8 Sep 2023 10:01:11 +0200 Subject: [PATCH 215/570] Translated using Weblate (Odia) (#574) Currently translated at 39.5% (249 of 630 strings) Translated using Weblate (Odia) Currently translated at 25.0% (1 of 4 strings) Translated using Weblate (Odia) Currently translated at 38.8% (245 of 630 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Tamil) Currently translated at 20.9% (132 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Ukrainian) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (630 of 630 strings) Translated using Weblate (Croatian) Currently translated at 99.8% (629 of 630 strings) Translated using Weblate (Croatian) Currently translated at 99.3% (626 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Arabic (Najdi)) Currently translated at 54.9% (346 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Ukrainian) Currently translated at 100.0% (630 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (French) Currently translated at 100.0% (630 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Hungarian) Currently translated at 85.7% (540 of 630 strings) Translated using Weblate (Romanian) Currently translated at 95.7% (603 of 630 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 50.1% (316 of 630 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (630 of 630 strings) Translated using Weblate (Portuguese (Brazil)) Currently translated at 96.1% (606 of 630 strings) Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Translated using Weblate (Arabic (Najdi)) Currently translated at 44.9% (283 of 630 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 39.6% (250 of 630 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 39.5% (249 of 630 strings) Translated using Weblate (German) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (German) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 33.4% (211 of 630 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (630 of 630 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 strings) Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Update translation files Updated by "Remove blank strings" hook in Weblate. Translated using Weblate (Arabic (Najdi)) Currently translated at 32.0% (202 of 630 strings) Translated using Weblate (Portuguese (Brazil)) Currently translated at 91.5% (577 of 630 strings) Translated using Weblate (Arabic (Saudi Arabia)) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 23.9% (151 of 630 strings) Added translation using Weblate (Arabic (South Levantine)) Translated using Weblate (Amharic) Currently translated at 14.9% (94 of 630 strings) Translated using Weblate (Tigrinya) Currently translated at 15.0% (95 of 630 strings) Added translation using Weblate (Amharic) Added translation using Weblate (Tigrinya) Merge remote-tracking branch 'origin/master' Translated using Weblate (Hungarian) Currently translated at 85.2% (537 of 630 strings) Translated using Weblate (Hungarian) Currently translated at 81.4% (513 of 630 strings) Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/am/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ars/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/or/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ro/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ti/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ar_SA/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/or/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/uk/ Translation: Cloudstream/App Translation: Cloudstream/Fastlane Co-authored-by: Alexandru Co-authored-by: Anarchydr Co-authored-by: Cait Martin Newnham <85128509+helloiamcait@users.noreply.github.com> Co-authored-by: Carlos Luiz Co-authored-by: Chi Uma Co-authored-by: GobinathAL Co-authored-by: Gyuri Bajzik Co-authored-by: Joel Brink Co-authored-by: Julian Co-authored-by: Mubarek Seyd Juhar Co-authored-by: Sam Cooper Co-authored-by: Skrripy Co-authored-by: Subham Jena Co-authored-by: mbottari Co-authored-by: pedrolinharesmoreira Co-authored-by: tabtomi8 --- app/src/main/res/values-ars/strings.xml | 67 +++++++++++++- app/src/main/res/values-bp/strings.xml | 43 ++++++--- app/src/main/res/values-de/strings.xml | 92 +++++++++---------- app/src/main/res/values-fr/strings.xml | 6 +- app/src/main/res/values-hr/strings.xml | 13 ++- app/src/main/res/values-hu/strings.xml | 7 +- app/src/main/res/values-or/strings.xml | 7 +- app/src/main/res/values-ro/strings.xml | 3 +- app/src/main/res/values-ta/strings.xml | 26 ++++-- app/src/main/res/values-uk/strings.xml | 38 ++++---- fastlane/metadata/android/or/changelogs/2.txt | 1 + fastlane/metadata/android/uk/changelogs/2.txt | 2 +- 12 files changed, 209 insertions(+), 96 deletions(-) create mode 100644 fastlane/metadata/android/or/changelogs/2.txt diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index ea8aa05c..5135c97e 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -200,4 +200,69 @@ استخدم %sتعذر إنشاء واجهة المستخدم بشكل صحيح، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور الصفات - + نوع الحافة + العب + حدث خطأ أثناء تحميل الروابط + التخزين الداخلي + الترجمة + استئناف تحميل + معلومات + وقفة التحميل + الغي + احفظ + إعدادات الترجمة + لون الخط + لون المخطط التفصيلي + اقفل + امسح + سرعة اللاعب + لون الخلفية + لون النافذة + ارتفاع الترجمة + حذف ملف + تعطيل الإبلاغ التلقائي عن الأخطاء + بدأ التحديث + انسخ + بث + ملف اللعب + مزيد من المعلومات + تصفية الإشارات المرجعية + إشارات مرجعية + زيل + ضبط حالة المشاهدة + مدبلجة + اخفي + قدم + وصف + يستمر التشغيل في مشغل مصغر فوق التطبيقات الأخرى + نهائيا %sسيؤدي هذا الى حذف +\nهل أنت متأكد؟ + الخط + حجم الخط + زيل + هذا المزود عبارة عن تورنت، ويوصى باستخدام فيبيان + لا يتم توفير البيانات الوصفية بواسطة الموقع، وسيفشل تحميل الفيديو إذا لم يكن موجودًا في الموقع. + جاري التنفيذ + مكتمل + حالة + التحديد التلقائي للغة + زر تغيير حجم المشغل + مواصلة المشاهدة + مزيد من المعلومات + البحث باستخدام مقدمي الخدمات + البحث باستخدام الأنواع + بنيني الى المطورين %d تم منح + لم يتم تقديم بنيني + تحميل اللغات + لغة الترجمة + اضغط لإعادة التعيين إلى الوضع الافتراضي + %s قم باستيراد الخطوط بوضعها في + قد تكون هناك حاجة إلى فيبيان حتى يعمل هذا المزود بشكل صحيح + لم يتم العثور على قطعة أرض + لم يتم العثور على وصف + 🐈عرض لوجكات + سجل + صور في صور + %d +\nباقي + \ 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 b70eec12..daa352a7 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -157,7 +157,7 @@ Mostrar episódios de Filler em anime Mostrar trailers Mostrar posters do Kitsu - Esconder qualidades de vídeo selecionadas nos resultados da Pesquisa + Esconder qualidades de vídeo selecionadas nos resultados da pesquisa Atualizações de plugin automáticas Mostrar atualizações do app Automaticamente procurar por novas atualizações ao abrir @@ -222,7 +222,7 @@ Filme Série Desenho Animado - @string/anime + Anime @string/ova Torrent Documentário @@ -265,14 +265,14 @@ Cache do vídeo em disco Limpar cache de vídeo e imagem Causará travamentos aleatórios se definido muito alto. Não mude caso 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 + Causa problemas em sistemas com pouco espaço de armazenamento se definido muito alto, como em dispositivos Android TV. DNS sobre HTTPS Útil para burlar bloqueios de provedores de internet Clonar site Remover site Adiciona um clone de um site existente, com uma URL diferente Caminho para Download - Url do servidor Nginx + URL do servidor NGINX Mostrar Anime Dublado/Legendado Ajustar para a Tela Esticar @@ -338,7 +338,7 @@ Sombreado Em Relevo Sincronizar legendas - 1000ms + 1000 ms Atraso de legenda Use isto se as legendas forem mostradas %dms adiantadas Use isto se as legendas forem mostradas %dms atrasadas @@ -382,9 +382,9 @@ Resolução e título Título Resolução - Id invalida + ID inválido Dado invalido - URL invalido + URL inválida Erro Remover legendas ocultas(CC) das legendas Remover bloat das legendas @@ -406,8 +406,8 @@ Plugin Carregado Plugin Apagado Falha ao carregar %s - Iniciada a transferência %d %s - Transferido %d %s com sucesso + Iniciada a transferência %d %s… + Transferido %d %s Tudo %s já transferido Transferência em batch Plugin @@ -444,7 +444,7 @@ Navegador Copia de Segurança A Barra de Progresso pode ser usada quando o player estiver oculto - Inscrever + Inscrito Essa lista está vazia. Tente mudar para outra. Reproduzir Livestream Log do Teste @@ -493,10 +493,10 @@ \nNOTA: Se a soma for 10 ou mais, o Player pulará automaticamente o carregamento quando o link for carregado! Arquivo de modo de segurança encontrado! \nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido. - Inscrevel em %d + Inscrito em %s Episódio %d Lançado Selecionar padrão - Disinscrevel em %d + Desinscrito de %s Alguns aparelhos não possuem suporte para este pacote de instalação. Tente a opção legada se a atualização não instalar. Dados móveis Perfil %d @@ -550,4 +550,21 @@ Faixas de áudio Adicionado em (novo para antigo) Faixas de video - + Legendas + Navegador + 18+ + Links + Funcionalidades do Player + Instalador APK + Aparência + Desativar + Usar + Link da stream + Gestos + Plugin baixado + Não foi possível se conectar ao GitHub. Ativando proxy jsDelivr… + Cache + Vídeo + Android TV + Wi-Fi + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6739465a..233e38e4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -5,19 +5,19 @@ Episode %d wird veröffentlicht in Vorschaubild Vorschaubild - Halten, um auf die Standardeinstellungen zurückzusetzen + Halten, um auf Standardeinstellungen zurückzusetzen Wiederherstellung der Daten aus der Datei %s fehlgeschlagen Daten erfolgreich gesichert Fehler beim Sichern von %s Dieser Anbieter hat keine Chromecast-Unterstützung Chromecast-Mirror In App wiedergeben - Vermischte Openings + Gemischte Openings Abspann Intro Verlauf löschen Verlauf - Überspringen Knopf für Openings/Endings anzeigen + Button zum Überspringen für Openings/Endings anzeigen Zu viel Text. Kann nicht in der Zwischenablage gespeichert werden. Episodenvorschaubild Medienvorschaubild @@ -34,7 +34,7 @@ CloudStream Mit CloudStream abspielen Startseite - Suchen + Suche Downloads Einstellungen Suchen… @@ -44,8 +44,8 @@ Nächste Episode Genres Teilen - In Browser öffnen - Puffern überspringen + Im Browser öffnen + Laden überspringen Lädt… Am schauen Pausiert @@ -79,7 +79,7 @@ Datei abspielen Download fortsetzen Download pausieren - Automatische Fehlerberichterstattung deaktivieren + Automatische Fehlerberichtserstattung deaktivieren Mehr Infos Verstecken Abspielen @@ -106,8 +106,8 @@ Schriftgröße Suche anhand Anbietern Suche anhand Typen - %d Benenes an die Devs verteilt - Noch keine Benenes verteilt + %d Benenes an die Devs geschenkt + Noch keine Benenes verschenkt Sprache automatisch wählen Sprachen herunterladen Untertitelsprache @@ -117,8 +117,8 @@ Mehr Infos @string/home_play Damit dieser Anbieter korrekt funktioniert, ist möglicherweise ein VPN erforderlich - Dieser Anbieter bietet Torrents an, ein VPN wird dringend empfohlen - Metadaten werden nicht von der Website bereitgestellt, das Laden des Videos schlägt fehl, wenn sie auf der Website nicht vorhanden sind. + Dieser Anbieter bietet Torrents an, ein VPN wird deswegen dringend empfohlen + Metadaten werden nicht von der Website bereitgestellt, das Laden des Videos schlägt fehl, wenn sie nicht auf der Website vorhanden sind. Beschreibung Keine Handlung gefunden Keine Beschreibung gefunden @@ -143,7 +143,7 @@ Doppeltippen zum Pausieren Zeit für vor- und zurückspulen im Player (Sekunden) Zweimal auf die rechte oder linke Seite tippen, um vor- oder zurückzuspulen - Doppelt in die Mitte tippen, um zu pausieren + Zweimal in die Mitte tippen, um zu pausieren Systemhelligkeit verwenden Systemhelligkeit anstelle eines dunklen Overlay im Player verwenden Episodenfortschritt aktualisieren @@ -163,7 +163,7 @@ Füller-Episoden für Animes anzeigen Trailer anzeigen Vorschaubilder von Kitsu anzeigen - Ausgewählte Videoqualität bei Suchergebnissen ausblenden + Ausgewählte Videoqualität in den Suchergebnissen ausblenden Automatische Plugin-Updates App-Updates anzeigen Automatisches Suchen nach neuen Updates nach dem Start. @@ -172,11 +172,11 @@ Github Light Novel App von denselben Entwicklern Anime App von denselben Entwicklern - Discord beitreten - Eine Benene an die Devs verteilen - Verteilte Benenes + Trete dem Discord Server bei + Eine Benene an die Devs schenken + Geschenkte Benenes App-Sprache - Keine Verlinkung gefunden + Keine Links gefunden Link in die Zwischenablage kopiert Episode abspielen Auf Standardwert zurücksetzen @@ -240,7 +240,7 @@ Remote-Fehler Renderfehler Unerwarteter Playerfehler - Downloadfehler, Speicherberechtigungen prüfen + Downloadfehler, bitte überprüfen sie die Speicherberechtigungen Chromecast-Episode In %s wiedergeben In Browser wiedergeben @@ -255,7 +255,7 @@ Titel UI-Elemente auf Vorschaubild umschalten Kein Update gefunden - Auf Update prüfen + Auf Updates prüfen Sperren Skalieren Quelle @@ -270,16 +270,16 @@ Videopufferlänge Video-Cache in Speicher Video- und Bild-Cache leeren - Verursacht Abstürze, wenn zu hoch eingestellt. Nicht ändern, wenn wenig Arbeitsspeicher verfügbar ist, wie z.B. ein Android TV oder ein altes Telefon. - Kann auf Systemen mit geringem Speicherplatz, wie z. B. Android TV-Geräten, zu Problemen führen, wenn der Wert zu hoch eingestellt ist. + Verursacht Abstürze, wenn zu hoch eingestellt. Nicht ändern, wenn wenig Arbeitsspeicher verfügbar ist, wie z.B. auf einem Android TV oder auf einem alten Smartphone. + Kann auf Systemen mit geringem Speicherplatz, wie z. B. auf Android TV-Geräten, zu Problemen führen, wenn der Wert zu hoch eingestellt ist. DNS über HTTPS - Nützlich für die Umgehung von ISP-Sperren + Nützlich zur Umgehung von ISP-Sperren Website klonen Website entfernen Einen Klon einer bestehenden Website mit einer anderen URL hinzufügen Downloadpfad Nginx-Server-URL - Dubbed/Subbed Anime anzeigen (Synchronisiert/Untertitelt) + Dubbed/Subbed Anime anzeigen An Bildschirm anpassen Strecken Vergrößern @@ -308,7 +308,7 @@ 127.0.0.1 MeineCooleSeite example.com - Sprachcode (en) + Sprachencode (en) %s %s Account Ausloggen @@ -317,13 +317,13 @@ Account hinzufügen Account erstellen Synchronisation hinzufügen - Hinzugefügt %s + %s hinzugefügt Sync Bewertung %d / 10 /\?\? /%d - Authentifiziert %s + %s authentifiziert Die Authentifizierung bei %s ist fehlgeschlagen Keine Normal @@ -335,10 +335,10 @@ Schatten Erhöht Untertitel synchronisieren - 1000ms + 1000 ms Untertitelverzögerung - Verwenden, wenn die Untertitel %dms zu früh angezeigt werden - Verwenden, wenn die Untertitel %dms zu spät angezeigt werden + Verwenden, wenn die Untertitel %d ms zu früh angezeigt werden + Verwenden, wenn die Untertitel %d ms zu spät angezeigt werden Keine Untertitelverzögerung Vogel Quax zwickt Johnys Pferd Bim Empfohlen @@ -359,7 +359,7 @@ HD TS TC - BlueRay + Blue-ray WP DVD 4K @@ -408,7 +408,7 @@ Plugins Dadurch werden auch alle Repository-Plugins gelöscht Repository löschen - Lade eine Liste der Websiten herunter, welche du verwenden möchtest + Lade eine Liste der Websites herunter, welche du verwenden möchtest Heruntergeladen: %d Deaktiviert: %d Nicht heruntergeladen: %d @@ -416,7 +416,7 @@ \n \nAufgrund eines hirnlosen DMCA-Takedowns durch Sky UK Limited 🤮 können wir die Repository-Site nicht in der App verlinken. \n -\nTrete unserem Discord bei oder suche online. +\nTrete unserem Discord Server bei oder suche online. Community-Repositories anzeigen Öffentliche Liste Alle Untertitel in Großbuchstaben @@ -427,7 +427,7 @@ Videospuren Bei Neustart anwenden Abgesicherter Modus aktiviert - Alle Erweiterungen wurden aufgrund eines Absturzes deaktiviert, damit Sie diejenige finden können, die Probleme verursacht. + Alle Erweiterungen wurden aufgrund eines Absturzes deaktiviert, damit Sie diejenige finden können, welche Probleme verursacht. Absturzinfo ansehen Bewertung: %s Beschreibung @@ -460,7 +460,7 @@ Automatische Installation aller noch nicht installierten Plugins aus hinzugefügten Repositories. Einrichtungsvorgang wiederholen APK-Installer - Einige Telefone unterstützen den neuen Package-Installer nicht. Benutze die Legacy-Option, wenn sich die Updates nicht installieren lassen. + Einige Smartphones unterstützen den neuen Package-Installer nicht. Benutze die Legacy-Option, wenn sich die Updates nicht installieren lassen. %s %d%s Links App-Updates @@ -482,7 +482,7 @@ Nein App-Update wird heruntergeladen… App-Update wird installiert… - Konnte die neue Version der App nicht installieren + Die neue Version der App konnte nicht installieren werden Legacy PackageInstaller Aktualisierung gestartet @@ -493,18 +493,18 @@ Browser Sortieren nach Sortieren - Bewertung (gut bis schlecht) - Bewertung (schlecht bis gut) - Aktualisiert (neu bis alt) - Aktualisiert (alt bis neu) - Alphabetisch (A bis Z) - Alphabetisch (Z bis A) + Bewertung (gut zu schlecht) + Bewertung (schlecht zu gut) + Aktualisiert (neu zu alt) + Aktualisiert (alt zu neu) + Alphabetisch (A zu Z) + Alphabetisch (Z zu A) Bibliothek auswählen Öffnen mit Deine Bibliothek ist leer :( \nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu. - Diese Liste ist leer. Versuche zu einer anderen Liste zu wechseln. - Datei für abgesicherten Modus gefunden! + Diese Liste ist leer. Versuch zu einer anderen Liste zu wechseln. + Datei für den abgesicherten Modus gefunden! \nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird. Player ausgeblendet - Betrag zum vor- und zurückspulen Der Betrag, welcher verwendet wird, wenn der Player eingeblendet ist @@ -549,7 +549,7 @@ Filtermodus für Plugin-Downloads auswählen Es wurde bereits abgestimmt Keine Plugins im Repository gefunden - Repository nicht gefunden, überprüfe die URL und probiere eine VPN - Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein schwerwiegender Fehler und sollte sofort gemeldet werden. %s + Repository nicht gefunden, überprüf die URL und versuch ein VPN + Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein SCHWERWIEGENDER FEHLER und sollte sofort gemeldet werden. %s Deaktivieren - + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 208e6140..2849b744 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -540,7 +540,7 @@ \n \nREMARQUE : Si la somme est de 10 ou plus, le joueur sautera automatiquement le chargement lorsque ce lien est chargé ! Aucun plugin trouvé dans ce dossier - Dossier non trouvé, vérifiez l\'url et essayé un VPN + Dépôt introuvable, vérifiez l\'URL et essayez avec un VPN Données mobiles Définir par défaut Utiliser @@ -552,4 +552,6 @@ Qualités L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s Sélectionnez le mode pour filtrer le téléchargement des plugins - + Fond de profil + \@string/default_subtitles + \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 35df36ac..a0bf44ca 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -566,4 +566,15 @@ Pozadina profila Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s Odaberi modus za filtriranje preuzimanja dodataka - + Onemogući + \@string/default_subtitles + U repozitoriju nisu pronađeni dodaci + Repozitorij nije pronađen, provjerite URL i pokušajte koristiti VPN + Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je video prioritet. +\n +\nIzvor A: 3 +\nKvaliteta B: 7 +\nImat će kombinirani prioritet videozapisa od 10. +\n +\nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! + \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 05a7f0a7..4c30caaf 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -219,7 +219,7 @@ Folyamatban levő Év Webhely - Szinopszis + Összegzés Nincsenek feliratok Távoli hiba Render hiba @@ -237,7 +237,7 @@ Csúsztassa felfelé vagy lefelé a bal vagy jobb oldalon a fényerő vagy a hangerő megváltoztatásához Biztonsági mentés 0 Banán a fejlesztőknek - Húzás a kereséshez + Húzd el, hogy beless Következő epizód automatikus lejátszása Következő epizód lejátszása amikor az aktuális epizód véget ér Dupla koppintás a kereséshez @@ -510,4 +510,5 @@ TV elrendezés Automatikus Az átugrás mértéke, amikor a lejátszó látható - + Válassza ki a módot a pluginek letöltésének szűréséhez + \ No newline at end of file diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 9b9385c2..e5044571 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -153,4 +153,9 @@ ଆପ୍ ଅଦ୍ୟତନ ଦେଖାଇବା ଅଦ୍ୟତନ ଆରମ୍ଭ ହୋଇଛି ସନ୍ଧାନ କରିବା… - + ସଂକ୍ଷିପ୍ତବୃତ୍ତି + ଚଳଚ୍ଚିତ୍ର ଚଲାଅ + %s ସନ୍ଧାନ କରିବା… + ପରବର୍ତ୍ତୀ ଅଧ୍ୟାୟ + କୌଣସି ତଥ୍ୟ ନାହିଁ + \ 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 1f288d2a..3db9accb 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -570,4 +570,5 @@ UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s Selectați modul de filtrare a descărcării plugin-urilor @string/default_subtitles - + Ați votat deja + \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index affb04bf..41cfa846 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -6,12 +6,12 @@ முகப்பு தேடு பதிவிறக்கம் - தகவல் எதுவும் இல்லை + தரவு இல்லை மேலும் விருப்பங்கள் - அடுத்த எபிசோட் + அடுத்த அத்தியாயம் வகைகள் பகிர் - Browser இல் திற + உலாவியில் திற ஏற்றுவதைத் தவிர் பார்த்து கொண்டிருப்பது நிறுத்தி வைக்கப்பட்டுள்ளது @@ -21,9 +21,9 @@ ஸ்ட்ரீம் டோரண்ட் வசன வரிகள் பின் செல் - எபிசோடை இயக்கு + அத்தியாயத்தை இயக்கு எபிசோட் பதிவிற்கான அனுமதி கொடுக்கவும் - பதிவிறக்கம் செய்யப்பட்டது + பதிவிறக்கப்பட்டது பதிவிறக்குகிறது பதிவிறக்கம் இடைநிறுத்தப்பட்டது பதிவிறக்கம் தொடங்கியது @@ -67,10 +67,10 @@ ஏற்றுகிறது… கைவிடப்பட்டது பதிவிறக்கம் முடிந்தது - இணைப்பை மீண்டும் முயற்சிக்கவும்… + இணைப்பை மீண்டும் முயலவும்… திரைப்படத்தை இயக்கு லைவ்ஸ்ட்ரீம் இயக்கு - டிரெய்லரை இயக்கவும் + டிரெய்லரை இயக்கு மூலம் இணைப்புகளை ஏற்றுவதில் பிழை இயக்கு @@ -107,4 +107,14 @@ இடைநிறுத்துவதற்கு இருமுறை தட்டவும் Chromecast வசன அமைப்புகள் இருண்ட மேலடுக்குக்குப் பதிலாக ஆப் பிளேயரில் சிஸ்டம் பிரகாசத்தைப் பயன்படுத்தவும் - + அத்தியாயம் %d-இன் வெளியீட்டு நேரம் + %dம %dநி + %dநி + அடுத்து ஏதாவது + உலாவி + %d நிமி + CloudStream-உடன் இயக்கு + புதிய புதுப்பிப்பு உள்ளது +\n%s->%s + நிரப்பி + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 4866ecd4..8e0dd88e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -18,7 +18,7 @@ Попередній перегляд фону Швидкість (%.2fx) Знайдено нове оновлення! -\n%s -> %s +\n%s –> %s Пошук Завантаження %d хв @@ -37,7 +37,7 @@ Покинуто Переглянути фільм Переглянути трейлер - Трансляція через торрент + Трансляція через торент Повторити підключення… Назад Переглянути епізод @@ -75,7 +75,7 @@ Продовжити перегляд Вилучити Детальніше - Цей постачальник є торрентом, рекомендується VPN + Цей постачальник є торентом, рекомендується використовувати VPN Опис Сюжет не знайдено Опис не знайдено @@ -86,9 +86,9 @@ Субтитри Chromecast Налаштування субтитрів Chromecast Режим Eigengravy - Проведіть пальцем, щоб змінити налаштування + Проведіть, щоб змінити налаштування Проведіть вгору або вниз з лівого або правого боку, щоб змінити яскравість чи гучність - Відтворювати наступний епізод після закінчення поточного + Відтворює наступний епізод після закінчення поточного Головна CloudStream Філер @@ -130,7 +130,7 @@ Картинка в картинці Налаштування субтитрів плеєра Додає опцію керування швидкістю в плеєрі - Проведіть пальцем, щоб перемотати + Проведіть, щоб перемотати Двічі торкніться, щоб перемотати Двічі торкніться для паузи Крок перемотки (секунди) @@ -224,7 +224,7 @@ Двічі торкніться праворуч або ліворуч, щоб перемотати відео вперед або назад Використовуйте системну яскравість у плеєрі замість темної накладки Завантажено файл резервної копії - Торренти + Торенти Автоматична синхронізація прогресу поточного епізоду Відсутні дозволи на зберігання. Будь ласка, спробуйте ще раз. Показувати постери від Kitsu @@ -256,7 +256,7 @@ NSFW Фільм OVA - Торрент + Торент Мітка якості NSFW Переглянути в браузері @@ -294,9 +294,9 @@ Основний колір Тема застосунку Розташування назви постера - Розмістіть назву під постером + Розмістити назву під постером Пароль123 - Моє круте ім\'я + Моє круте ім’я hello@world.com Мій крутий сайт Код мови (uk) @@ -348,9 +348,9 @@ Роздільна здатність відеоплеєра Довжина буфера відео Очистити кеш відео та зображень - Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об\'ємом пам\'яті, наприклад Android TV. + Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об’ємом пам’яті, наприклад Android TV. Корисно для обходу блокувань провайдера - Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об\'ємом вільної пам\'яті, наприклад Android TV. + Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об’ємом вільної пам’яті, наприклад Android TV. DNS через HTTPS Шлях завантаження Додайте клон існуючого сайту, з іншою URL-адресою @@ -374,10 +374,10 @@ Оцінений Завантажити з файлу Макс. - Щастям б\'єш жук їх глицю в фон й ґедзь пріч + Щастям б’єш жук їх глицю в фон й ґедзь пріч 1000 мс - Використовуйте цей параметр, якщо субтитри з\'являються на %d мс занадто рано - Використовуйте це, якщо субтитри з\'являються із запізненням на %d мс + Використовуйте цей параметр, якщо субтитри з’являються на %d мс занадто рано + Використовуйте це, якщо субтитри з’являються із запізненням на %d мс Завантажено %s Підтримка Фон @@ -507,8 +507,8 @@ Файл безпечного режиму знайдено! \nРозширеня не завантажуються під час запуску, доки файл не буде видалено. Android TV - Плеєр сховано - обсяг перемотки - Плеєр показано - обсяг перемотки + Плеєр сховано – обсяг перемотки + Плеєр показано – обсяг перемотки Обсяг перемотки, який використовується, коли плеєр видимий Обсяг перемотки, який використовується, коли плеєр прихований Тест провалено @@ -532,7 +532,7 @@ Встановити за замовчуванням Профілі Допомога - Тут ви можете змінити порядок джерел. Якщо відео має вищий пріоритет, воно з\'явиться вище у списку джерел. Сума пріоритету джерела та пріоритету якості є пріоритетом відео. + Тут ви можете змінити порядок джерел. Якщо відео має вищий пріоритет, воно з’явиться вище у списку джерел. Сума пріоритету джерела та пріоритету якості є пріоритетом відео. \n \nДжерело A: 3 \nЯкість B: 7 @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - + \ No newline at end of file diff --git a/fastlane/metadata/android/or/changelogs/2.txt b/fastlane/metadata/android/or/changelogs/2.txt new file mode 100644 index 00000000..e8b23e5f --- /dev/null +++ b/fastlane/metadata/android/or/changelogs/2.txt @@ -0,0 +1 @@ +- ପରିବର୍ତ୍ତନ ପୋଥି ଯୋଡ଼ାଗଲା! diff --git a/fastlane/metadata/android/uk/changelogs/2.txt b/fastlane/metadata/android/uk/changelogs/2.txt index 2c8d9f7e..97e84fa8 100644 --- a/fastlane/metadata/android/uk/changelogs/2.txt +++ b/fastlane/metadata/android/uk/changelogs/2.txt @@ -1 +1 @@ -- Додано журнал змін! +– Додано журнал змін! From 1629db2fc9c617d6bbe9617035213f3064a822ba Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Fri, 8 Sep 2023 08:01:34 +0000 Subject: [PATCH 216/570] chore(locales): fix locale issues --- app/src/main/res/values-ars/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 4 ++-- app/src/main/res/values-hr/strings.xml | 4 ++-- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-or/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 2 +- app/src/main/res/values-ta/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index 5135c97e..a1042b7e 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -265,4 +265,4 @@ صور في صور %d \nباقي - \ 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 daa352a7..016fbe43 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -567,4 +567,4 @@ Vídeo Android TV Wi-Fi - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 233e38e4..3efc4072 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -552,4 +552,4 @@ Repository nicht gefunden, überprüf die URL und versuch ein VPN Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein SCHWERWIEGENDER FEHLER und sollte sofort gemeldet werden. %s Deaktivieren - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2849b744..63d03a6b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -553,5 +553,5 @@ L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s Sélectionnez le mode pour filtrer le téléchargement des plugins Fond de profil - \@string/default_subtitles - \ No newline at end of file + @string/default_subtitles + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index a0bf44ca..477ab92b 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -567,7 +567,7 @@ Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s Odaberi modus za filtriranje preuzimanja dodataka Onemogući - \@string/default_subtitles + @string/default_subtitles U repozitoriju nisu pronađeni dodaci Repozitorij nije pronađen, provjerite URL i pokušajte koristiti VPN Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je video prioritet. @@ -577,4 +577,4 @@ \nImat će kombinirani prioritet videozapisa od 10. \n \nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! - \ No newline at end of file + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 4c30caaf..677beaf8 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -511,4 +511,4 @@ Automatikus Az átugrás mértéke, amikor a lejátszó látható Válassza ki a módot a pluginek letöltésének szűréséhez - \ No newline at end of file + diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index e5044571..177f7ea1 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -158,4 +158,4 @@ %s ସନ୍ଧାନ କରିବା… ପରବର୍ତ୍ତୀ ଅଧ୍ୟାୟ କୌଣସି ତଥ୍ୟ ନାହିଁ - \ 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 3db9accb..b6971c37 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -571,4 +571,4 @@ Selectați modul de filtrare a descărcării plugin-urilor @string/default_subtitles Ați votat deja - \ No newline at end of file + diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 41cfa846..3f4134e5 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -117,4 +117,4 @@ புதிய புதுப்பிப்பு உள்ளது \n%s->%s நிரப்பி - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8e0dd88e..f9dccfc4 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - \ No newline at end of file + From 130cc16e258a08c1a3a2ba75e5669d9ffee6d024 Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Fri, 8 Sep 2023 22:13:04 +0000 Subject: [PATCH 217/570] Simkl API optimizations (#581) * Fix episode removal in simkl * Simkl API optimizations --- .../syncproviders/providers/SimklApi.kt | 570 ++++++++++++------ 1 file changed, 377 insertions(+), 193 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index b4a9d789..cd1df562 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -5,7 +5,9 @@ import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.AcraApplication 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.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -13,6 +15,7 @@ import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mapper import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.logError @@ -33,6 +36,9 @@ import java.text.SimpleDateFormat import java.time.Instant import java.util.Date import java.util.TimeZone +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration class SimklApi(index: Int) : AccountManager(index), SyncAPI { override var name = "Simkl" @@ -59,6 +65,80 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { */ private var lastScoreTime = -1L + private object SimklCache { + private const val SIMKL_CACHE_KEY = "SIMKL_API_CACHE" + + enum class CacheTimes(val value: String) { + OneMonth("30d"), + ThirtyMinutes("30m") + } + + private class SimklCacheWrapper( + @JsonProperty("obj") val obj: T?, + @JsonProperty("validUntil") val validUntil: Long, + @JsonProperty("cacheTime") val cacheTime: Long = unixTime, + ) { + /** Returns true if cache is newer than cacheDays */ + fun isFresh(): Boolean { + return validUntil > unixTime + } + + fun remainingTime(): Duration { + val unixTime = unixTime + return if (validUntil > unixTime) { + (validUntil - unixTime).toDuration(DurationUnit.SECONDS) + } else { + Duration.ZERO + } + } + } + + fun cleanOldCache() { + getKeys(SIMKL_CACHE_KEY)?.forEach { + val isOld = AcraApplication.getKey>(it)?.isFresh() == false + if (isOld) { + removeKey(it) + } + } + } + + fun setKey(path: String, value: T, cacheTime: Duration) { + debugPrint { "Set cache: $SIMKL_CACHE_KEY/$path for ${cacheTime.inWholeDays} days or ${cacheTime.inWholeSeconds} seconds." } + setKey( + SIMKL_CACHE_KEY, + path, + // Storing as plain sting is required to make generics work. + SimklCacheWrapper(value, unixTime + cacheTime.inWholeSeconds).toJson() + ) + } + + /** + * Gets cached object, if object is not fresh returns null and removes it from cache + */ + inline fun getKey(path: String): T? { + // Required for generic otherwise "LinkedHashMap cannot be cast to MediaObject" + val type = mapper.typeFactory.constructParametricType( + SimklCacheWrapper::class.java, + T::class.java + ) + val cache = getKey(SIMKL_CACHE_KEY, path)?.let { + mapper.readValue>(it, type) + } + + return if (cache?.isFresh() == true) { + debugPrint { + "Cache hit at: $SIMKL_CACHE_KEY/$path. " + + "Remains fresh for ${cache.remainingTime().inWholeDays} days or ${cache.remainingTime().inWholeSeconds} seconds." + } + cache.obj + } else { + debugPrint { "Cache miss at: $SIMKL_CACHE_KEY/$path" } + removeKey(SIMKL_CACHE_KEY, path) + null + } + } + } + companion object { private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET @@ -210,18 +290,18 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("img") val img: String? ) { companion object { - fun convertToEpisodes(list: List?): List { + fun convertToEpisodes(list: List?): List? { return list?.map { MediaObject.Season.Episode(it.episode) - } ?: emptyList() + } } - fun convertToSeasons(list: List?): List { + fun convertToSeasons(list: List?): List? { return list?.filter { it.season != null }?.groupBy { it.season - }?.map { (season, episodes) -> - MediaObject.Season(season!!, convertToEpisodes(episodes)) - } ?: emptyList() + }?.mapNotNull { (season, episodes) -> + convertToEpisodes(episodes)?.let { MediaObject.Season(season!!, it) } + }?.ifEmpty { null } } } } @@ -235,11 +315,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("title") val title: String?, @JsonProperty("year") val year: Int?, @JsonProperty("ids") val ids: Ids?, + @JsonProperty("total_episodes") val total_episodes: Int? = null, + @JsonProperty("status") val status: String? = null, @JsonProperty("poster") val poster: String? = null, @JsonProperty("type") val type: String? = null, @JsonProperty("seasons") val seasons: List? = null, @JsonProperty("episodes") val episodes: List? = null ) { + fun hasEnded(): Boolean { + return status == "released" || status == "ended" + } + @JsonInclude(JsonInclude.Include.NON_EMPTY) data class Season( @JsonProperty("number") val number: Int, @@ -281,6 +367,194 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } } + class SimklScoreBuilder private constructor() { + data class Builder( + private var url: String? = null, + private var interceptor: Interceptor? = null, + private var ids: MediaObject.Ids? = null, + private var score: Int? = null, + private var status: Int? = null, + private var addEpisodes: Pair?, List?>? = null, + private var removeEpisodes: Pair?, List?>? = null, + ) { + fun interceptor(interceptor: Interceptor) = apply { this.interceptor = interceptor } + fun apiUrl(url: String) = apply { this.url = url } + fun ids(ids: MediaObject.Ids) = apply { this.ids = ids } + fun score(score: Int?, oldScore: Int?) = apply { + if (score != oldScore) { + this.score = score + } + } + + fun status(newStatus: Int?, oldStatus: Int?) = apply { + // Only set status if its new + if (newStatus != oldStatus) { + this.status = newStatus + } else { + this.status = null + } + } + + fun episodes( + allEpisodes: List?, + newEpisodes: Int?, + oldEpisodes: Int?, + ) = apply { + if (allEpisodes == null || newEpisodes == null) return@apply + + fun getEpisodes(rawEpisodes: List) = + if (rawEpisodes.any { it.season != null }) { + EpisodeMetadata.convertToSeasons(rawEpisodes) to null + } else { + null to EpisodeMetadata.convertToEpisodes(rawEpisodes) + } + + // Do not add episodes if there is no change + if (newEpisodes > (oldEpisodes ?: 0)) { + this.addEpisodes = getEpisodes(allEpisodes.take(newEpisodes)) + } + if ((oldEpisodes ?: 0) > newEpisodes) { + this.removeEpisodes = getEpisodes(allEpisodes.drop(newEpisodes)) + } + } + + suspend fun execute(): Boolean { + val time = getDateTime(unixTime) + + return if (this.status == SimklListStatusType.None.value) { + app.post( + "$url/sync/history/remove", + json = StatusRequest( + shows = listOf(HistoryMediaObject(ids = ids)), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } else { + val episodeRemovalResponse = removeEpisodes?.let { (seasons, episodes) -> + app.post( + "${this.url}/sync/history/remove", + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + ids = ids, + seasons = seasons, + episodes = episodes + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } ?: true + + val historyResponse = + // Only post if there are episodes or score to upload + if (addEpisodes != null || score != null) { + app.post( + "${this.url}/sync/history", + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + null, + null, + ids, + addEpisodes?.first, + addEpisodes?.second, + score, + score?.let { time }, + ) + ), movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } else { + true + } + + val statusResponse = status?.let { setStatus -> + val newStatus = + SimklListStatusType.values() + .firstOrNull { it.value == setStatus }?.originalName + ?: SimklListStatusType.Watching.originalName!! + + app.post( + "${this.url}/sync/add-to-list", + json = StatusRequest( + shows = listOf( + StatusMediaObject( + null, + null, + ids, + newStatus, + ) + ), movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } ?: true + + statusResponse && episodeRemovalResponse && historyResponse + } + } + } + } + + suspend fun getEpisodes( + simklId: Int?, + type: String?, + episodes: Int?, + hasEnded: Boolean? + ): Array? { + if (simklId == null) return null + + val cacheKey = "Episodes/$simklId" + val cache = SimklCache.getKey>(cacheKey) + + // Return cached result if its higher or equal the amount of episodes. + if (cache != null && cache.size >= (episodes ?: 0)) { + return cache + } + + // There is always one season in Anime -> no request necessary + if (type == "anime" && episodes != null) { + return episodes.takeIf { it > 0 }?.let { + (1..it).map { episode -> + EpisodeMetadata( + null, null, null, episode, null + ) + }.toTypedArray() + } + } + val url = when (type) { + "anime" -> "https://api.simkl.com/anime/episodes/$simklId" + "tv" -> "https://api.simkl.com/tv/episodes/$simklId" + "movie" -> return null + else -> return null + } + + debugPrint { "Requesting episodes from $url" } + return app.get(url, params = mapOf("client_id" to clientId)) + .parsedSafe>()?.also { + val cacheTime = + if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value + + // 1 Month cache + SimklCache.setKey(cacheKey, it, Duration.parse(cacheTime)) + } + } + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + class HistoryMediaObject( + @JsonProperty("title") title: String? = null, + @JsonProperty("year") year: Int? = null, + @JsonProperty("ids") ids: Ids? = null, + @JsonProperty("seasons") seasons: List? = null, + @JsonProperty("episodes") episodes: List? = null, + @JsonProperty("rating") val rating: Int? = null, + @JsonProperty("rated_at") val rated_at: String? = null, + ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) + @JsonInclude(JsonInclude.Include.NON_EMPTY) class RatingMediaObject( @JsonProperty("title") title: String?, @@ -299,15 +573,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime) ) : MediaObject(title, year, ids) - @JsonInclude(JsonInclude.Include.NON_EMPTY) - class HistoryMediaObject( - @JsonProperty("title") title: String?, - @JsonProperty("year") year: Int?, - @JsonProperty("ids") ids: Ids?, - @JsonProperty("seasons") seasons: List?, - @JsonProperty("episodes") episodes: List?, - ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) - @JsonInclude(JsonInclude.Include.NON_EMPTY) data class StatusRequest( @JsonProperty("movies") val movies: List, @@ -404,13 +669,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } data class ShowMetadata( - override val last_watched_at: String?, - override val status: String, - override val user_rating: Int?, - override val last_watched: String?, - override val watched_episodes_count: Int?, - override val total_episodes_count: Int?, - val show: Show + @JsonProperty("last_watched_at") override val last_watched_at: String?, + @JsonProperty("status") override val status: String, + @JsonProperty("user_rating") override val user_rating: Int?, + @JsonProperty("last_watched") override val last_watched: String?, + @JsonProperty("watched_episodes_count") override val watched_episodes_count: Int?, + @JsonProperty("total_episodes_count") override val total_episodes_count: Int?, + @JsonProperty("show") val show: Show ) : Metadata { override fun getIds(): Show.Ids { return this.show.ids @@ -435,23 +700,23 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } data class Show( - val title: String, - val poster: String?, - val year: Int?, - val ids: Ids, + @JsonProperty("title") val title: String, + @JsonProperty("poster") val poster: String?, + @JsonProperty("year") val year: Int?, + @JsonProperty("ids") val ids: Ids, ) { data class Ids( - val simkl: Int, - val slug: String?, - val imdb: String?, - val zap2it: String?, - val tmdb: String?, - val offen: String?, - val tvdb: String?, - val mal: String?, - val anidb: String?, - val anilist: String?, - val traktslug: String? + @JsonProperty("simkl") val simkl: Int, + @JsonProperty("slug") val slug: String?, + @JsonProperty("imdb") val imdb: String?, + @JsonProperty("zap2it") val zap2it: String?, + @JsonProperty("tmdb") val tmdb: String?, + @JsonProperty("offen") val offen: String?, + @JsonProperty("tvdb") val tvdb: String?, + @JsonProperty("mal") val mal: String?, + @JsonProperty("anidb") val anidb: String?, + @JsonProperty("anilist") val anilist: String?, + @JsonProperty("traktslug") val traktslug: String? ) { fun matchesId(database: SyncServices, id: String): Boolean { return when (database) { @@ -491,20 +756,58 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } } + /** + * Useful to get episodes on demand to prevent unnecessary requests. + */ + class SimklEpisodeConstructor( + private val simklId: Int?, + private val type: String?, + private val totalEpisodeCount: Int?, + private val hasEnded: Boolean? + ) { + suspend fun getEpisodes(): Array? { + return getEpisodes(simklId, type, totalEpisodeCount, hasEnded) + } + } + class SimklSyncStatus( override var status: Int, override var score: Int?, + val oldScore: Int?, override var watchedEpisodes: Int?, - val episodes: Array?, + val episodeConstructor: SimklEpisodeConstructor, override var isFavorite: Boolean? = null, override var maxEpisodes: Int? = null, /** Save seen episodes separately to know the change from old to new. * Required to remove seen episodes if count decreases */ val oldEpisodes: Int, + val oldStatus: String? ) : SyncAPI.AbstractSyncStatus() override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { val realIds = readIdFromString(id) + + // Key which assumes all ids are the same each time :/ + // This could be some sort of reference system to make multiple IDs + // point to the same key. + val idKey = + realIds.toList().map { "${it.first.originalName}=${it.second}" }.sorted().joinToString() + + val cachedObject = SimklCache.getKey(idKey) + val searchResult: MediaObject = cachedObject + ?: (searchByIds(realIds)?.firstOrNull()?.also { result -> + val cacheTime = + if (result.hasEnded()) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value + SimklCache.setKey(idKey, result, Duration.parse(cacheTime)) + }) ?: return null + + val episodeConstructor = SimklEpisodeConstructor( + searchResult.ids?.simkl, + searchResult.type, + searchResult.total_episodes, + searchResult.hasEnded() + ) + val foundItem = getSyncListSmart()?.let { list -> listOf(list.shows, list.anime, list.movies).flatten().firstOrNull { show -> realIds.any { (database, id) -> @@ -513,172 +816,63 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } } - // Search to get episodes - val searchResult = searchByIds(realIds)?.firstOrNull() - val episodes = getEpisodes(searchResult?.ids?.simkl, searchResult?.type) - if (foundItem != null) { return SimklSyncStatus( status = foundItem.status?.let { SimklListStatusType.fromString(it)?.value } ?: return null, score = foundItem.user_rating, watchedEpisodes = foundItem.watched_episodes_count, - maxEpisodes = foundItem.total_episodes_count, - episodes = episodes, + maxEpisodes = searchResult.total_episodes, + episodeConstructor = episodeConstructor, oldEpisodes = foundItem.watched_episodes_count ?: 0, + oldScore = foundItem.user_rating, + oldStatus = foundItem.status ) } else { - return if (searchResult != null) { - SimklSyncStatus( - status = SimklListStatusType.None.value, - score = 0, - watchedEpisodes = 0, - maxEpisodes = if (searchResult.type == "movie") 0 else null, - episodes = episodes, - oldEpisodes = 0, - ) - } else { - null - } + return SimklSyncStatus( + status = SimklListStatusType.None.value, + score = 0, + watchedEpisodes = 0, + maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes, + episodeConstructor = episodeConstructor, + oldEpisodes = 0, + oldStatus = null, + oldScore = null + ) } } override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { val parsedId = readIdFromString(id) lastScoreTime = unixTime - - if (status.status == SimklListStatusType.None.value) { - return app.post( - "$mainUrl/sync/history/remove", - json = StatusRequest( - shows = listOf( - HistoryMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - emptyList(), - emptyList() - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ).isSuccessful - } - - val realScore = status.score - val ratingResponseSuccess = if (realScore != null) { - // Remove rating if score is 0 - val ratingsSuffix = if (realScore == 0) "/remove" else "" - debugPrint { "Rate ${this.name} item: rating=$realScore" } - app.post( - "$mainUrl/sync/ratings$ratingsSuffix", - json = StatusRequest( - // Not possible to know if TV or Movie - shows = listOf( - RatingMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - realScore - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ).isSuccessful - } else { - true - } - val simklStatus = status as? SimklSyncStatus + + val builder = SimklScoreBuilder.Builder() + .apiUrl(this.mainUrl) + .score(status.score, simklStatus?.oldScore) + .status(status.status, (status as? SimklSyncStatus)?.oldStatus?.let { oldStatus -> + SimklListStatusType.values().firstOrNull { + it.originalName == oldStatus + }?.value + }) + .interceptor(interceptor) + .ids(MediaObject.Ids.fromMap(parsedId)) + + + // Get episodes only when required + val episodes = simklStatus?.episodeConstructor?.getEpisodes() + // All episodes if marked as completed val watchedEpisodes = if (status.status == SimklListStatusType.Completed.value) { - simklStatus?.episodes?.size + episodes?.size } else { status.watchedEpisodes } - // Only post episodes if available episodes and the status is correct - val episodeResponseSuccess = - if (simklStatus != null && watchedEpisodes != null && !simklStatus.episodes.isNullOrEmpty() && listOf( - SimklListStatusType.Paused.value, - SimklListStatusType.Dropped.value, - SimklListStatusType.Watching.value, - SimklListStatusType.Completed.value, - SimklListStatusType.ReWatching.value - ).contains(status.status) - ) { - suspend fun postEpisodes( - url: String, - rawEpisodes: List - ): Boolean { - val (seasons, episodes) = if (rawEpisodes.any { it.season != null }) { - EpisodeMetadata.convertToSeasons(rawEpisodes) to null - } else { - null to EpisodeMetadata.convertToEpisodes(rawEpisodes) - } - debugPrint { "Synced history using $url: seasons=${seasons?.toList()}, episodes=${episodes?.toList()}" } - return app.post( - url, - json = StatusRequest( - shows = listOf( - HistoryMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - seasons, - episodes - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ).isSuccessful - } + builder.episodes(episodes?.toList(), watchedEpisodes, simklStatus?.oldEpisodes) - // If episodes decrease: remove all episodes beyond watched episodes. - val removeResponse = if (simklStatus.oldEpisodes > watchedEpisodes) { - val removeEpisodes = simklStatus.episodes - .drop(watchedEpisodes) - postEpisodes("$mainUrl/sync/history/remove", removeEpisodes) - } else { - true - } - val cutEpisodes = simklStatus.episodes.take(watchedEpisodes) - val addResponse = postEpisodes("$mainUrl/sync/history/", cutEpisodes) - - removeResponse && addResponse - } else true - - val newStatus = - SimklListStatusType.values().firstOrNull { it.value == status.status }?.originalName - ?: SimklListStatusType.Watching.originalName - - val statusResponseSuccess = if (newStatus != null) { - debugPrint { "Add to ${this.name} list: status=$newStatus" } - app.post( - "$mainUrl/sync/add-to-list", - json = StatusRequest( - shows = listOf( - StatusMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - newStatus - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ).isSuccessful - } else { - true - } - - debugPrint { "All scoring complete: rating=$ratingResponseSuccess, status=$statusResponseSuccess, episode=$episodeResponseSuccess" } requireLibraryRefresh = true - return ratingResponseSuccess && statusResponseSuccess && episodeResponseSuccess + return builder.execute() } @@ -694,17 +888,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ).parsedSafe() } - suspend fun getEpisodes(simklId: Int?, type: String?): Array? { - if (simklId == null) return null - val url = when (type) { - "anime" -> "https://api.simkl.com/anime/episodes/$simklId" - "tv" -> "https://api.simkl.com/tv/episodes/$simklId" - "movie" -> return null - else -> return null - } - return app.get(url, params = mapOf("client_id" to clientId)).parsedSafe() - } - override suspend fun search(name: String): List? { return app.get( "$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name) @@ -737,16 +920,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { return null } - private suspend fun getSyncListSince(since: Long?): AllItemsResponse { + private suspend fun getSyncListSince(since: Long?): AllItemsResponse? { val params = getDateTime(since)?.let { mapOf("date_from" to it) } ?: emptyMap() + // Can return null on no change. return app.get( "$mainUrl/sync/all-items/", params = params, interceptor = interceptor - ).parsed() + ).parsedSafe() } private suspend fun getActivities(): ActivitiesResponse? { From b6e99d7358ee5d13127be79869fa162a505f3a09 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 9 Sep 2023 23:18:21 +0200 Subject: [PATCH 218/570] backend change for events in player --- .../ui/player/AbstractPlayerFragment.kt | 95 ++++++-- .../cloudstream3/ui/player/CS3IPlayer.kt | 215 +++++++----------- .../ui/player/FullScreenPlayer.kt | 4 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 11 +- .../cloudstream3/ui/player/IPlayer.kt | 136 +++++++++-- .../ui/result/ResultFragmentPhone.kt | 4 +- .../ui/result/ResultTrailerPlayer.kt | 10 +- 7 files changed, 289 insertions(+), 186 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 53ee5e12..c6f02f1a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -92,11 +92,11 @@ abstract class AbstractPlayerFragment( throw NotImplementedError() } - open fun playerPositionChanged(posDur: Pair) { + open fun playerPositionChanged(position: Long, duration : Long) { throw NotImplementedError() } - open fun playerDimensionsLoaded(widthHeight: Pair) { + open fun playerDimensionsLoaded(width: Int, height : Int) { throw NotImplementedError() } @@ -132,8 +132,8 @@ abstract class AbstractPlayerFragment( } } - private fun updateIsPlaying(playing: Pair) { - val (wasPlaying, isPlaying) = playing + private fun updateIsPlaying(wasPlaying : CSPlayerLoading, + isPlaying : CSPlayerLoading) { val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying @@ -206,7 +206,7 @@ abstract class AbstractPlayerFragment( CSPlayerEvent.values()[intent.getIntExtra( EXTRA_CONTROL_TYPE, 0 - )] + )], source = PlayerEventSource.UI ) } } @@ -216,7 +216,7 @@ abstract class AbstractPlayerFragment( val isPlaying = player.getIsPlaying() val isPlayingValue = if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused - updateIsPlaying(Pair(isPlayingValue, isPlayingValue)) + updateIsPlaying(isPlayingValue, isPlayingValue) } else { // Restore the full-screen UI. piphide?.isVisible = true @@ -249,7 +249,7 @@ abstract class AbstractPlayerFragment( } } - open fun playerError(exception: Exception) { + open fun playerError(exception: Throwable) { fun showToast(message: String, gotoNext: Boolean = false) { if (gotoNext && hasNextMirror()) { showToast( @@ -326,6 +326,7 @@ abstract class AbstractPlayerFragment( } } + @SuppressLint("UnsafeOptInUsageError") private fun playerUpdated(player: Any?) { if (player is ExoPlayer) { context?.let { ctx -> @@ -366,6 +367,71 @@ abstract class AbstractPlayerFragment( // } //} + /** This receives the events from the player, if you want to append functionality you do it here, + * do note that this only receives events for UI changes, + * and returning early WONT stop it from changing in eg the player time or pause status */ + open fun mainCallback(event : PlayerEvent) { + when(event) { + is ResizedEvent -> { + playerDimensionsLoaded(event.width, event.height) + } + is PlayerAttachedEvent -> { + playerUpdated(event.player) + } + is SubtitlesUpdatedEvent -> { + subtitlesChanged() + } + is TimestampSkippedEvent -> { + onTimestampSkipped(event.timestamp) + } + is TimestampInvokedEvent -> { + onTimestamp(event.timestamp) + } + is TracksChangedEvent -> { + onTracksInfoChanged() + } + is EmbeddedSubtitlesFetchedEvent -> { + embeddedSubtitlesFetched(event.tracks) + } + is ErrorEvent -> { + playerError(event.error) + } + is RequestAudioFocusEvent -> { + requestAudioFocus() + } + is EpisodeSeekEvent -> { + when(event.offset) { + -1 -> prevEpisode() + 1 -> nextEpisode() + else -> {} + } + } + is StatusEvent -> { + updateIsPlaying(wasPlaying = event.wasPlaying, isPlaying = event.isPlaying) + } + is PositionEvent -> { + playerPositionChanged(position = event.toMs, duration = event.durationMs) + } + is VideoEndedEvent -> { + context?.let { ctx -> + // Only play next episode if autoplay is on (default) + if (PreferenceManager.getDefaultSharedPreferences(ctx) + ?.getBoolean( + ctx.getString(R.string.autoplay_next_key), + true + ) == true + ) { + player.handleEvent( + CSPlayerEvent.NextEpisode, + source = PlayerEventSource.Player + ) + } + } + } + is PauseEvent -> Unit + is PlayEvent -> Unit + } + } @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -374,25 +440,13 @@ abstract class AbstractPlayerFragment( player.releaseCallbacks() player.initCallbacks( - playerUpdated = ::playerUpdated, - updateIsPlaying = ::updateIsPlaying, - playerError = ::playerError, - requestAutoFocus = ::requestAudioFocus, - nextEpisode = ::nextEpisode, - prevEpisode = ::prevEpisode, - playerPositionChanged = ::playerPositionChanged, - playerDimensionsLoaded = ::playerDimensionsLoaded, + eventHandler = ::mainCallback, requestedListeningPercentages = listOf( SKIP_OP_VIDEO_PERCENTAGE, PRELOAD_NEXT_EPISODE_PERCENTAGE, NEXT_WATCH_EPISODE_PERCENTAGE, UPDATE_SYNC_PROGRESS_PERCENTAGE, ), - subtitlesUpdates = ::subtitlesChanged, - embeddedSubtitlesFetched = ::embeddedSubtitlesFetched, - onTracksInfoChanged = ::onTracksInfoChanged, - onTimestampInvoked = ::onTimestamp, - onTimestampSkipped = ::onTimestampSkipped ) if (player is CS3IPlayer) { @@ -461,6 +515,7 @@ abstract class AbstractPlayerFragment( resize(PlayerResize.values()[resize], showToast) } + @SuppressLint("UnsafeOptInUsageError") fun resize(resize: PlayerResize, showToast: Boolean) { setKey(RESIZE_MODE_KEY, resize.ordinal) val type = when (resize) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 4a88a2e7..fe4e3423 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -8,7 +8,6 @@ import android.os.Looper import android.util.Log import android.util.Rational import android.widget.FrameLayout -import androidx.media3.common.C import androidx.media3.common.C.* import androidx.media3.common.Format import androidx.media3.common.MediaItem @@ -135,80 +134,24 @@ class CS3IPlayer : IPlayer { * Boolean = if it's active * */ private var playerSelectedSubtitleTracks = listOf>() - - /** isPlaying */ - private var updateIsPlaying: ((Pair) -> Unit)? = null - private var requestAutoFocus: (() -> Unit)? = null - private var playerError: ((Exception) -> Unit)? = null - private var subtitlesUpdates: (() -> Unit)? = null - - /** width x height */ - private var playerDimensionsLoaded: ((Pair) -> Unit)? = null - - /** used for playerPositionChanged */ private var requestedListeningPercentages: List? = null - /** Fired when seeking the player or on requestedListeningPercentages, - * used to make things appear on que - * position, duration */ - private var playerPositionChanged: ((Pair) -> Unit)? = null + private var eventHandler: ((PlayerEvent) -> Unit)? = null - private var nextEpisode: (() -> Unit)? = null - private var prevEpisode: (() -> Unit)? = null - - private var playerUpdated: ((Any?) -> Unit)? = null - private var embeddedSubtitlesFetched: ((List) -> Unit)? = null - private var onTracksInfoChanged: (() -> Unit)? = null - private var onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)? = null - private var onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)? = null + fun event(event: PlayerEvent) { + eventHandler?.invoke(event) + } override fun releaseCallbacks() { - playerUpdated = null - updateIsPlaying = null - requestAutoFocus = null - playerError = null - playerDimensionsLoaded = null - requestedListeningPercentages = null - playerPositionChanged = null - nextEpisode = null - prevEpisode = null - subtitlesUpdates = null - onTracksInfoChanged = null - onTimestampInvoked = null - requestSubtitleUpdate = null - onTimestampSkipped = null + eventHandler = null } override fun initCallbacks( - playerUpdated: (Any?) -> Unit, - updateIsPlaying: ((Pair) -> Unit)?, - requestAutoFocus: (() -> Unit)?, - playerError: ((Exception) -> Unit)?, - playerDimensionsLoaded: ((Pair) -> Unit)?, + eventHandler: ((PlayerEvent) -> Unit), requestedListeningPercentages: List?, - playerPositionChanged: ((Pair) -> Unit)?, - nextEpisode: (() -> Unit)?, - prevEpisode: (() -> Unit)?, - subtitlesUpdates: (() -> Unit)?, - embeddedSubtitlesFetched: ((List) -> Unit)?, - onTracksInfoChanged: (() -> Unit)?, - onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)?, - onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)?, ) { - this.playerUpdated = playerUpdated - this.updateIsPlaying = updateIsPlaying - this.requestAutoFocus = requestAutoFocus - this.playerError = playerError - this.playerDimensionsLoaded = playerDimensionsLoaded this.requestedListeningPercentages = requestedListeningPercentages - this.playerPositionChanged = playerPositionChanged - this.nextEpisode = nextEpisode - this.prevEpisode = prevEpisode - this.subtitlesUpdates = subtitlesUpdates - this.embeddedSubtitlesFetched = embeddedSubtitlesFetched - this.onTracksInfoChanged = onTracksInfoChanged - this.onTimestampInvoked = onTimestampInvoked - this.onTimestampSkipped = onTimestampSkipped + this.eventHandler = eventHandler } // I know, this is not a perfect solution, however it works for fixing subs @@ -217,7 +160,7 @@ class CS3IPlayer : IPlayer { try { Handler(it).post { try { - seekTime(1L) + seekTime(1L, source = PlayerEventSource.Player) } catch (e: Exception) { logError(e) } @@ -271,8 +214,9 @@ class CS3IPlayer : IPlayer { subtitleHelper.setAllSubtitles(subtitles) } - var currentSubtitles: SubtitleData? = null + private var currentSubtitles: SubtitleData? = null + @SuppressLint("UnsafeOptInUsageError") private fun List.getTrack(id: String?): Pair? { if (id == null) return null // This beast of an expression does: @@ -526,14 +470,14 @@ class CS3IPlayer : IPlayer { Log.i(TAG, "onStop") saveData() - exoPlayer?.pause() + handleEvent(CSPlayerEvent.Pause, PlayerEventSource.Player) //releasePlayer() } override fun onPause() { Log.i(TAG, "onPause") saveData() - exoPlayer?.pause() + handleEvent(CSPlayerEvent.Pause, PlayerEventSource.Player) //releasePlayer() } @@ -611,6 +555,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") private fun Context.createOfflineSource(): DataSource.Factory { return DefaultDataSourceFactory(this, USER_AGENT) } @@ -846,43 +791,55 @@ class CS3IPlayer : IPlayer { return null } - fun updatedTime(writePosition: Long? = null) { + fun updatedTime( + writePosition: Long? = null, + source: PlayerEventSource = PlayerEventSource.Player + ) { val position = writePosition ?: exoPlayer?.currentPosition getCurrentTimestamp(position)?.let { timestamp -> - onTimestampInvoked?.invoke(timestamp) + event(TimestampInvokedEvent(timestamp, source)) } val duration = exoPlayer?.contentDuration if (duration != null && position != null) { - playerPositionChanged?.invoke(Pair(position, duration)) + event( + PositionEvent( + source, + fromMs = exoPlayer?.currentPosition ?: 0, + position, + duration + ) + ) } } - override fun seekTime(time: Long) { - exoPlayer?.seekTime(time) + override fun seekTime(time: Long, source: PlayerEventSource) { + exoPlayer?.seekTime(time, source) } - override fun seekTo(time: Long) { - updatedTime(time) + override fun seekTo(time: Long, source: PlayerEventSource) { + updatedTime(time, source) exoPlayer?.seekTo(time) } - private fun ExoPlayer.seekTime(time: Long) { - updatedTime(currentPosition + time) + private fun ExoPlayer.seekTime(time: Long, source: PlayerEventSource) { + updatedTime(currentPosition + time, source) seekTo(currentPosition + time) } - override fun handleEvent(event: CSPlayerEvent) { + override fun handleEvent(event: CSPlayerEvent, source: PlayerEventSource) { Log.i(TAG, "handleEvent ${event.name}") try { exoPlayer?.apply { when (event) { CSPlayerEvent.Play -> { + event(PlayEvent(source)) play() } CSPlayerEvent.Pause -> { + event(PauseEvent(source)) pause() } @@ -899,32 +856,32 @@ class CS3IPlayer : IPlayer { CSPlayerEvent.PlayPauseToggle -> { if (isPlaying) { - pause() + handleEvent(CSPlayerEvent.Pause, source) } else { - play() + handleEvent(CSPlayerEvent.Play, source) } } - CSPlayerEvent.SeekForward -> seekTime(seekActionTime) - CSPlayerEvent.SeekBack -> seekTime(-seekActionTime) - CSPlayerEvent.NextEpisode -> nextEpisode?.invoke() - CSPlayerEvent.PrevEpisode -> prevEpisode?.invoke() + CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source) + CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source) + CSPlayerEvent.NextEpisode -> event(EpisodeSeekEvent(offset = 1, source = source)) + CSPlayerEvent.PrevEpisode -> event(EpisodeSeekEvent(offset = -1, source = source)) CSPlayerEvent.SkipCurrentChapter -> { //val dur = this@CS3IPlayer.getDuration() ?: return@apply getCurrentTimestamp()?.let { lastTimeStamp -> if (lastTimeStamp.skipToNextEpisode) { - handleEvent(CSPlayerEvent.NextEpisode) + handleEvent(CSPlayerEvent.NextEpisode, source) } else { seekTo(lastTimeStamp.endMs + 1L) } - onTimestampSkipped?.invoke(lastTimeStamp) + event(TimestampSkippedEvent(timestamp = lastTimeStamp, source = source)) } } } } - } catch (e: Exception) { - Log.e(TAG, "handleEvent error", e) - playerError?.invoke(e) + } catch (t: Throwable) { + Log.e(TAG, "handleEvent error", t) + event(ErrorEvent(t)) } } @@ -963,18 +920,14 @@ class CS3IPlayer : IPlayer { requestSubtitleUpdate = ::reloadSubs - playerUpdated?.invoke(exoPlayer) + event(PlayerAttachedEvent(exoPlayer)) exoPlayer?.prepare() exoPlayer?.let { exo -> - updateIsPlaying?.invoke( - Pair( - CSPlayerLoading.IsBuffering, - CSPlayerLoading.IsBuffering - ) - ) + event(StatusEvent(CSPlayerLoading.IsBuffering, CSPlayerLoading.IsBuffering)) isPlaying = exo.isPlaying } + exoPlayer?.addListener(object : Player.Listener { override fun onTracksChanged(tracks: Tracks) { normalSafeApiCall { @@ -1008,18 +961,19 @@ class CS3IPlayer : IPlayer { ) } - embeddedSubtitlesFetched?.invoke(exoPlayerReportedTracks) - onTracksInfoChanged?.invoke() - subtitlesUpdates?.invoke() + event(EmbeddedSubtitlesFetchedEvent(tracks = exoPlayerReportedTracks)) + event(TracksChangedEvent()) + event(SubtitlesUpdatedEvent()) } } + @SuppressLint("UnsafeOptInUsageError") override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { exoPlayer?.let { exo -> - updateIsPlaying?.invoke( - Pair( - if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused, - if (playbackState == Player.STATE_BUFFERING) CSPlayerLoading.IsBuffering else if (exo.isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused + event( + StatusEvent( + wasPlaying = if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused, + isPlaying = if (playbackState == Player.STATE_BUFFERING) CSPlayerLoading.IsBuffering else if (exo.isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused ) ) isPlaying = exo.isPlaying @@ -1041,23 +995,15 @@ class CS3IPlayer : IPlayer { } Player.STATE_ENDED -> { - // Only play next episode if autoplay is on (default) - if (PreferenceManager.getDefaultSharedPreferences(context) - ?.getBoolean( - context.getString(com.lagradost.cloudstream3.R.string.autoplay_next_key), - true - ) == true - ) { - handleEvent(CSPlayerEvent.NextEpisode) - } + event(VideoEndedEvent()) } Player.STATE_BUFFERING -> { - updatedTime() + updatedTime(source = PlayerEventSource.Player) } Player.STATE_IDLE -> { - // IDLE + } else -> Unit @@ -1082,7 +1028,7 @@ class CS3IPlayer : IPlayer { } else -> { - playerError?.invoke(error) + event(ErrorEvent(error)) } } @@ -1096,7 +1042,7 @@ class CS3IPlayer : IPlayer { override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) if (isPlaying) { - requestAutoFocus?.invoke() + event(RequestAudioFocusEvent()) onRenderFirst() } } @@ -1116,12 +1062,15 @@ class CS3IPlayer : IPlayer { true ) == true ) { - handleEvent(CSPlayerEvent.NextEpisode) + handleEvent( + CSPlayerEvent.NextEpisode, + source = PlayerEventSource.Player + ) } } Player.STATE_BUFFERING -> { - updatedTime() + updatedTime(source = PlayerEventSource.Player) } Player.STATE_IDLE -> { @@ -1134,18 +1083,18 @@ class CS3IPlayer : IPlayer { override fun onVideoSizeChanged(videoSize: VideoSize) { super.onVideoSizeChanged(videoSize) - playerDimensionsLoaded?.invoke(Pair(videoSize.width, videoSize.height)) + event(ResizedEvent(height = videoSize.height, width = videoSize.width)) } override fun onRenderedFirstFrame() { super.onRenderedFirstFrame() onRenderFirst() - updatedTime() + updatedTime(source = PlayerEventSource.Player) } }) - } catch (e: Exception) { - Log.e(TAG, "loadExo error", e) - playerError?.invoke(e) + } catch (t: Throwable) { + Log.e(TAG, "loadExo error", t) + event(ErrorEvent(t)) } } @@ -1156,7 +1105,7 @@ class CS3IPlayer : IPlayer { lastTimeStamps = timeStamps timeStamps.forEach { timestamp -> exoPlayer?.createMessage { _, _ -> - updatedTime() + updatedTime(source = PlayerEventSource.Player) //if (payload is EpisodeSkip.SkipStamp) // this should always be true // onTimestampInvoked?.invoke(payload) } @@ -1166,7 +1115,7 @@ class CS3IPlayer : IPlayer { ?.setDeleteAfterDelivery(false) ?.send() } - updatedTime() + updatedTime(source = PlayerEventSource.Player) } @SuppressLint("UnsafeOptInUsageError") @@ -1187,7 +1136,7 @@ class CS3IPlayer : IPlayer { if (invalid) { releasePlayer(saveTime = false) - playerError?.invoke(InvalidFileException("Too short playback")) + event(ErrorEvent(InvalidFileException("Too short playback"))) return } @@ -1196,7 +1145,7 @@ class CS3IPlayer : IPlayer { val width = format?.width val height = format?.height if (height != null && width != null) { - playerDimensionsLoaded?.invoke(Pair(width, height)) + event(ResizedEvent(width = width, height = height)) updatedTime() exoPlayer?.apply { requestedListeningPercentages?.forEach { percentage -> @@ -1230,9 +1179,9 @@ class CS3IPlayer : IPlayer { subtitleHelper.setActiveSubtitles(activeSubtitles.toSet()) loadExo(context, listOf(MediaItemSlice(mediaItem, Long.MIN_VALUE)), subSources) - } catch (e: Exception) { - Log.e(TAG, "loadOfflinePlayer error", e) - playerError?.invoke(e) + } catch (t: Throwable) { + Log.e(TAG, "loadOfflinePlayer error", t) + event(ErrorEvent(t)) } } @@ -1365,9 +1314,9 @@ class CS3IPlayer : IPlayer { } loadExo(context, mediaItems, subSources, cacheFactory) - } catch (e: Exception) { - Log.e(TAG, "loadOnlinePlayer error", e) - playerError?.invoke(e) + } catch (t: Throwable) { + Log.e(TAG, "loadOnlinePlayer error", t) + event(ErrorEvent(t)) } } 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 0f3c189d..6dabb5b7 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 @@ -874,7 +874,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { currentTouch )?.let { seekTo -> if (abs(seekTo - startTime) > MINIMUM_SEEK_TIME) { - player.seekTo(seekTo) + player.seekTo(seekTo, PlayerEventSource.UI) } } } @@ -909,7 +909,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } else -> { - player.handleEvent(CSPlayerEvent.PlayPauseToggle) + player.handleEvent(CSPlayerEvent.PlayPauseToggle, PlayerEventSource.UI) } } } else if (doubleTapEnabled && isFullScreenPlayer) { 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 2b9304b6..b2542ffa 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 @@ -551,7 +551,7 @@ class GeneratorPlayer : FullScreenPlayer() { //println("CURRENT SELECTED :$currentSelectedSubtitles of $currentSubs") context?.let { ctx -> val isPlaying = player.getIsPlaying() - player.handleEvent(CSPlayerEvent.Pause) + player.handleEvent(CSPlayerEvent.Pause, PlayerEventSource.UI) val currentSubtitles = sortSubs(currentSubs) val sourceDialog = Dialog(ctx, R.style.AlertDialogCustomBlack) @@ -883,7 +883,7 @@ class GeneratorPlayer : FullScreenPlayer() { } - override fun playerError(exception: Exception) { + override fun playerError(exception: Throwable) { Log.i(TAG, "playerError = $currentSelectedLink") super.playerError(exception) } @@ -945,14 +945,13 @@ class GeneratorPlayer : FullScreenPlayer() { var maxEpisodeSet: Int? = null var hasRequestedStamps: Boolean = false - override fun playerPositionChanged(posDur: Pair) { + override fun playerPositionChanged(position: Long, duration : Long) { // Don't save livestream data if ((currentMeta as? ResultEpisode)?.tvType?.isLiveStream() == true) return // Don't save NSFW data if ((currentMeta as? ResultEpisode)?.tvType == TvType.NSFW) return - val (position, duration) = posDur if (duration <= 0L) return // idk how you achieved this, but div by zero crash if (!hasRequestedStamps) { hasRequestedStamps = true @@ -1209,8 +1208,8 @@ class GeneratorPlayer : FullScreenPlayer() { } } - override fun playerDimensionsLoaded(widthHeight: Pair) { - setPlayerDimen(widthHeight) + override fun playerDimensionsLoaded(width: Int, height : Int) { + setPlayerDimen(width to height) } private fun unwrapBundle(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index 3038cb8d..ec006234 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -45,9 +45,120 @@ enum class CSPlayerLoading { IsPaused, IsPlaying, IsBuffering, - //IsDone, } +enum class PlayerEventSource { + /** This event was invoked from the user pressing some button or selecting something */ + UI, + + /** This event was invoked automatically */ + Player, + + /** This event was invoked from a external sync tool like WatchTogether */ + Sync, +} + +abstract class PlayerEvent { + abstract val source: PlayerEventSource +} + +/** this is used to update UI based of the current time, + * using requestedListeningPercentages as well as saving time */ +data class PositionEvent( + override val source: PlayerEventSource, + val fromMs: Long, + val toMs: Long, + /** duration of the entire video */ + val durationMs: Long, +) : PlayerEvent() { + /** how many ms (+-) we have skipped */ + val seekMs : Long get() = toMs - fromMs +} + +/** player error when rendering or misc, used to display toast or log */ +data class ErrorEvent( + val error: Throwable, + override val source: PlayerEventSource = PlayerEventSource.Player, +) : PlayerEvent() + +/** Event when timestamps appear, null when it should disappear */ +data class TimestampInvokedEvent( + val timestamp: EpisodeSkip.SkipStamp, + override val source: PlayerEventSource = PlayerEventSource.Player, +) : PlayerEvent() + +/** Event for when a chapter is skipped, aka when event is handled (or for future use when skip automatically ads/sponsor) */ +data class TimestampSkippedEvent( + val timestamp: EpisodeSkip.SkipStamp, + override val source: PlayerEventSource = PlayerEventSource.Player, +) : PlayerEvent() + +/** this is used by the player to load the next or prev episode */ +data class EpisodeSeekEvent( + /** -1 = prev, 1 = next, will never be 0, atm the user cant seek more than +-1 */ + val offset: Int, + override val source: PlayerEventSource = PlayerEventSource.Player, +) : PlayerEvent() { + init { + assert(offset != 0) + } +} + +/** Event when the video is resized aka changed resolution or mirror */ +data class ResizedEvent( + val height: Int, + val width: Int, + override val source: PlayerEventSource = PlayerEventSource.Player, +) : PlayerEvent() + +/** Event when the player status update, along with the previous status (for animation)*/ +data class StatusEvent( + val wasPlaying: CSPlayerLoading, + val isPlaying: CSPlayerLoading, + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Event when tracks are changed, used for UI changes */ +data class TracksChangedEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Event from player to give all embedded subtitles */ +data class EmbeddedSubtitlesFetchedEvent( + val tracks: List, + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** on attach player to view */ +data class PlayerAttachedEvent( + val player: Any?, + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Event from player to inform that subtitles have updated in some way */ +data class SubtitlesUpdatedEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** current player starts, asking for all other programs to shut the fuck up */ +data class RequestAudioFocusEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Pause event, separate from StatusEvent */ +data class PauseEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Play event, separate from StatusEvent */ +data class PlayEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Event when the player video has ended, up to the settings on what to do when that happens */ +data class VideoEndedEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() interface Track { /** @@ -108,27 +219,16 @@ interface IPlayer { fun getDuration(): Long? fun getPosition(): Long? - fun seekTime(time: Long) - fun seekTo(time: Long) + fun seekTime(time: Long, source: PlayerEventSource = PlayerEventSource.UI) + fun seekTo(time: Long, source: PlayerEventSource = PlayerEventSource.UI) fun getSubtitleOffset(): Long // in ms fun setSubtitleOffset(offset: Long) // in ms fun initCallbacks( - playerUpdated: (Any?) -> Unit, // attach player to view - updateIsPlaying: ((Pair) -> Unit)? = null, // (wasPlaying, isPlaying) - requestAutoFocus: (() -> Unit)? = null, // current player starts, asking for all other programs to shut the fuck up - playerError: ((Exception) -> Unit)? = null, // player error when rendering or misc, used to display toast or log - playerDimensionsLoaded: ((Pair) -> Unit)? = null, // (with, height), for UI - requestedListeningPercentages: List? = null, // this is used to request when the player should report back view percentage - playerPositionChanged: ((Pair) -> Unit)? = null,// (position, duration) this is used to update UI based of the current time - nextEpisode: (() -> Unit)? = null, // this is used by the player to load the next episode - prevEpisode: (() -> Unit)? = null, // this is used by the player to load the previous episode - subtitlesUpdates: (() -> Unit)? = null, // callback from player to inform that subtitles have updated in some way - embeddedSubtitlesFetched: ((List) -> Unit)? = null, // callback from player to give all embedded subtitles - onTracksInfoChanged: (() -> Unit)? = null, // Callback when tracks are changed, used for UI changes - onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)? = null, // Callback when timestamps appear, null when it should disappear - onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)? = null, // callback for when a chapter is skipped, aka when event is handled (or for future use when skip automatically ads/sponsor) + eventHandler: ((PlayerEvent) -> Unit), + /** this is used to request when the player should report back view percentage */ + requestedListeningPercentages: List? = null, ) fun releaseCallbacks() @@ -155,7 +255,7 @@ interface IPlayer { fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean // returns true if the player requires a reload, null for nothing fun getCurrentPreferredSubtitle(): SubtitleData? - fun handleEvent(event: CSPlayerEvent) + fun handleEvent(event: CSPlayerEvent, source: PlayerEventSource = PlayerEventSource.UI) fun onStop() fun onPause() 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 a932a57c..ef2ed0df 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 @@ -130,8 +130,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { return currentTrailerIndex + 1 < currentTrailers.size } - override fun playerError(exception: Exception) { - if (player.getIsPlaying()) { // because we dont want random toasts in player + override fun playerError(exception: Throwable) { + if (player.getIsPlaying()) { // because we don't want random toasts in player super.playerError(exception) } else { nextMirror() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index 91e97dfc..5208e4a5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.ui.result import android.animation.ValueAnimator import android.content.Context import android.content.res.Configuration -import android.graphics.Rect import android.os.Bundle import android.view.View import android.view.ViewGroup @@ -12,6 +11,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent +import com.lagradost.cloudstream3.ui.player.PlayerEventSource import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.utils.IOnBackPressed @@ -32,7 +32,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { override fun prevEpisode() {} - override fun playerPositionChanged(posDur: Pair) {} + override fun playerPositionChanged(position: Long, duration : Long) {} override fun nextMirror() {} @@ -99,8 +99,8 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { } } - override fun playerDimensionsLoaded(widthHeight: Pair) { - playerWidthHeight = widthHeight + override fun playerDimensionsLoaded(width: Int, height : Int) { + playerWidthHeight = width to height fixPlayerSize() } @@ -164,7 +164,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { playerBinding?.playerIntroPlay?.setOnClickListener { playerBinding?.playerIntroPlay?.isGone = true - player.handleEvent(CSPlayerEvent.Play) + player.handleEvent(CSPlayerEvent.Play, PlayerEventSource.UI) updateUIVisibility() fixPlayerSize() } From 85c4c74222188ef1b998bdef62eb011cea8efedc Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 9 Sep 2023 23:53:35 +0200 Subject: [PATCH 219/570] fixed shitty external extractor code --- .../main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt | 3 +++ 1 file changed, 3 insertions(+) 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 0a926374..62217a0b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -378,6 +378,9 @@ open class ExtractorLink constructor( open val extractorData: String? = null, open val type: ExtractorLinkType, ) : VideoDownloadManager.IDownloadableMinimum { + val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8 + val isDash : Boolean get() = type == ExtractorLinkType.DASH + constructor( source: String, name: String, From 0afbc90cd2a57c0cd5bb6d0709dcd44c0fc85b26 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 9 Sep 2023 23:57:18 +0200 Subject: [PATCH 220/570] fixed last fix --- .../main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt | 1 - 1 file changed, 1 deletion(-) 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 62217a0b..5edff7a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -227,7 +227,6 @@ data class ExtractorLinkPlayList( val playlist: List, override val referer: String, override val quality: Int, - val isM3u8: Boolean = false, override val headers: Map = mapOf(), /** Used for getExtractorVerifierJob() */ override val extractorData: String? = null, From f6b0ea8dfa4253446e9532689531a0aba6505f54 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sun, 10 Sep 2023 16:31:01 +0300 Subject: [PATCH 221/570] Stream button type switch support (#597) * Stream button type switch support * Hide Hls playlist switch --- .../com/lagradost/cloudstream3/ui/player/LinkGenerator.kt | 5 ++--- app/src/main/res/layout/stream_input.xml | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index ba2cdb40..ca2d9c81 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -67,9 +67,8 @@ class LinkGenerator( link.name ?: link.url, unshortenLinkSafe(link.url), // unshorten because it might be a raw link referer ?: "", - Qualities.Unknown.value, isM3u8 ?: normalSafeApiCall { - URI(link.url).path?.substringAfterLast(".")?.contains("m3u") - } ?: false + Qualities.Unknown.value, + type = INFER_TYPE, ) to null ) } diff --git a/app/src/main/res/layout/stream_input.xml b/app/src/main/res/layout/stream_input.xml index 20a91b4a..83605ce3 100644 --- a/app/src/main/res/layout/stream_input.xml +++ b/app/src/main/res/layout/stream_input.xml @@ -49,7 +49,8 @@ android:layout_marginTop="10dp" android:text="@string/hls_playlist" android:textColor="?attr/textColor" - android:textSize="16sp" /> + android:textSize="16sp" + android:visibility="invisible" /> From 7f7c81828a294a6f77cbc563dcf1b77b96173e6e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:37:02 +0200 Subject: [PATCH 222/570] added UI event for seekbar --- .../ui/player/AbstractPlayerFragment.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index c6f02f1a..4316bbc6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -7,6 +7,7 @@ import android.graphics.drawable.AnimatedVectorDrawable import android.media.metrics.PlaybackErrorEvent import android.os.Build import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -25,8 +26,10 @@ import androidx.media3.common.PlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.DefaultTimeBar import androidx.media3.ui.PlayerView import androidx.media3.ui.SubtitleView +import androidx.media3.ui.TimeBar import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode @@ -371,6 +374,7 @@ abstract class AbstractPlayerFragment( * do note that this only receives events for UI changes, * and returning early WONT stop it from changing in eg the player time or pause status */ open fun mainCallback(event : PlayerEvent) { + Log.i(TAG, "Handle event: $event") when(event) { is ResizedEvent -> { playerDimensionsLoaded(event.width, event.height) @@ -433,7 +437,7 @@ abstract class AbstractPlayerFragment( } } - @SuppressLint("SetTextI18n") + @SuppressLint("SetTextI18n", "UnsafeOptInUsageError") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { resizeMode = getKey(RESIZE_MODE_KEY) ?: 0 resize(resizeMode, false) @@ -454,6 +458,19 @@ abstract class AbstractPlayerFragment( subStyle = SubtitlesFragment.getCurrentSavedStyle() player.initSubtitles(subView, subtitleHolder, subStyle) + /** this might seam a bit fucky and that is because it is, the seek event is captured twice, once by the player + * and once by the UI even if it should only be registered once by the UI */ + playerView?.findViewById(R.id.exo_progress)?.addListener(object : TimeBar.OnScrubListener { + override fun onScrubStart(timeBar: TimeBar, position: Long) = Unit + override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit + override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) { + if (canceled) return + val playerDuration = player.getDuration() ?: return + val playerPosition = player.getPosition() ?: return + mainCallback(PositionEvent(source = PlayerEventSource.UI, durationMs = playerDuration, fromMs = playerPosition, toMs = position)) + } + }) + SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged try { From 10bc688eaf00d34bd41da06d53f1142fc7888f09 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:29:30 +0200 Subject: [PATCH 223/570] fixed tracker on dub --- .../lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 b2c57137..b398b54e 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 @@ -1538,7 +1538,11 @@ class ResultViewModel2 : ViewModel() { this.name, this.japName ).filter { it.length > 2 } - .distinct(), // the reason why we filter is due to not wanting smth like " " or "?" + .distinct().map { + // this actually would be nice if we improved a bit as 3rd season == season 3 == III ect + // right now it just removes the dubbed status + it.lowercase().replace(Regex("""\(?[ds]ub(bed)?\)?(\s|$)""") , "").trim() + }, TrackerType.getTypes(this.type), this.year ) From 01e7acdeace32f20e6c23f5cb187977bd6131d45 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Mon, 11 Sep 2023 19:31:11 +0700 Subject: [PATCH 224/570] getTracker: switched to anilist api (#593) * getTracker: switched to anilist api --------- Co-authored-by: Sofie99 --- .../com/lagradost/cloudstream3/MainAPI.kt | 119 +++++++++++++----- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 80332445..0175e0d0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -22,8 +22,10 @@ import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf +import com.lagradost.nicehttp.RequestBodyTypes import okhttp3.Interceptor -import org.mozilla.javascript.Scriptable +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody import java.text.SimpleDateFormat import java.util.* import kotlin.math.absoluteValue @@ -180,7 +182,7 @@ object APIHolder { /** * 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. + * Uses the anilist 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() @@ -189,7 +191,8 @@ object APIHolder { suspend fun getTracker( titles: List, types: Set?, - year: Int? + year: Int?, + lessAccurate: Boolean = false ): Tracker? { return try { require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" } @@ -197,30 +200,70 @@ object APIHolder { val mainTitle = titles[0] val search = trackerCache[mainTitle] - ?: app.get("https://api.consumet.org/meta/anilist/$mainTitle") - .parsedSafe()?.also { - trackerCache[mainTitle] = it - } ?: return null + ?: searchAnilist(mainTitle)?.also { + trackerCache[mainTitle] = it + } ?: return null - val res = search.results?.find { media -> - val matchingYears = year == null || media.releaseDate == year + val res = search.data?.page?.media?.find { media -> + val matchingYears = year == null || media.seasonYear == 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 + val matchingTypes = types?.any { it.name.equals(media.format, true) } == true + if(lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears } ?: return null - Tracker(res.malId, res.aniId, res.image, res.cover) + Tracker(res.idMal, res.id.toString(), res.coverImage?.extraLarge ?: res.coverImage?.large, res.bannerImage) } catch (t: Throwable) { logError(t) null } } + private suspend fun searchAnilist( + title: String?, + ): AniSearch? { + val query = """ + query ( + ${'$'}page: Int = 1 + ${'$'}search: String + ${'$'}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC] + ${'$'}type: MediaType + ) { + Page(page: ${'$'}page, perPage: 20) { + media( + search: ${'$'}search + sort: ${'$'}sort + type: ${'$'}type + ) { + id + idMal + title { romaji english } + coverImage { extraLarge large } + bannerImage + seasonYear + format + } + } + } + """.trimIndent().trim() + + val data = mapOf( + "query" to query, + "variables" to mapOf( + "search" to title, + "sort" to "SEARCH_MATCH", + "type" to "ANIME", + ) + ).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull()) + + return app.post("https://graphql.anilist.co", requestBody = data) + .parsedSafe() + } + fun Context.getApiSettings(): HashSet { //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) @@ -1730,30 +1773,42 @@ data class Tracker( val cover: String? = null, ) -data class Title( - @JsonProperty("romaji") val romaji: String? = null, - @JsonProperty("english") val english: String? = null, +data class AniSearch( + @JsonProperty("data") var data: Data? = Data() ) { - fun isMatchingTitles(title: String?): Boolean { - if (title == null) return false - return english.equals(title, true) || romaji.equals(title, true) + data class Data( + @JsonProperty("Page") var page: Page? = Page() + ) { + data class Page( + @JsonProperty("media") var media: ArrayList = arrayListOf() + ) { + data class Media( + @JsonProperty("title") var title: Title? = null, + @JsonProperty("id") var id: Int? = null, + @JsonProperty("idMal") var idMal: Int? = null, + @JsonProperty("seasonYear") var seasonYear: Int? = null, + @JsonProperty("format") var format: String? = null, + @JsonProperty("coverImage") var coverImage: CoverImage? = null, + @JsonProperty("bannerImage") var bannerImage: String? = null, + ) { + data class CoverImage( + @JsonProperty("extraLarge") var extraLarge: String? = null, + @JsonProperty("large") var large: String? = null, + ) + data class Title( + @JsonProperty("romaji") var romaji: String? = null, + @JsonProperty("english") var 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 **/ From 2baa75496ec16ba5e8cf2eba13641563bcf6f8fc Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Mon, 11 Sep 2023 18:13:42 +0000 Subject: [PATCH 225/570] Fix opensubtitles (#598) * Fix OpenSubtitles --- .../providers/OpenSubtitlesApi.kt | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) 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 3e372c2d..4030649d 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 @@ -2,12 +2,13 @@ package com.lagradost.cloudstream3.syncproviders.providers import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty -import com.google.common.collect.BiMap -import com.google.common.collect.HashBiMap -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.subtitles.AbstractSubApi import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities @@ -15,8 +16,8 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager import com.lagradost.cloudstream3.utils.AppUtils -import java.net.URLEncoder -import java.nio.charset.StandardCharsets +import okhttp3.Interceptor +import okhttp3.Response class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi { override val idPrefix = "opensubtitles" @@ -36,6 +37,23 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi var currentSession: SubtitleOAuthEntity? = null } + private val headerInterceptor = OpenSubtitleInterceptor() + + /** Automatically adds required api headers */ + private class OpenSubtitleInterceptor : Interceptor { + /** Required user agent! */ + private val userAgent = "Cloudstream3 v0.1" + override fun intercept(chain: Interceptor.Chain): Response { + return chain.proceed( + chain.request().newBuilder() + .removeHeader("user-agent") + .addHeader("user-agent", userAgent) + .addHeader("Api-Key", apiKey) + .build() + ) + } + } + private fun canDoRequest(): Boolean { return unixTimeMs > currentCoolDown } @@ -98,13 +116,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val response = app.post( url = "$host/login", headers = mapOf( - "Api-Key" to apiKey, - "Content-Type" to "application/json" + "Content-Type" to "application/json", ), data = mapOf( "username" to username, "password" to password - ) + ), + interceptor = headerInterceptor ) //Log.i(TAG, "Responsecode = ${response.code}") //Log.i(TAG, "Result => ${response.text}") @@ -149,11 +167,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi // "pt" to "pt-PT", // "pt" to "pt-BR" ) - private fun fixLanguage(language: String?) : String? { + + private fun fixLanguage(language: String?): String? { return languageExceptions[language] ?: language } + // O(n) but good enough, BiMap did not want to work properly - private fun fixLanguageReverse(language: String?) : String? { + private fun fixLanguageReverse(language: String?): String? { return languageExceptions.entries.firstOrNull { it.value == language }?.key ?: language } @@ -183,9 +203,9 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val req = app.get( url = searchQueryUrl, headers = mapOf( - Pair("Api-Key", apiKey), Pair("Content-Type", "application/json") - ) + ), + interceptor = headerInterceptor ) Log.i(TAG, "Search Req => ${req.text}") if (!req.isSuccessful) { @@ -207,7 +227,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi //Use any valid name/title in hierarchy val name = filename ?: featureDetails?.movieName ?: featureDetails?.title ?: featureDetails?.parentTitle ?: attr.release ?: query.query - val lang = fixLanguageReverse(attr.language)?: "" + val lang = fixLanguageReverse(attr.language) ?: "" val resEpNum = featureDetails?.episodeNumber ?: query.epNumber val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber val year = featureDetails?.year ?: query.year @@ -251,13 +271,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi "Authorization", "Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}" ), - Pair("Api-Key", apiKey), Pair("Content-Type", "application/json"), Pair("Accept", "*/*") ), data = mapOf( Pair("file_id", data.data) - ) + ), + interceptor = headerInterceptor ) Log.i(TAG, "Request result => (${req.code}) ${req.text}") //Log.i(TAG, "Request headers => ${req.headers}") From 7d6ba8c7a4feed2636ab7fd386d1c76ffa73f2de Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 11 Sep 2023 23:05:10 +0200 Subject: [PATCH 226/570] tv changes for better centering + bigger text + better contrast --- .../lagradost/cloudstream3/CommonActivity.kt | 37 ++++++++++++-- .../lagradost/cloudstream3/MainActivity.kt | 50 ++++++++++++++++--- .../ui/player/FullScreenPlayer.kt | 15 +----- .../ui/result/ResultTrailerPlayer.kt | 2 + .../main/res/layout/fragment_result_tv.xml | 40 +++++++++------ 5 files changed, 107 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 0bcd4152..a7d899b6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -7,9 +7,14 @@ import android.content.Context import android.content.pm.PackageManager import android.content.res.Resources import android.os.Build +import android.util.DisplayMetrics import android.util.Log -import android.view.* +import android.view.Gravity +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View import android.view.View.NO_ID +import android.view.ViewGroup import android.widget.TextView import android.widget.Toast import androidx.activity.ComponentActivity @@ -40,7 +45,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode import com.lagradost.cloudstream3.utils.UIHelper.toPx import org.schabi.newpipe.extractor.NewPipe import java.lang.ref.WeakReference -import java.util.* +import java.util.Locale +import kotlin.math.max +import kotlin.math.min enum class FocusDirection { Start, @@ -63,6 +70,19 @@ object CommonActivity { return (this as MainActivity?)?.mSessionManager?.currentCastSession } + val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics + + // screenWidth and screenHeight does always + // refer to the screen while in landscape mode + val screenWidth: Int + get() { + return max(displayMetrics.widthPixels, displayMetrics.heightPixels) + } + val screenHeight: Int + get() { + return min(displayMetrics.widthPixels, displayMetrics.heightPixels) + } + var canEnterPipMode: Boolean = false var canShowPipMode: Boolean = false @@ -328,6 +348,14 @@ object CommonActivity { currentLook = currentLook.parent as? View ?: break }*/ + private fun View.hasContent() : Boolean { + return isShown && when(this) { + //is RecyclerView -> this.childCount > 0 + is ViewGroup -> this.childCount > 0 + else -> true + } + } + /** skips the initial stage of searching for an id using the view, see getNextFocus for specification */ fun continueGetNextFocus( root: Any?, @@ -348,16 +376,17 @@ object CommonActivity { } ?: return null next = localLook(view, nextId) ?: next + val shown = next.hasContent() // if cant focus but visible then break and let android decide // the exception if is the view is a parent and has children that wants focus val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent -> parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0 } ?: false - if (!next.isFocusable && next.isShown && !hasChildrenThatWantsFocus) return null + if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null // if not shown then continue because we will "skip" over views to get to a replacement - if (!next.isShown) { + if (!shown) { // we don't want a while true loop, so we let android decide if we find a recursive view if (next == view) return null return getNextFocus(root, next, direction, depth + 1) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index fbad4fce..a07ae2c2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.content.res.Configuration +import android.graphics.Rect import android.net.Uri import android.os.Build import android.os.Bundle @@ -52,6 +53,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.snackbar.Snackbar +import com.google.common.collect.Comparators.min import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.APIHolder.apis @@ -64,13 +66,13 @@ import com.lagradost.cloudstream3.CommonActivity.loadThemes import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint +import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale import com.lagradost.cloudstream3.databinding.ActivityMainBinding import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding import com.lagradost.cloudstream3.mvvm.Resource -import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observeNullable @@ -832,6 +834,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { focusOutline.get()?.isVisible = false } } + /*private val scrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + current = current.copy(x = current.x + dx, y = current.y + dy) + setTargetPosition(current) + } + }*/ private fun setTargetPosition(target: FocusTarget) { focusOutline.get()?.apply { @@ -874,7 +883,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (!exactlyTheSame) { lastView?.removeOnLayoutChangeListener(layoutListener) lastView?.removeOnAttachStateChangeListener(attachListener) - (lastView?.parent as? RecyclerView)?.removeOnLayoutChangeListener(layoutListener) + (lastView?.parent as? RecyclerView)?.apply { + removeOnLayoutChangeListener(layoutListener) + //removeOnScrollListener(scrollListener) + } } val wasGone = focusOutline.isGone @@ -952,7 +964,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { focusOutline.isVisible = false } if (!exactlyTheSame) { - (newFocus.parent as? RecyclerView)?.addOnLayoutChangeListener(layoutListener) + (newFocus.parent as? RecyclerView)?.apply { + addOnLayoutChangeListener(layoutListener) + //addOnScrollListener(scrollListener) + } newFocus.addOnLayoutChangeListener(layoutListener) newFocus.addOnAttachStateChangeListener(attachListener) } @@ -970,8 +985,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { ) // if they are the same within then snap, aka scrolling - val deltaMin = 50.toPx - if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMin && (start.y - end.y).absoluteValue < deltaMin) { + val deltaMinX = min(end.width / 2, 60.toPx) + val deltaMinY = min(end.height / 2, 60.toPx) + if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMinX && (start.y - end.y).absoluteValue < deltaMinY) { animator?.cancel() last = start current = end @@ -1000,7 +1016,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // animate between a and b animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply { startDelay = 0 - duration = 100 + duration = 200 addUpdateListener { animation -> val animatedValue = animation.animatedValue as Float val target = FocusTarget.lerp(last, current, minOf(animatedValue, 1.0f)) @@ -1095,7 +1111,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> // println("refocus $oldFocus -> $newFocus") + try { + val r = Rect(0,0,0,0) + newFocus.getDrawingRect(r) + val x = r.centerX() + val y = r.centerY() + val dx = 0 //screenWidth / 2 + val dy = screenHeight / 2 + val r2 = Rect(x-dx,y-dy,x+dx,y+dy) + newFocus.requestRectangleOnScreen(r2, false) + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_ : Throwable) { } TvFocus.updateFocusView(newFocus) + /*var focus = newFocus + + while(focus != null) { + if(focus is ScrollingView && focus.canScrollVertically()) { + focus.scrollBy() + } + when(focus.parent) { + is View -> focus = newFocus + else -> break + } + }*/ } newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) 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 6dabb5b7..e698191d 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 @@ -38,6 +38,8 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener +import com.lagradost.cloudstream3.CommonActivity.screenHeight +import com.lagradost.cloudstream3.CommonActivity.screenWidth import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding @@ -126,19 +128,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected var useTrueSystemBrightness = true private val fullscreenNotch = true //TODO SETTING - protected val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics - - // screenWidth and screenHeight does always - // refer to the screen while in landscape mode - protected val screenWidth: Int - get() { - return max(displayMetrics.widthPixels, displayMetrics.heightPixels) - } - protected val screenHeight: Int - get() { - return min(displayMetrics.widthPixels, displayMetrics.heightPixels) - } - private var statusBarHeight: Int? = null private var navigationBarHeight: Int? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index 5208e4a5..c30e70e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -9,6 +9,8 @@ import android.view.ViewGroup import android.widget.FrameLayout import androidx.core.view.isGone import androidx.core.view.isVisible +import com.lagradost.cloudstream3.CommonActivity.screenHeight +import com.lagradost.cloudstream3.CommonActivity.screenWidth import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.PlayerEventSource diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 1fde999c..4d236d78 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -128,7 +128,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit - - @@ -411,7 +409,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:padding="5dp" android:requiresFadingEdge="vertical" android:textColor="?attr/textColor" - android:textSize="12sp" + android:textSize="16sp" tools:text="Ryan Quicksave Romano is an eccentric adventurer with a strange power: he can create a save-point in time and redo his life whenever he dies. Arriving in New Rome, the glitzy capital of sin of a rebuilding Europe, he finds the city torn between mega-corporations, sponsored heroes, superpowered criminals, and true monsters. It's a time of chaos, where potions can grant the power to rule the world and dangers lurk everywhere. " /> - + android:layout_height="match_parent"> + + + + Date: Thu, 14 Sep 2023 10:53:35 +0000 Subject: [PATCH 227/570] More robust player release (#601) --- .../ui/player/AbstractPlayerFragment.kt | 1 + .../cloudstream3/ui/player/CS3IPlayer.kt | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 4316bbc6..8388e58f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -517,6 +517,7 @@ abstract class AbstractPlayerFragment( canEnterPipMode = false mMediaSession?.release() mMediaSession = null + playerView?.player = null SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged keepScreenOn(false) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index fe4e3423..331cfb73 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -51,6 +51,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle @@ -85,6 +86,12 @@ const val toleranceAfterUs = 300_000L class CS3IPlayer : IPlayer { private var isPlaying = false private var exoPlayer: ExoPlayer? = null + set(value) { + // If the old value is not null then the player has not been properly released. + debugAssert({ field != null && value != null }, { "Previous player instance should be released!" }) + field = value + } + var cacheSize = 0L var simpleCacheSize = 0L var videoBufferMs = 0L @@ -682,13 +689,13 @@ class CS3IPlayer : IPlayer { metadataRendererOutput ).map { if (it is TextRenderer) { - currentTextRenderer = CustomTextRenderer( + val currentTextRenderer = CustomTextRenderer( subtitleOffset, textRendererOutput, eventHandler.looper, CustomSubtitleDecoderFactory() - ) - currentTextRenderer!! + ).also { this.currentTextRenderer = it } + currentTextRenderer } else it }.toTypedArray() } @@ -1323,7 +1330,7 @@ class CS3IPlayer : IPlayer { override fun reloadPlayer(context: Context) { Log.i(TAG, "reloadPlayer") - exoPlayer?.release() + releasePlayer(false) currentLink?.let { loadOnlinePlayer(context, it) } ?: currentDownloadedFile?.let { From 2bed79b1f18e7b7ae145377865f427a781197e11 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:53:54 +0700 Subject: [PATCH 228/570] Update Gofile.kt (#600) --- .../main/java/com/lagradost/cloudstream3/extractors/Gofile.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt index d76b0e11..eaf9c65f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt @@ -19,7 +19,7 @@ open class Gofile : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z]+)").find(url)?.groupValues?.get(1) + val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1) val token = app.get("$mainApi/createAccount").parsedSafe()?.data?.get("token") val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let { Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1) @@ -59,4 +59,4 @@ open class Gofile : ExtractorApi() { @JsonProperty("data") val data: Data? = null, ) -} \ No newline at end of file +} From 6957a8f95dc7144c5b6c454930950d533c276f95 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 14 Sep 2023 20:30:44 +0200 Subject: [PATCH 229/570] bump --- app/build.gradle.kts | 4 ++-- app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f52d6e5e..66ba16c6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,8 +58,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 59 - versionName = "4.1.8" + versionCode = 60 + versionName = "4.1.9" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 0175e0d0..5b674c4c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -179,6 +179,13 @@ object APIHolder { private var trackerCache: HashMap = hashMapOf() + /** backwards compatibility, use getTracker4 instead */ + suspend fun getTracker( + titles: List, + types: Set?, + year: Int?, + ): Tracker? = getTracker(titles, types, year, false) + /** * Get anime tracker information based on title, year and type. * Both titles are attempted to be matched with both Romaji and English title. @@ -192,7 +199,7 @@ object APIHolder { titles: List, types: Set?, year: Int?, - lessAccurate: Boolean = false + lessAccurate: Boolean ): Tracker? { return try { require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" } From 24977a8d628a3418051e9bbbead8d4a88f7df035 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Fri, 15 Sep 2023 20:47:59 +0000 Subject: [PATCH 230/570] Potential fix for crash loops (#608) --- .../com/lagradost/cloudstream3/MainActivity.kt | 12 ++++++++---- .../cloudstream3/plugins/PluginManager.kt | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a07ae2c2..82a52a2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1092,15 +1092,19 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { updateTv() // backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting? - try { + normalSafeApiCall { val appVer = BuildConfig.VERSION_NAME val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: "" if (appVer != lastAppAutoBackup) { setKey("VERSION_NAME", BuildConfig.VERSION_NAME) - backup() + normalSafeApiCall { + backup() + } + normalSafeApiCall { + // Recompile oat on new version + PluginManager.deleteAllOatFiles(this) + } } - } catch (t: Throwable) { - logError(t) } // just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH 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 87b0ba3b..5bb96ed1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -137,6 +137,20 @@ object PluginManager { } } + /** + * Deletes all generated oat files which will force Android to recompile the dex extensions. + * This might fix unrecoverable SIGSEGV exceptions when old oat files are loaded in a new app update. + */ + fun deleteAllOatFiles(context: Context) { + File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}").listFiles()?.forEach { repo -> + repo.listFiles { file -> file.name == "oat" && file.isDirectory }?.forEach { file -> + val success = file.deleteRecursively() + Log.i(TAG, "Deleted oat directory: ${file.absolutePath} Success=$success") + } + } + } + + fun getPluginsOnline(): Array { return getKey(PLUGINS_KEY) ?: emptyArray() } From 627c1bb223a3cc4b8a917807e91100211f30a859 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Fri, 15 Sep 2023 22:30:34 +0000 Subject: [PATCH 231/570] Many UI fixes (#606) * Lower targetSdk to get all installed packages * Update sdk version * Let's not be too radical * Many fixes * Revert targetSdk * Make account homepage persistent --- .../lagradost/cloudstream3/MainActivity.kt | 25 +- ...RecyclerView.kt => CustomRecyclerViews.kt} | 39 ++- .../cloudstream3/ui/home/HomeFragment.kt | 3 +- .../cloudstream3/ui/home/HomeViewModel.kt | 17 +- .../ui/result/ResultFragmentTv.kt | 4 +- .../ui/result/ResultViewModel2.kt | 3 +- .../ui/settings/SettingsProviders.kt | 4 +- .../ui/setup/SetupFragmentMedia.kt | 4 +- .../cloudstream3/utils/DataStoreHelper.kt | 33 ++- app/src/main/res/drawable/episodes_shadow.xml | 6 +- .../main/res/layout/fragment_result_tv.xml | 239 ++++++++++-------- 11 files changed, 239 insertions(+), 138 deletions(-) rename app/src/main/java/com/lagradost/cloudstream3/ui/{AutofitRecyclerView.kt => CustomRecyclerViews.kt} (79%) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 82a52a2a..263a40f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -128,6 +128,7 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.IOnBackPressed @@ -305,6 +306,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // kinda shitty solution, but cant com main->home otherwise for popups val bookmarksUpdatedEvent = Event() + /** + * Used by data store helper to fully reload home when switching accounts + */ + val reloadHomeEvent = Event() /** @@ -539,6 +544,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val isTrueTv = isTrueTvSettings() navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + + // Hide downloads on TV + navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv + navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv } } @@ -1116,16 +1125,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> // println("refocus $oldFocus -> $newFocus") try { - val r = Rect(0,0,0,0) + val r = Rect(0, 0, 0, 0) newFocus.getDrawingRect(r) val x = r.centerX() val y = r.centerY() val dx = 0 //screenWidth / 2 val dy = screenHeight / 2 - val r2 = Rect(x-dx,y-dy,x+dx,y+dy) + val r2 = Rect(x - dx, y - dy, x + dx, y + dy) newFocus.requestRectangleOnScreen(r2, false) - // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) - } catch (_ : Throwable) { } + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_: Throwable) { + } TvFocus.updateFocusView(newFocus) /*var focus = newFocus @@ -1186,7 +1196,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } else if (lastError == null) { ioSafe { - getKey(USER_SELECTED_HOMEPAGE_API)?.let { homeApi -> + DataStoreHelper.currentHomePage?.let { homeApi -> mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi)) } ?: run { mainPluginsLoadedEvent.invoke(false) @@ -1547,6 +1557,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { migrateResumeWatching() } + getKey(USER_SELECTED_HOMEPAGE_API)?.let { homepage -> + DataStoreHelper.currentHomePage = homepage + removeKey(USER_SELECTED_HOMEPAGE_API) + } + try { if (getKey(HAS_DONE_SETUP_KEY, false) != true) { navController.navigate(R.id.navigation_setup_language) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt similarity index 79% rename from app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt rename to app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt index 28ced48c..1a9549e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui import android.content.Context import android.util.AttributeSet import android.view.View +import androidx.core.view.children import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs @@ -70,8 +71,8 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : val orientation = this.orientation // fixes arabic by inverting left and right layout focus - val correctDirection = if(this.isLayoutRTL) { - when(direction) { + val correctDirection = if (this.isLayoutRTL) { + when (direction) { View.FOCUS_RIGHT -> View.FOCUS_LEFT View.FOCUS_LEFT -> View.FOCUS_RIGHT else -> direction @@ -83,12 +84,15 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : View.FOCUS_DOWN -> { return spanCount } + View.FOCUS_UP -> { return -spanCount } + View.FOCUS_RIGHT -> { return 1 } + View.FOCUS_LEFT -> { return -1 } @@ -98,12 +102,15 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : View.FOCUS_DOWN -> { return 1 } + View.FOCUS_UP -> { return -1 } + View.FOCUS_RIGHT -> { return spanCount } + View.FOCUS_LEFT -> { return -spanCount } @@ -155,4 +162,32 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att layoutManager = manager } +} + +/** + * Recyclerview wherein the max item width or height is set by the biggest view to prevent inconsistent view sizes. + */ +class MaxRecyclerView(ctx: Context, attrs: AttributeSet) : RecyclerView(ctx, attrs) { + private var biggestObserved: Int = 0 + private val orientation = LayoutManager.getProperties(context, attrs, 0, 0).orientation + private val isHorizontal = orientation == HORIZONTAL + private fun View.updateMaxSize() { + if (isHorizontal) { + this.minimumHeight = biggestObserved + } else { + this.minimumWidth = biggestObserved + } + } + + override fun onChildAttachedToWindow(child: View) { + child.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + val observed = if (isHorizontal) child.measuredHeight else child.measuredWidth + if (observed > biggestObserved) { + biggestObserved = observed + children.forEach { it.updateMaxSize() } + } else { + child.updateMaxSize() + } + super.onChildAttachedToWindow(child) + } } \ No newline at end of file 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 b84c619e..0797e9a0 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 @@ -69,7 +69,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import java.util.* @@ -669,7 +668,7 @@ class HomeFragment : Fragment() { } homeViewModel.reloadStored() - homeViewModel.loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false) + homeViewModel.loadAndCancel(DataStoreHelper.currentHomePage, false) //loadHomePage(false) // nice profile pic on homepage diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index b27223ec..13d34b59 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -49,7 +49,6 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.cloudstream3.utils.VideoDownloadHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -426,23 +425,29 @@ class HomeViewModel : ViewModel() { } private fun afterPluginsLoaded(forceReload: Boolean) { - loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), forceReload) + loadAndCancel(DataStoreHelper.currentHomePage, forceReload) } private fun afterMainPluginsLoaded(unused: Boolean = false) { - loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false) + loadAndCancel(DataStoreHelper.currentHomePage, false) + } + + private fun reloadHome(unused: Boolean = false) { + loadAndCancel(DataStoreHelper.currentHomePage, true) } init { MainActivity.bookmarksUpdatedEvent += ::bookmarksUpdated MainActivity.afterPluginsLoadedEvent += ::afterPluginsLoaded MainActivity.mainPluginsLoadedEvent += ::afterMainPluginsLoaded + MainActivity.reloadHomeEvent += ::reloadHome } override fun onCleared() { MainActivity.bookmarksUpdatedEvent -= ::bookmarksUpdated MainActivity.afterPluginsLoadedEvent -= ::afterPluginsLoaded MainActivity.mainPluginsLoadedEvent -= ::afterMainPluginsLoaded + MainActivity.reloadHomeEvent -= ::reloadHome super.onCleared() } @@ -495,7 +500,7 @@ class HomeViewModel : ViewModel() { val api = getApiFromNameNull(preferredApiName) if (preferredApiName == noneApi.name) { // just set to random - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) + if (fromUI) DataStoreHelper.currentHomePage = noneApi.name loadAndCancel(noneApi) } else if (preferredApiName == randomApi.name) { // randomize the api, if none exist like if not loaded or not installed @@ -506,7 +511,7 @@ class HomeViewModel : ViewModel() { } else { val apiRandom = validAPIs.random() loadAndCancel(apiRandom) - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) + if (fromUI) DataStoreHelper.currentHomePage = apiRandom.name } } else if (api == null) { // API is not found aka not loaded or removed, post the loading @@ -520,7 +525,7 @@ class HomeViewModel : ViewModel() { } } else { // if the api is found, then set it to it and save key - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, api.name) + if (fromUI) DataStoreHelper.currentHomePage = api.name loadAndCancel(api) } } 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 be3de52b..c40d995b 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 @@ -177,7 +177,7 @@ class ResultFragmentTv : Fragment() { isVisible = true } - this.animate().alpha(if (turnVisible) 1.0f else 0.0f).apply { + this.animate().alpha(if (turnVisible) 0.97f else 0.0f).apply { duration = 200 interpolator = DecelerateInterpolator() setListener(object : Animator.AnimatorListener { @@ -294,9 +294,9 @@ class ResultFragmentTv : Fragment() { toggleEpisodes(true) binding?.apply { val views = listOf( + resultDubSelection, resultSeasonSelection, resultRangeSelection, - resultDubSelection, resultEpisodes, resultPlayTrailer, ) 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 b398b54e..6acf476a 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 @@ -518,7 +518,8 @@ class ResultViewModel2 : ViewModel() { val episodeNumber = episodes[currentIndex].episode if (episodeNumber < currentMin) { currentMin = episodeNumber - } else if (episodeNumber > currentMax) { + } + if (episodeNumber > currentMax) { currentMax = episodeNumber } ++currentIndex diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 0bef5e9a..7e57fc5b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -14,7 +14,7 @@ import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard @@ -96,7 +96,7 @@ class SettingsProviders : PreferenceFragmentCompat() { this.getString(R.string.prefer_media_type_key), selectedList.map { it.toString() }.toMutableSet() ).apply() - removeKey(USER_SELECTED_HOMEPAGE_API) + DataStoreHelper.currentHomePage = null //(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index 6916cafe..f9197213 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -15,8 +15,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API class SetupFragmentMedia : Fragment() { @@ -77,7 +77,7 @@ class SetupFragmentMedia : Fragment() { .apply() // Regenerate set homepage - removeKey(USER_SELECTED_HOMEPAGE_API) + DataStoreHelper.currentHomePage = null } } 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 2eb2ab01..7bce1b6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -77,10 +77,28 @@ object DataStoreHelper { var selectedKeyIndex by PreferenceDelegate("$TAG/account_key_index", 0) val currentAccount: String get() = selectedKeyIndex.toString() - private fun setAccount(account: Account) { + /** + * Get or set the current account homepage. + * Setting this does not automatically reload the homepage. + */ + var currentHomePage: String? + get() = getKey("$currentAccount/$USER_SELECTED_HOMEPAGE_API") + set(value) { + val key = "$currentAccount/$USER_SELECTED_HOMEPAGE_API" + if (value == null) { + removeKey(key) + } else { + setKey(key, value) + } + } + + private fun setAccount(account: Account, refreshHomePage: Boolean) { selectedKeyIndex = account.keyIndex showToast(account.name) MainActivity.bookmarksUpdatedEvent(true) + if (refreshHomePage) { + MainActivity.reloadHomeEvent(true) + } } private fun editAccount(context: Context, account: Account, isNewAccount: Boolean) { @@ -112,7 +130,7 @@ object DataStoreHelper { accounts = currentAccounts.toTypedArray() // update UI - setAccount(getDefaultAccount(context)) + setAccount(getDefaultAccount(context), true) MainActivity.bookmarksUpdatedEvent(true) dialog?.dismissSafe() } @@ -161,8 +179,13 @@ object DataStoreHelper { currentAccounts.add(currentEditAccount) } + // Save the current homepage for new accounts + val currentHomePage = DataStoreHelper.currentHomePage + // set the new default account as well as add the key for the new account - setAccount(currentEditAccount) + setAccount(currentEditAccount, false) + DataStoreHelper.currentHomePage = currentHomePage + accounts = currentAccounts.toTypedArray() dialog.dismissSafe() @@ -204,7 +227,7 @@ object DataStoreHelper { ) binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter( selectCallBack = { account -> - setAccount(account) + setAccount(account, true) builder.dismissSafe() }, addAccountCallback = { @@ -353,7 +376,7 @@ object DataStoreHelper { removeKeys(folder2) } - fun deleteBookmarkedData(id : Int?) { + fun deleteBookmarkedData(id: Int?) { if (id == null) return removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString()) removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) diff --git a/app/src/main/res/drawable/episodes_shadow.xml b/app/src/main/res/drawable/episodes_shadow.xml index b4cdd382..a77cbf25 100644 --- a/app/src/main/res/drawable/episodes_shadow.xml +++ b/app/src/main/res/drawable/episodes_shadow.xml @@ -1,6 +1,8 @@ + android:centerColor="?attr/primaryBlackBackground" + android:centerX="0.2" + android:endColor="?attr/primaryBlackBackground" + android:startColor="@color/transparent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 4d236d78..a143fbda 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -535,129 +535,150 @@ https://developer.android.com/design/ui/tv/samples/jet-fit - - - - - - - - - - - - - + tools:visibility="visible"> - + - style="@style/Widget.AppCompat.ProgressBar" - android:layout_gravity="center" - android:layout_width="50dp" - android:layout_height="50dp" />--> + + - - - - + android:focusableInTouchMode="false" + android:importantForAccessibility="no" + android:src="@drawable/episodes_shadow" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/shadow_space_2" + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + - Date: Sun, 17 Sep 2023 20:35:01 +0200 Subject: [PATCH 232/570] fix --- .../ui/player/PlayerGeneratorViewModel.kt | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 42659f8d..3179cb9f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.ui.result.ResultEpisode @@ -15,6 +16,7 @@ import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorUri import kotlinx.coroutines.Job +import kotlinx.coroutines.launch class PlayerGeneratorViewModel : ViewModel() { companion object { @@ -38,6 +40,11 @@ class PlayerGeneratorViewModel : ViewModel() { private val _currentSubtitleYear = MutableLiveData(null) val currentSubtitleYear: LiveData = _currentSubtitleYear + /** + * Save the Episode ID to prevent starting multiple link loading Jobs when preloading links. + */ + private var currentLoadingEpisodeId: Int? = null + fun setSubtitleYear(year: Int?) { _currentSubtitleYear.postValue(year) } @@ -72,18 +79,32 @@ class PlayerGeneratorViewModel : ViewModel() { } fun preLoadNextLinks() { + val id = getId() + // Do not preload if already loading + if (id == currentLoadingEpisodeId) return + Log.i(TAG, "preLoadNextLinks") currentJob?.cancel() - currentJob = viewModelScope.launchSafe { - if (generator?.hasCache == true && generator?.hasNext() == true) { - safeApiCall { - generator?.generateLinks( - type = LoadType.InApp, - clearCache = false, - callback = {}, - subtitleCallback = {}, - offset = 1 - ) + currentLoadingEpisodeId = id + + currentJob = viewModelScope.launch { + try { + if (generator?.hasCache == true && generator?.hasNext() == true) { + safeApiCall { + generator?.generateLinks( + type = LoadType.InApp, + clearCache = false, + callback = {}, + subtitleCallback = {}, + offset = 1 + ) + } + } + } catch (t: Throwable) { + logError(t) + } finally { + if (currentLoadingEpisodeId == id) { + currentLoadingEpisodeId = null } } } @@ -162,14 +183,14 @@ class PlayerGeneratorViewModel : ViewModel() { // load more data _loadingLinks.postValue(Resource.Loading()) val loadingState = safeApiCall { - generator?.generateLinks(type = type,clearCache = clearCache, callback = { + generator?.generateLinks(type = type, clearCache = clearCache, callback = { currentLinks.add(it) // Clone to prevent ConcurrentModificationException normalSafeApiCall { // Extra normalSafeApiCall since .toSet() iterates. _currentLinks.postValue(currentLinks.toSet()) } - }, subtitleCallback = { + }, subtitleCallback = { currentSubs.add(it) normalSafeApiCall { _currentSubs.postValue(currentSubs.toSet()) From 0d2a19b350021427922d7419f485ef1c46550366 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 17 Sep 2023 20:38:59 +0200 Subject: [PATCH 233/570] bump --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 66ba16c6..ca99894d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,8 +58,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 60 - versionName = "4.1.9" + versionCode = 61 + versionName = "4.1.10" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") From 2ae5b6cefbaa85fffaa12838a87d5623e15e73ca Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 18 Sep 2023 22:28:26 +0200 Subject: [PATCH 234/570] fixed the fucking updater :skull: --- app/build.gradle.kts | 4 ++-- .../lagradost/cloudstream3/utils/InAppUpdater.kt | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ca99894d..0f815f8b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,8 +58,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 61 - versionName = "4.1.10" + versionCode = 62 + versionName = "4.2.0" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index b2c4aa5c..0dce0b2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -109,18 +109,19 @@ class InAppUpdater { releases.sortedWith(compareBy { versionRegex.find(it.name)?.groupValues?.get(2) }).toList().lastOrNull()*/ - val found = + val foundList = response.filter { rel -> !rel.prerelease }.sortedWith(compareBy { release -> - release.assets.filter { it.content_type == "application/vnd.android.package-archive" } - .getOrNull(0)?.name?.let { it1 -> + release.assets.firstOrNull { it.content_type == "application/vnd.android.package-archive" }?.name?.let { it1 -> versionRegex.find( it1 - )?.groupValues?.get(2) + )?.groupValues?.let { + it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() + } } - }).toList().lastOrNull() - + }).toList() + val found = foundList.lastOrNull() val foundAsset = found?.assets?.getOrNull(0) val currentVersion = packageName?.let { packageManager.getPackageInfo( From 15333123cd298357baf85fa2c5f044ada60481d6 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Mon, 18 Sep 2023 21:22:39 +0000 Subject: [PATCH 235/570] TV UI fixes (#612) * TV UI fixes --- .../cloudstream3/ui/result/ActorAdaptor.kt | 26 +++++++++++--- .../ui/result/ResultFragmentTv.kt | 35 +++++++++++++------ .../ui/settings/SettingsFragment.kt | 5 +++ .../main/res/layout/fragment_search_tv.xml | 2 +- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 531cb5d2..7b743388 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -3,6 +3,8 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.IdRes +import androidx.annotation.LayoutRes import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -12,7 +14,10 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.CastItemBinding import com.lagradost.cloudstream3.utils.UIHelper.setImage -class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerView.Adapter() { +class ActorAdaptor( + private var nextFocusUpId: Int? = null, + private val focusCallback: (View?) -> Unit = {} +) : RecyclerView.Adapter() { data class ActorMetaData( var isInverted: Boolean, val actor: ActorData, @@ -22,7 +27,8 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return CardViewHolder( - CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), focusCallback + CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), + focusCallback ) } @@ -64,10 +70,10 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV } } - private class CardViewHolder + private inner class CardViewHolder constructor( val binding: CastItemBinding, - private val focusCallback : (View?) -> Unit = {} + private val focusCallback: (View?) -> Unit = {} ) : RecyclerView.ViewHolder(binding.root) { @@ -78,8 +84,18 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV Pair(actor.voiceActor?.image, actor.actor.image) } + // Fix tv focus escaping the recyclerview + if (position == 0) { + itemView.nextFocusLeftId = R.id.result_cast_items + } else if ((position - 1) == itemCount) { + itemView.nextFocusRightId = R.id.result_cast_items + } + nextFocusUpId?.let { + itemView.nextFocusUpId = it + } + itemView.setOnFocusChangeListener { v, hasFocus -> - if(hasFocus) { + if (hasFocus) { focusCallback(v) } } 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 c40d995b..4503cb88 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 @@ -114,10 +114,20 @@ class ResultFragmentTv : Fragment() { } } - private fun hasNoFocus(): Boolean { - val focus = activity?.currentFocus - if (focus == null || !focus.isVisible) return true - return focus == binding?.resultRoot +// private fun hasNoFocus(): Boolean { +// val focus = activity?.currentFocus +// if (focus == null || !focus.isVisible) return true +// return focus == binding?.resultRoot +// } + + /** + * Force focus any play button. + * Note that this will steal any focus if the episode loading is too slow (unlikely). + */ + private fun focusPlayButton() { + binding?.resultPlayMovie?.requestFocus() + binding?.resultPlaySeries?.requestFocus() + binding?.resultResumeSeries?.requestFocus() } private fun setRecommendations(rec: List?, validApiName: String?) { @@ -413,7 +423,13 @@ class ResultFragmentTv : Fragment() { setHorizontal() } - resultCastItems.adapter = ActorAdaptor { + val aboveCast = listOf( + binding?.resultEpisodesShow, + binding?.resultBookmarkButton, + ).firstOrNull { + it?.isVisible == true + } + resultCastItems.adapter = ActorAdaptor(aboveCast?.id) { toggleEpisodes(false) } } @@ -454,9 +470,7 @@ class ResultFragmentTv : Fragment() { resultPlaySeries.isVisible = false resultResumeSeries.isVisible = true - if (hasNoFocus()) { - resultResumeSeries.requestFocus() - } + focusPlayButton() resultResumeSeries.text = if (resume.isMovie) context?.getString(R.string.play_movie_button) else context?.getNameFull( @@ -539,9 +553,7 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } - if (hasNoFocus()) { - resultPlayMovie.requestFocus() - } + focusPlayButton() } } } @@ -663,6 +675,7 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } + focusPlayButton() } /* diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index e53fa91a..4895b0d2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -205,6 +205,11 @@ class SettingsFragment : Fragment() { } } } + + // Default focus on TV + if (isTrueTv) { + settingsGeneral.requestFocus() + } } } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search_tv.xml b/app/src/main/res/layout/fragment_search_tv.xml index 4c4af404..5fec8c6a 100644 --- a/app/src/main/res/layout/fragment_search_tv.xml +++ b/app/src/main/res/layout/fragment_search_tv.xml @@ -99,7 +99,7 @@ android:id="@+id/search_autofit_results" android:layout_width="match_parent" android:layout_height="match_parent" - + android:layout_marginStart="@dimen/navbar_width" android:background="?attr/primaryBlackBackground" android:clipToPadding="false" android:descendantFocusability="afterDescendants" From 527a6388a96a4fa961e1544e6b54acdf6a6f5ee5 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 21 Sep 2023 22:46:23 +0200 Subject: [PATCH 236/570] small fix --- .../lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 4503cb88..7c784a43 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 @@ -648,6 +648,9 @@ class ResultFragmentTv : Fragment() { .show() } } + + // Used to request focus the first time the episodes are loaded. + var hasLoadedEpisodesOnce = false observeNullable(viewModel.episodes) { episodes -> binding?.apply { resultEpisodes.isVisible = episodes is Resource.Success @@ -675,7 +678,10 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } - focusPlayButton() + if (!hasLoadedEpisodesOnce) { + hasLoadedEpisodesOnce = true + focusPlayButton() + } } /* From d4fff7cee67d8f7b5e610caf98c5d1816cabac83 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 21 Sep 2023 20:50:31 +0000 Subject: [PATCH 237/570] Add homepage search on TV (#618) * Add search button on homepage for TV --- .../lagradost/cloudstream3/MainActivity.kt | 1 + .../ui/home/HomeParentItemAdapterPreview.kt | 5 ++++ .../ui/quicksearch/QuickSearchFragment.kt | 5 ++++ app/src/main/res/layout/fragment_home.xml | 16 +++++++++++++ .../main/res/layout/fragment_home_head_tv.xml | 20 ++++++++++++++-- app/src/main/res/layout/fragment_home_tv.xml | 24 +++++++++++++++++-- 6 files changed, 67 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 263a40f0..627893c3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -497,6 +497,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { R.id.navigation_results_phone, R.id.navigation_results_tv, R.id.navigation_player, + R.id.navigation_quick_search, ).contains(destination.id) binding?.navHostFragment?.apply { 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 1d8e1399..d7956f39 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 @@ -461,6 +461,11 @@ class HomeParentItemAdapterPreview( } } + homePreviewSearchButton.setOnClickListener { _ -> + // Open blank screen. + viewModel.queryTextSubmit("") + } + // This makes the hidden next buttons only available when on the info button // Otherwise you might be able to go to the next item without being at the info button homePreviewInfoBtt.setOnFocusChangeListener { _, hasFocus -> 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 89a09ae2..53c7c2fa 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 @@ -33,6 +33,7 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchViewModel +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar @@ -269,6 +270,10 @@ class QuickSearchFragment : Fragment() { activity?.popCurrentPage() } + if (isTrueTvSettings()) { + binding?.quickSearch?.requestFocus() + } + arguments?.getString(AUTOSEARCH_KEY)?.let { binding?.quickSearch?.setQuery(it, true) arguments?.remove(AUTOSEARCH_KEY) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index ac660ccd..36cb5f42 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -114,6 +114,22 @@ android:nextFocusRight="@id/home_switch_account" /> + + + + + android:nextFocusRight="@id/home_preview_search_button" + android:nextFocusDown="@id/home_preview_play_btt" /> + + Date: Mon, 25 Sep 2023 09:40:58 +0000 Subject: [PATCH 238/570] Fix episode layout when using RTL language (#631) * Fix episode layout when using RTL language --- .../lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 6 ++---- app/src/main/res/layout/fragment_result_tv.xml | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) 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 7c784a43..5e4869cc 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 @@ -216,11 +216,9 @@ class ResultFragmentTv : Fragment() { episodesShadow.fade(show) episodeHolderTv.fade(show) if (episodesShadow.isRtl()) { - episodesShadow.scaleX = -1.0f - episodesShadow.scaleY = -1.0f + episodesShadowBackground.scaleX = -1f } else { - episodesShadow.scaleX = 1.0f - episodesShadow.scaleY = 1.0f + episodesShadowBackground.scaleX = 1f } } } diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index a143fbda..feaf6fbc 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -572,6 +572,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit app:layout_constraintTop_toTopOf="@+id/shadow_space_1" /> Date: Mon, 25 Sep 2023 17:18:05 +0530 Subject: [PATCH 239/570] refactor: speedostream and newpipe, tmdb update (#632) * migrate speedostream (yomovies) * update tmdb and newpipe --- app/build.gradle.kts | 14 ++++++-------- .../{SpeedoStream.kt => Minoplres.kt} | 17 ++++------------- .../cloudstream3/utils/ExtractorApi.kt | 8 ++------ 3 files changed, 12 insertions(+), 27 deletions(-) rename app/src/main/java/com/lagradost/cloudstream3/extractors/{SpeedoStream.kt => Minoplres.kt} (74%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0f815f8b..f13095fb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -171,7 +171,7 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test:core") - //implementation("io.karn:khttp-android:0.1.2") //okhttp instead + // implementation("io.karn:khttp-android:0.1.2") //okhttp instead // implementation("org.jsoup:jsoup:1.13.1") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") @@ -199,8 +199,6 @@ dependencies { // Custom ffmpeg extension for audio codecs implementation("com.github.recloudstream:media-ffmpeg:1.1.0") - //implementation("com.google.android.exoplayer:extension-leanback:2.14.0") - // Bug reports implementation("ch.acra:acra-core:5.11.0") implementation("ch.acra:acra-toast:5.11.0") @@ -214,13 +212,13 @@ dependencies { // subtitle color picker implementation("com.jaredrummler:colorpicker:1.1.0") - //run JS + // run JS // do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not // available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown implementation("org.mozilla:rhino:1.7.13") // TorrentStream - //implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0") + // implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0") // Downloading implementation("androidx.work:work-runtime:2.8.1") @@ -236,11 +234,11 @@ dependencies { implementation("com.github.LagradOst:SafeFile:0.0.5") // API because cba maintaining it myself - implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") + implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0") implementation("com.github.discord:OverlappingPanels:0.1.5") // debugImplementation because LeakCanary should only run in debug builds. - //debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") + // debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") // for shimmer when loading implementation("com.facebook.shimmer:shimmer:0.5.0") @@ -252,7 +250,7 @@ dependencies { // newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev // this should be updated frequently to avoid trailer fu*kery - implementation("com.github.teamnewpipe:NewPipeExtractor:1f08d28") + implementation("com.github.teamnewpipe:NewPipeExtractor:917554a") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6") // Library/extensions searching with Levenshtein distance diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt similarity index 74% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt index 213ecdf3..702501a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt @@ -7,21 +7,12 @@ import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper -class SpeedoStream2 : SpeedoStream() { - override val mainUrl = "https://speedostream.mom" -} +open class Minoplres : ExtractorApi() { -class SpeedoStream1 : SpeedoStream() { - override val mainUrl = "https://speedostream.pm" -} - -open class SpeedoStream : ExtractorApi() { - override val name = "SpeedoStream" - override val mainUrl = "https://speedostream.bond" + override val name = "Minoplres" // formerly SpeedoStream override val requiresReferer = true - - // .bond, .pm, .mom redirect to .bond - private val hostUrl = "https://speedostream.bond" + override val mainUrl = "https://minoplres.xyz" // formerly speedostream.bond + private val hostUrl = "https://minoplres.xyz" override suspend fun getUrl(url: String, referer: String?): List { val sources = mutableListOf() 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 5edff7a1..9db62dc8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -82,6 +82,7 @@ import com.lagradost.cloudstream3.extractors.Maxstream import com.lagradost.cloudstream3.extractors.Mcloud import com.lagradost.cloudstream3.extractors.Megacloud import com.lagradost.cloudstream3.extractors.Meownime +import com.lagradost.cloudstream3.extractors.Minoplres import com.lagradost.cloudstream3.extractors.MixDrop import com.lagradost.cloudstream3.extractors.MixDropBz import com.lagradost.cloudstream3.extractors.MixDropCh @@ -118,9 +119,6 @@ import com.lagradost.cloudstream3.extractors.Sbthe import com.lagradost.cloudstream3.extractors.Sendvid import com.lagradost.cloudstream3.extractors.ShaveTape import com.lagradost.cloudstream3.extractors.Solidfiles -import com.lagradost.cloudstream3.extractors.SpeedoStream -import com.lagradost.cloudstream3.extractors.SpeedoStream1 -import com.lagradost.cloudstream3.extractors.SpeedoStream2 import com.lagradost.cloudstream3.extractors.Ssbstream import com.lagradost.cloudstream3.extractors.StreamM4u import com.lagradost.cloudstream3.extractors.StreamSB @@ -748,9 +746,7 @@ val extractorApis: MutableList = arrayListOf( Vido(), Linkbox(), Acefile(), - SpeedoStream(), - SpeedoStream1(), - SpeedoStream2(), + Minoplres(), // formerly SpeedoStream Zorofile(), Embedgram(), Mvidoo(), From 74060e7da32e0f8af23051149b383c92a6b1efb2 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Mon, 25 Sep 2023 18:48:35 +0700 Subject: [PATCH 240/570] Chillx: fix key (#628) --- .../main/java/com/lagradost/cloudstream3/extractors/Chillx.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index bcf8848c..a9fafc39 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -29,7 +29,7 @@ open class Chillx : ExtractorApi() { override val requiresReferer = true companion object { - private const val KEY = "m4H6D9%0\$N&F6rQ&" + private const val KEY = "eN0^>\$^#M08uFv%c" } override suspend fun getUrl( From 0351053d809ef9e86c5faabf29702c2c50d9409c Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Mon, 25 Sep 2023 22:57:18 +0200 Subject: [PATCH 241/570] Readded downloads to tv --- app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 627893c3..d5187029 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -547,8 +547,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv // Hide downloads on TV - navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv - navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv + //navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv + //navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv } } From 194678c419343cc8c441bf7946906b039d6e47a6 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 28 Sep 2023 13:21:03 +0300 Subject: [PATCH 242/570] Player Source & Subs navigation change (#633) --- app/src/main/res/layout/player_select_source_and_subs.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/player_select_source_and_subs.xml b/app/src/main/res/layout/player_select_source_and_subs.xml index 7351a41f..f5eab1e7 100644 --- a/app/src/main/res/layout/player_select_source_and_subs.xml +++ b/app/src/main/res/layout/player_select_source_and_subs.xml @@ -66,8 +66,8 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_forced" - android:nextFocusLeft="@id/sort_subtitles" - android:nextFocusRight="@id/apply_btt" + android:nextFocusRight="@id/sort_subtitles" + android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" tools:layout_height="100dp" tools:listitem="@layout/sort_bottom_single_choice" /> @@ -140,7 +140,8 @@ android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_forced" android:nextFocusLeft="@id/sort_providers" - android:nextFocusRight="@id/cancel_btt" + android:nextFocusRight="@id/apply_btt" + android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" tools:layout_height="200dp" tools:listfooter="@layout/sort_bottom_footer_add_choice" From 16c2290090a1becb4edab44bf85a54837d6007f0 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 28 Sep 2023 13:22:51 +0300 Subject: [PATCH 243/570] Amazon FireTV focus fixes (#635) * Fix quick search button focus * Switch profile button focus * Cast & Recommendations focus * Player: Profiles settings focus * Player: Subtitles encoding settings focus * profile selection: card item focus * Search history item selectable & deleteable * Search: search filter button next focus fix --- app/src/main/res/layout/cast_item.xml | 2 +- app/src/main/res/layout/fragment_home_head_tv.xml | 2 ++ app/src/main/res/layout/fragment_search_tv.xml | 2 +- app/src/main/res/layout/player_quality_profile_item.xml | 1 + app/src/main/res/layout/player_select_source_and_subs.xml | 4 ++++ app/src/main/res/layout/search_history_item.xml | 4 ++-- 6 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/cast_item.xml b/app/src/main/res/layout/cast_item.xml index 54df59a8..c09cecfa 100644 --- a/app/src/main/res/layout/cast_item.xml +++ b/app/src/main/res/layout/cast_item.xml @@ -34,7 +34,7 @@ android:id="@+id/actor_image" android:layout_width="match_parent" - + android:focusable="true" android:layout_height="match_parent" android:contentDescription="@string/episode_poster_img_des" android:scaleType="centerCrop" diff --git a/app/src/main/res/layout/fragment_home_head_tv.xml b/app/src/main/res/layout/fragment_home_head_tv.xml index f9ea6974..05cb3a41 100644 --- a/app/src/main/res/layout/fragment_home_head_tv.xml +++ b/app/src/main/res/layout/fragment_home_head_tv.xml @@ -55,6 +55,7 @@ android:layout_gravity="end" android:background="@drawable/player_button_tv_attr_no_bg" android:contentDescription="@string/search" + android:focusable="true" android:nextFocusLeft="@id/home_preview_change_api" android:nextFocusRight="@id/home_preview_switch_account" android:nextFocusDown="@id/home_preview_info_btt" @@ -70,6 +71,7 @@ android:layout_gravity="end" android:background="@drawable/player_button_tv_attr_no_bg" android:contentDescription="@string/account" + android:focusable="true" android:nextFocusLeft="@id/home_preview_search_button" android:nextFocusRight="@id/home_preview_switch_account" android:nextFocusDown="@id/home_preview_info_btt" diff --git a/app/src/main/res/layout/fragment_search_tv.xml b/app/src/main/res/layout/fragment_search_tv.xml index 5fec8c6a..b3f88cd2 100644 --- a/app/src/main/res/layout/fragment_search_tv.xml +++ b/app/src/main/res/layout/fragment_search_tv.xml @@ -84,7 +84,7 @@ android:nextFocusLeft="@id/main_search" android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" - android:nextFocusDown="@id/search_autofit_results" + android:nextFocusDown="@id/tvtypes_chips_scroll" android:src="@drawable/ic_baseline_tune_24" app:tint="?attr/textColor" /> diff --git a/app/src/main/res/layout/player_quality_profile_item.xml b/app/src/main/res/layout/player_quality_profile_item.xml index 0eab2407..5178a12f 100644 --- a/app/src/main/res/layout/player_quality_profile_item.xml +++ b/app/src/main/res/layout/player_quality_profile_item.xml @@ -10,6 +10,7 @@ android:id="@+id/card_view" android:layout_width="0dp" android:layout_height="0dp" + android:focusable="true" app:layout_constraintDimensionRatio="1" android:layout_marginStart="10dp" android:animateLayoutChanges="true" diff --git a/app/src/main/res/layout/player_select_source_and_subs.xml b/app/src/main/res/layout/player_select_source_and_subs.xml index f5eab1e7..6bf8006b 100644 --- a/app/src/main/res/layout/player_select_source_and_subs.xml +++ b/app/src/main/res/layout/player_select_source_and_subs.xml @@ -27,6 +27,7 @@ android:layout_height="wrap_content" android:layout_rowWeight="1" android:layout_marginTop="10dp" + android:focusable="true" android:foreground="@drawable/outline_drawable_forced" android:gravity="center_vertical" android:orientation="horizontal"> @@ -66,6 +67,7 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_forced" + android:nextFocusUp="@id/profiles_click_settings" android:nextFocusRight="@id/sort_subtitles" android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" @@ -94,6 +96,7 @@ android:layout_height="wrap_content" android:layout_rowWeight="1" android:layout_marginTop="10dp" + android:focusable="true" android:foreground="@drawable/outline_drawable_forced" android:orientation="horizontal" android:paddingTop="10dp" @@ -139,6 +142,7 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_forced" + android:nextFocusUp="@id/subtitles_click_settings" android:nextFocusLeft="@id/sort_providers" android:nextFocusRight="@id/apply_btt" android:nextFocusDown="@id/apply_btt" diff --git a/app/src/main/res/layout/search_history_item.xml b/app/src/main/res/layout/search_history_item.xml index 3e9ee833..4c50d0c0 100644 --- a/app/src/main/res/layout/search_history_item.xml +++ b/app/src/main/res/layout/search_history_item.xml @@ -6,7 +6,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/outline_drawable_less" - + android:focusable="true" android:nextFocusRight="@id/home_history_remove" android:orientation="horizontal"> diff --git a/app/src/main/res/layout/player_select_source_priority.xml b/app/src/main/res/layout/player_select_source_priority.xml index 86a8a756..2af3c339 100644 --- a/app/src/main/res/layout/player_select_source_priority.xml +++ b/app/src/main/res/layout/player_select_source_priority.xml @@ -42,8 +42,8 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_less" - android:nextFocusLeft="@id/sort_subtitles" - android:nextFocusRight="@id/apply_btt" + android:nextFocusRight="@id/sort_subtitles" + android:nextFocusDown="@id/profile_text_editable" android:requiresFadingEdge="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:layout_height="100dp" @@ -92,6 +92,8 @@ android:layout_height="50dp" android:background="?attr/selectableItemBackgroundBorderless" android:padding="12dp" + android:focusable="true" + android:nextFocusLeft="@id/sort_sources" android:src="@drawable/baseline_help_outline_24" android:contentDescription="@string/help" /> @@ -115,8 +117,10 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_less" - android:nextFocusLeft="@id/sort_providers" - android:nextFocusRight="@id/cancel_btt" + android:nextFocusLeft="@id/sort_sources" + android:nextFocusRight="@id/apply_btt" + android:nextFocusUp="@id/help_btt" + android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:layout_height="200dp" diff --git a/app/src/main/res/layout/subtitle_offset.xml b/app/src/main/res/layout/subtitle_offset.xml index a98fafef..c17c5eff 100644 --- a/app/src/main/res/layout/subtitle_offset.xml +++ b/app/src/main/res/layout/subtitle_offset.xml @@ -35,6 +35,7 @@ android:layout_gravity="center" android:layout_weight="1" android:background="?android:attr/selectableItemBackgroundBorderless" + android:focusable="true" android:nextFocusRight="@id/subtitle_offset_subtract" android:padding="10dp" android:src="@drawable/ic_baseline_keyboard_arrow_left_24" @@ -48,6 +49,7 @@ android:layout_gravity="center" android:layout_weight="1" android:background="?android:attr/selectableItemBackgroundBorderless" + android:focusable="true" android:nextFocusLeft="@id/subtitle_offset_subtract_more" android:padding="10dp" android:src="@drawable/baseline_remove_24" @@ -70,6 +72,7 @@ android:layout_gravity="center" android:layout_weight="1" android:background="?android:attr/selectableItemBackgroundBorderless" + android:focusable="true" android:nextFocusRight="@id/subtitle_offset_add_more" android:padding="10dp" android:src="@drawable/ic_baseline_add_24" @@ -83,7 +86,9 @@ android:layout_gravity="center" android:layout_weight="1" android:background="?android:attr/selectableItemBackgroundBorderless" + android:focusable="true" android:nextFocusLeft="@id/subtitle_offset_add" + android:nextFocusDown="@id/apply_btt" android:padding="10dp" android:src="@drawable/ic_baseline_keyboard_arrow_right_24" app:tint="?attr/white" diff --git a/app/src/main/res/layout/who_is_watching_account.xml b/app/src/main/res/layout/who_is_watching_account.xml index afa1a2a7..4970d004 100644 --- a/app/src/main/res/layout/who_is_watching_account.xml +++ b/app/src/main/res/layout/who_is_watching_account.xml @@ -11,6 +11,7 @@ android:foreground="?attr/selectableItemBackgroundBorderless" app:cardCornerRadius="@dimen/rounded_image_radius" android:layout_margin="5dp" + android:focusable="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="1" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/who_is_watching_account_add.xml b/app/src/main/res/layout/who_is_watching_account_add.xml index ed67e144..91c7e419 100644 --- a/app/src/main/res/layout/who_is_watching_account_add.xml +++ b/app/src/main/res/layout/who_is_watching_account_add.xml @@ -11,6 +11,7 @@ android:foreground="?attr/selectableItemBackgroundBorderless" app:cardCornerRadius="@dimen/rounded_image_radius" android:layout_margin="5dp" + android:focusable="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="1" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/who_is_watching_account_edit.xml b/app/src/main/res/layout/who_is_watching_account_edit.xml index 74553517..cec37a4f 100644 --- a/app/src/main/res/layout/who_is_watching_account_edit.xml +++ b/app/src/main/res/layout/who_is_watching_account_edit.xml @@ -88,6 +88,7 @@ android:layout_width="match_parent" android:layout_height="60dp" android:layout_gravity="center" + android:focusable="true" android:contentDescription="@string/preview_background_img_des" android:scaleType="centerCrop" android:src="@drawable/profile_bg_blue" /> From bd05a67f260263f1e4d49580cd40fc587b354fd0 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 2 Oct 2023 17:44:06 +0200 Subject: [PATCH 245/570] preview seekbar --- app/build.gradle.kts | 2 + .../ui/player/AbstractPlayerFragment.kt | 42 ++++- .../cloudstream3/ui/player/CS3IPlayer.kt | 53 ++++++- .../cloudstream3/ui/player/GeneratorPlayer.kt | 1 + .../cloudstream3/ui/player/IPlayer.kt | 7 +- .../ui/player/PreviewGenerator.kt | 147 ++++++++++++++++++ .../ui/result/ResultFragmentPhone.kt | 3 +- app/src/main/res/drawable/video_frame.xml | 10 ++ .../main/res/layout/player_custom_layout.xml | 63 ++++++-- .../res/layout/player_custom_layout_tv.xml | 62 ++++++-- .../main/res/layout/trailer_custom_layout.xml | 78 +++++++--- app/src/main/res/values/dimens.xml | 2 +- 12 files changed, 413 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt create mode 100644 app/src/main/res/drawable/video_frame.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f13095fb..9f484c48 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -258,6 +258,8 @@ dependencies { // color palette for images -> colors implementation("androidx.palette:palette-ktx:1.0.0") + // seekbar https://github.com/rubensousa/PreviewSeekBar + implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") } tasks.register("androidSourcesJar", Jar::class) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 8388e58f..862504a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -18,10 +18,9 @@ import android.widget.ProgressBar import android.widget.Toast import androidx.annotation.LayoutRes import androidx.annotation.StringRes +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.preference.PreferenceManager -import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.media3.common.PlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession @@ -30,6 +29,10 @@ import androidx.media3.ui.DefaultTimeBar import androidx.media3.ui.PlayerView import androidx.media3.ui.SubtitleView import androidx.media3.ui.TimeBar +import androidx.preference.PreferenceManager +import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat +import com.github.rubensousa.previewseekbar.PreviewBar +import com.github.rubensousa.previewseekbar.media3.PreviewTimeBar import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode @@ -454,6 +457,41 @@ abstract class AbstractPlayerFragment( ) if (player is CS3IPlayer) { + // preview bar + val progressBar : PreviewTimeBar? = playerView?.findViewById(R.id.exo_progress) + val previewImageView : ImageView? = playerView?.findViewById(R.id.previewImageView) + val previewFrameLayout : FrameLayout? = playerView?.findViewById(R.id.previewFrameLayout) + if(progressBar != null && previewImageView != null && previewFrameLayout != null) { + var resume = false + progressBar.addOnScrubListener(object : PreviewBar.OnScrubListener { + override fun onScrubStart(previewBar: PreviewBar?) { + progressBar.isPreviewEnabled = player.hasPreview() + resume = player.getIsPlaying() + if (resume) player.handleEvent( + CSPlayerEvent.Pause, + PlayerEventSource.Player + ) + } + + override fun onScrubMove( + previewBar: PreviewBar?, + progress: Int, + fromUser: Boolean + ) { + } + + override fun onScrubStop(previewBar: PreviewBar?) { + if (resume) player.handleEvent(CSPlayerEvent.Play, PlayerEventSource.Player) + } + }) + progressBar.attachPreviewView(previewFrameLayout) + progressBar.setPreviewLoader { currentPosition, max -> + val bitmap = player.getPreview(currentPosition.toFloat().div(max.toFloat())) + previewImageView.isGone = bitmap == null + previewImageView.setImageBitmap(bitmap) + } + } + subView = playerView?.findViewById(R.id.exo_subtitles) subStyle = SubtitlesFragment.getCurrentSavedStyle() player.initSubtitles(subView, subtitleHolder, subStyle) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 331cfb73..946743a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player import android.annotation.SuppressLint import android.content.Context +import android.graphics.Bitmap import android.net.Uri import android.os.Handler import android.os.Looper @@ -88,7 +89,9 @@ class CS3IPlayer : IPlayer { private var exoPlayer: ExoPlayer? = null set(value) { // If the old value is not null then the player has not been properly released. - debugAssert({ field != null && value != null }, { "Previous player instance should be released!" }) + debugAssert( + { field != null && value != null }, + { "Previous player instance should be released!" }) field = value } @@ -96,6 +99,8 @@ class CS3IPlayer : IPlayer { var simpleCacheSize = 0L var videoBufferMs = 0L + private val imageGenerator = PreviewGenerator() + private val seekActionTime = 30000L private var ignoreSSL: Boolean = true @@ -182,6 +187,14 @@ class CS3IPlayer : IPlayer { subtitleHelper.initSubtitles(subView, subHolder, style) } + override fun getPreview(fraction: Float): Bitmap? { + return imageGenerator.getPreviewImage(fraction) + } + + override fun hasPreview(): Boolean { + return imageGenerator.hasPreview() + } + override fun loadPlayer( context: Context, sameEpisode: Boolean, @@ -190,7 +203,8 @@ class CS3IPlayer : IPlayer { startPosition: Long?, subtitles: Set, subtitle: SubtitleData?, - autoPlay: Boolean? + autoPlay: Boolean?, + preview : Boolean, ) { Log.i(TAG, "loadPlayer") if (sameEpisode) { @@ -210,9 +224,27 @@ class CS3IPlayer : IPlayer { // release the current exoplayer and cache releasePlayer() if (link != null) { + // only video support atm + if (link.type == ExtractorLinkType.VIDEO && preview) { + val headers = if (link.referer.isBlank()) { + link.headers + } else { + mapOf("referer" to link.referer) + link.headers + } + imageGenerator.load(sameEpisode, link.url, headers) + } else { + imageGenerator.clear(sameEpisode) + } loadOnlinePlayer(context, link) } else if (data != null) { + if (preview) { + imageGenerator.load(sameEpisode, context, data.uri) + } else { + imageGenerator.clear(sameEpisode) + } loadOfflinePlayer(context, data) + } else { + throw IllegalArgumentException("Requires link or uri") } } @@ -494,6 +526,7 @@ class CS3IPlayer : IPlayer { } override fun release() { + imageGenerator.release() releasePlayer() } @@ -871,8 +904,20 @@ class CS3IPlayer : IPlayer { CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source) CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source) - CSPlayerEvent.NextEpisode -> event(EpisodeSeekEvent(offset = 1, source = source)) - CSPlayerEvent.PrevEpisode -> event(EpisodeSeekEvent(offset = -1, source = source)) + CSPlayerEvent.NextEpisode -> event( + EpisodeSeekEvent( + offset = 1, + source = source + ) + ) + + CSPlayerEvent.PrevEpisode -> event( + EpisodeSeekEvent( + offset = -1, + source = source + ) + ) + CSPlayerEvent.SkipCurrentChapter -> { //val dur = this@CS3IPlayer.getDuration() ?: return@apply getCurrentTimestamp()?.let { lastTimeStamp -> 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 b2542ffa..1c751897 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 @@ -180,6 +180,7 @@ class GeneratorPlayer : FullScreenPlayer() { (if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle( currentSubs, settings = true, downloads = true ), + preview = isFullScreenPlayer ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index ec006234..a08360ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.player import android.content.Context +import android.graphics.Bitmap import android.util.Rational import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.EpisodeSkip @@ -246,11 +247,15 @@ interface IPlayer { startPosition: Long? = null, subtitles: Set, subtitle: SubtitleData?, - autoPlay: Boolean? = true + autoPlay: Boolean? = true, + preview : Boolean = true, ) fun reloadPlayer(context: Context) + fun getPreview(fraction : Float) : Bitmap? + fun hasPreview() : Boolean + fun setActiveSubtitles(subtitles: Set) fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean // returns true if the player requires a reload, null for nothing fun getCurrentPreferredSubtitle(): SubtitleData? diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt new file mode 100644 index 00000000..0f47d009 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -0,0 +1,147 @@ +package com.lagradost.cloudstream3.ui.player + +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.util.Log +import androidx.annotation.WorkerThread +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlin.math.absoluteValue +import kotlin.math.ceil +import kotlin.math.log2 + +const val MAX_LOD = 6 +const val MIN_LOD = 3 + +class PreviewGenerator { + // lod = level of detail where the number indicates how many ones there is + // 2^(lod-1) = images + private var loadedLod = 0 + private var loadedImages = 0 + private var images = Array((1 shl MAX_LOD) - 1) { + null + } + + fun hasPreview(): Boolean { + synchronized(images) { + return loadedLod >= MIN_LOD + } + } + + val TAG = "PreviewImg" + + fun getPreviewImage(fraction: Float): Bitmap? { + synchronized(images) { + if (loadedLod < MIN_LOD) { + Log.i(TAG, "Requesting preview for $fraction but $loadedLod < $MIN_LOD") + return null + } + Log.i(TAG, "Requesting preview for $fraction") + + var bestIdx = 0 + var bestDiff = 0.5f.minus(fraction).absoluteValue + + // this should be done mathematically, but for now we just loop all images + for (l in 1..loadedLod + 1) { + val items = (1 shl (l - 1)) + for (i in 0 until items) { + val idx = items - 1 + i + if (idx > loadedImages) { + break + } + val currentFraction = + (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) + val diff = currentFraction.minus(fraction).absoluteValue + if (diff < bestDiff) { + bestDiff = diff + bestIdx = idx + } + } + } + Log.i(TAG, "Best diff found at ${bestDiff * 100}% diff (${bestIdx})") + return images[bestIdx] + } + } + + // also check out https://github.com/wseemann/FFmpegMediaMetadataRetriever + private val retriever: MediaMetadataRetriever = MediaMetadataRetriever() + + fun clear(keepCache: Boolean = false) { + if (keepCache) return + synchronized(images) { + loadedLod = 0 + loadedImages = 0 + images.fill(null) + } + } + + private var currentJob: Job? = null + fun load(keepCache: Boolean, url: String, headers: Map) { + currentJob?.cancel() + currentJob = ioSafe { + Log.i(TAG, "Loading with url = $url headers = $headers") + clear(keepCache) + retriever.setDataSource(url, headers) + start(this) + } + } + + fun load(keepCache: Boolean, context: Context, uri: Uri) { + currentJob?.cancel() + currentJob = ioSafe { + Log.i(TAG, "Loading with uri = $uri") + clear(keepCache) + retriever.setDataSource(context, uri) + start(this) + } + } + + fun release() { + currentJob?.cancel() + clear(false) + } + + @Throws + @WorkerThread + private fun start(scope: CoroutineScope) { + Log.i(TAG, "Started loading preview") + + val durationMs = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() + ?: throw IllegalArgumentException("Bad video duration") + val durationUs = (durationMs * 1000L).toFloat() + //val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: throw IllegalArgumentException("Bad video width") + //val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: throw IllegalArgumentException("Bad video height") + + // log2 # 10s durations in the video ~= how many segments we have + val maxLod = ceil(log2((durationMs / 10_000).toFloat())).toInt().coerceIn(MIN_LOD, MAX_LOD) + + for (l in 1..maxLod) { + val items = (1 shl (l - 1)) + for (i in 0 until items) { + val idx = items - 1 + i // as sum(prev) = cur-1 + // frame = 100 / 2^lod + i * 100 / 2^(lod-1) = duration % where lod is one indexed + val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) + Log.i(TAG, "Generating preview for ${fraction * 100}%") + val frame = durationUs * fraction + val img = retriever.getFrameAtTime( + frame.toLong(), + MediaMetadataRetriever.OPTION_CLOSEST_SYNC + ) + if (!scope.isActive) return + synchronized(images) { + images[idx] = img + loadedImages = maxOf(loadedImages,idx) + } + } + + synchronized(images) { + loadedLod = maxOf(loadedLod, l) + } + } + } +} \ No newline at end of file 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 ef2ed0df..e5f16dd5 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 @@ -151,7 +151,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { startPosition = 0L, subtitles = emptySet(), subtitle = null, - autoPlay = false + autoPlay = false, + preview = false ) true } ?: run { diff --git a/app/src/main/res/drawable/video_frame.xml b/app/src/main/res/drawable/video_frame.xml new file mode 100644 index 00000000..19fcf26d --- /dev/null +++ b/app/src/main/res/drawable/video_frame.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 5592f3a6..0f76e4dd 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -86,7 +86,6 @@ + + android:importantForAccessibility="no" + android:visibility="gone" /> + + + - - - + + + + + - - - + - - + tools:ignore="ContentDescription" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" /> - - + + + + + - + - - + - - - + + + + + - - + + - 62dp 50dp - + 1dp \ No newline at end of file From 1d90858f64bd113df71dc68fa8e03f9be640c542 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Mon, 2 Oct 2023 13:04:40 -0600 Subject: [PATCH 246/570] Make search history account specific (#638) * Make search history account specific * Update for clear history --- .../com/lagradost/cloudstream3/ui/search/SearchFragment.kt | 7 ++++--- .../lagradost/cloudstream3/ui/search/SearchViewModel.kt | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) 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 bdf82377..845c36ef 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 @@ -55,6 +55,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar @@ -398,7 +399,7 @@ class SearchFragment : Fragment() { DialogInterface.OnClickListener { _, which -> when (which) { DialogInterface.BUTTON_POSITIVE -> { - removeKeys(SEARCH_HISTORY_KEY) + removeKeys("$currentAccount/$SEARCH_HISTORY_KEY") searchViewModel.updateHistory() } DialogInterface.BUTTON_NEGATIVE -> { @@ -510,7 +511,7 @@ class SearchFragment : Fragment() { binding?.mainSearch?.setQuery(searchItem.searchText, true) } SEARCH_HISTORY_REMOVE -> { - removeKey(SEARCH_HISTORY_KEY, searchItem.key) + removeKey("$currentAccount/$SEARCH_HISTORY_KEY", searchItem.key) searchViewModel.updateHistory() } else -> { @@ -559,4 +560,4 @@ class SearchFragment : Fragment() { .commit()*/ } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index 320687f8..839b9d3f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -14,6 +14,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -64,7 +65,7 @@ class SearchViewModel : ViewModel() { fun updateHistory() = viewModelScope.launch { ioSafe { - val items = getKeys(SEARCH_HISTORY_KEY)?.mapNotNull { + val items = getKeys("$currentAccount/$SEARCH_HISTORY_KEY")?.mapNotNull { getKey(it) }?.sortedByDescending { it.searchedAt } ?: emptyList() _currentHistory.postValue(items) @@ -87,7 +88,7 @@ class SearchViewModel : ViewModel() { if (!isQuickSearch) { val key = query.hashCode().toString() setKey( - SEARCH_HISTORY_KEY, + "$currentAccount/$SEARCH_HISTORY_KEY", key, SearchHistoryItem( searchedAt = System.currentTimeMillis(), @@ -140,4 +141,4 @@ class SearchViewModel : ViewModel() { _searchResponse.postValue(Resource.Success(list)) } } -} \ No newline at end of file +} From 08060314ad94054ed8c42bd38824122bfb24565e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:50:34 +0200 Subject: [PATCH 247/570] preview seekbar m3u8 --- .../cloudstream3/ui/player/CS3IPlayer.kt | 11 +- .../ui/player/PreviewGenerator.kt | 263 +++++++++++++++++- .../cloudstream3/utils/ExtractorApi.kt | 9 + .../cloudstream3/utils/M3u8Helper.kt | 42 ++- 4 files changed, 298 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 946743a3..6256bef6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -225,20 +225,15 @@ class CS3IPlayer : IPlayer { releasePlayer() if (link != null) { // only video support atm - if (link.type == ExtractorLinkType.VIDEO && preview) { - val headers = if (link.referer.isBlank()) { - link.headers - } else { - mapOf("referer" to link.referer) + link.headers - } - imageGenerator.load(sameEpisode, link.url, headers) + if (preview) { + imageGenerator.load(link, sameEpisode) } else { imageGenerator.clear(sameEpisode) } loadOnlinePlayer(context, link) } else if (data != null) { if (preview) { - imageGenerator.load(sameEpisode, context, data.uri) + imageGenerator.load(context, data, sameEpisode) } else { imageGenerator.clear(sameEpisode) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index 0f47d009..53699782 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -6,10 +6,18 @@ import android.media.MediaMetadataRetriever import android.net.Uri import android.util.Log import androidx.annotation.WorkerThread +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkType +import com.lagradost.cloudstream3.utils.ExtractorUri +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.M3u8Helper2 import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext import kotlin.math.absoluteValue import kotlin.math.ceil import kotlin.math.log2 @@ -17,7 +25,248 @@ import kotlin.math.log2 const val MAX_LOD = 6 const val MIN_LOD = 3 -class PreviewGenerator { +interface IPreviewGenerator { + fun hasPreview(): Boolean + fun getPreviewImage(fraction: Float): Bitmap? + fun clear(keepCache: Boolean = false) + fun release() +} + +class PreviewGenerator : IPreviewGenerator { + private var currentGenerator: IPreviewGenerator = NoPreviewGenerator() + override fun hasPreview(): Boolean { + return currentGenerator.hasPreview() + } + + override fun getPreviewImage(fraction: Float): Bitmap? { + return try { + currentGenerator.getPreviewImage(fraction) + } catch (t: Throwable) { + logError(t) + null + } + } + + override fun clear(keepCache: Boolean) { + currentGenerator.clear(keepCache) + } + + override fun release() { + currentGenerator.release() + } + + fun load(link: ExtractorLink, keepCache: Boolean) { + val gen = currentGenerator + when (link.type) { + ExtractorLinkType.M3U8 -> { + if (gen is M3u8PreviewGenerator) { + gen.load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) + } else { + currentGenerator.release() + currentGenerator = M3u8PreviewGenerator().apply { + load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) + } + } + } + + ExtractorLinkType.VIDEO -> { + if (gen is Mp4PreviewGenerator) { + gen.load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) + } else { + currentGenerator.release() + currentGenerator = Mp4PreviewGenerator().apply { + load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) + } + } + } + + else -> { + currentGenerator.clear(keepCache) + } + } + } + + fun load(context: Context, link: ExtractorUri, keepCache: Boolean) { + val gen = currentGenerator + if (gen is Mp4PreviewGenerator) { + gen.load(keepCache = keepCache, context = context, uri = link.uri) + } else { + currentGenerator.release() + currentGenerator = Mp4PreviewGenerator().apply { + load(keepCache = keepCache, context = context, uri = link.uri) + } + } + } +} + +class NoPreviewGenerator : IPreviewGenerator { + override fun hasPreview(): Boolean = false + override fun getPreviewImage(fraction: Float): Bitmap? = null + override fun clear(keepCache: Boolean) = Unit + override fun release() = Unit +} + +class M3u8PreviewGenerator : IPreviewGenerator { + // generated images 1:1 to idx of hsl + private var images: Array = arrayOf() + + private val TAG = "PreviewImgM3u8" + + // prefixSum[i] = sum(hsl.ts[0..i].time) + // where [0] = 0, [1] = hsl.ts[0].time aka time at start of segment, do [b] - [a] for range a,b + private var prefixSum: Array = arrayOf() + + // how many images has been generated + private var loadedImages: Int = 0 + + // how many images we can generate in total, == hsl.size ?: 0 + private var totalImages: Int = 0 + + override fun hasPreview(): Boolean { + return totalImages > 0 && loadedImages >= minOf(totalImages, 4) + } + + override fun getPreviewImage(fraction: Float): Bitmap? { + var bestIdx = -1 + var bestDiff = Double.MAX_VALUE + synchronized(images) { + // just find the best one in a for loop, we don't care about bin searching rn + for (i in 0..images.size) { + val diff = prefixSum[i].minus(fraction).absoluteValue + if (diff > bestDiff) { + break + } + if (images[i] != null) { + bestIdx = i + bestDiff = diff + } + } + return images.getOrNull(bestIdx) + } + /* + val targetIndex = prefixSum.binarySearch(target) + var ret = images[targetIndex] + if (ret != null) { + return ret + } + for (i in 0..images.size) { + ret = images.getOrNull(i+targetIndex) ?: + }*/ + } + + override fun clear(keepCache: Boolean) { + synchronized(images) { + currentJob?.cancel() + images = arrayOf() + prefixSum = arrayOf() + loadedImages = 0 + totalImages = 0 + } + } + + override fun release() { + clear() + images = arrayOf() + } + + private var currentJob: Job? = null + fun load(keepCache: Boolean, url: String, headers: Map) { + clear(keepCache) + currentJob?.cancel() + currentJob = ioSafe { + withContext(Dispatchers.IO) { + Log.i(TAG, "Loading with url = $url headers = $headers") + //tmpFile = + // File.createTempFile("video", ".ts", context.cacheDir).apply { + // deleteOnExit() + // } + val retriever = MediaMetadataRetriever() + val hsl = M3u8Helper2.hslLazy( + listOf( + M3u8Helper.M3u8Stream( + streamUrl = url, + headers = headers + ) + ), + selectBest = false + ) + + // no support for encryption atm + if (hsl.isEncrypted) { + totalImages = 0 + return@withContext + } + + // total duration of the entire m3u8 in seconds + val duration = hsl.allTsLinks.sumOf { it.time ?: 0.0 } + val durationInv = 1.0 / duration + + // if the total duration is less then 10s then something is very wrong or + // too short playback to matter + if (duration <= 10.0) { + totalImages = 0 + return@withContext + } + + totalImages = hsl.allTsLinks.size + + // we cant init directly as it is no guarantee of in order + prefixSum = Array(hsl.allTsLinks.size + 1) { 0.0 } + var runningSum = 0.0 + for (i in hsl.allTsLinks.indices) { + runningSum += (hsl.allTsLinks[i].time ?: 0.0) + prefixSum[i + 1] = runningSum * durationInv + } + synchronized(images) { + images = Array(hsl.size) { null } + loadedImages = 0 + } + + val maxLod = ceil(log2(duration)).toInt().coerceIn(MIN_LOD, MAX_LOD) + val count = hsl.allTsLinks.size + for (l in 1..maxLod) { + val items = (1 shl (l - 1)) + for (i in 0 until items) { + val index = (count.div(1 shl l) + (i * count) / items).coerceIn(0, hsl.size) + if (synchronized(images) { images[index] } != null) { + continue + } + Log.i(TAG, "Generating preview for $index") + + val ts = hsl.allTsLinks[index] + try { + retriever.setDataSource(ts.url, hsl.headers) + if (!isActive) { + return@withContext + } + val frame = retriever.getFrameAtTime(0) + if (!isActive) { + return@withContext + } + synchronized(images) { + images[index] = frame + loadedImages += 1 + } + } catch (t: Throwable) { + logError(t) + continue + } + + /* + val buffer = hsl.resolveLinkSafe(index) ?: continue + tmpFile?.writeBytes(buffer) + val buff = FileOutputStream(tmpFile) + retriever.setDataSource(buff.fd) + val frame = retriever.getFrameAtTime(0L)*/ + } + } + + } + } + } +} + +class Mp4PreviewGenerator : IPreviewGenerator { // lod = level of detail where the number indicates how many ones there is // 2^(lod-1) = images private var loadedLod = 0 @@ -26,15 +275,15 @@ class PreviewGenerator { null } - fun hasPreview(): Boolean { + override fun hasPreview(): Boolean { synchronized(images) { return loadedLod >= MIN_LOD } } - val TAG = "PreviewImg" + val TAG = "PreviewImgMp4" - fun getPreviewImage(fraction: Float): Bitmap? { + override fun getPreviewImage(fraction: Float): Bitmap? { synchronized(images) { if (loadedLod < MIN_LOD) { Log.i(TAG, "Requesting preview for $fraction but $loadedLod < $MIN_LOD") @@ -70,7 +319,7 @@ class PreviewGenerator { // also check out https://github.com/wseemann/FFmpegMediaMetadataRetriever private val retriever: MediaMetadataRetriever = MediaMetadataRetriever() - fun clear(keepCache: Boolean = false) { + override fun clear(keepCache: Boolean) { if (keepCache) return synchronized(images) { loadedLod = 0 @@ -100,7 +349,7 @@ class PreviewGenerator { } } - fun release() { + override fun release() { currentJob?.cancel() clear(false) } @@ -135,7 +384,7 @@ class PreviewGenerator { if (!scope.isActive) return synchronized(images) { images[idx] = img - loadedImages = maxOf(loadedImages,idx) + loadedImages = maxOf(loadedImages, idx) } } 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 9db62dc8..d89e67fa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -378,6 +378,15 @@ open class ExtractorLink constructor( val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8 val isDash : Boolean get() = type == ExtractorLinkType.DASH + fun getAllHeaders() : Map { + if (referer.isBlank()) { + return headers + } else if (headers.keys.none { it.equals("referer", ignoreCase = true) }) { + return headers + mapOf("referer" to referer) + } + return headers + } + constructor( source: String, name: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 11dfa441..d3fe7162 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -71,7 +71,7 @@ object M3u8Helper2 { private val QUALITY_REGEX = Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""") private val TS_EXTENSION_REGEX = - Regex("""#EXTINF:.*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways + Regex("""#EXTINF:(([0-9]*[.])?[0-9]+|).*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways //Regex("""(.*\.(ts|jpg|html).*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts private fun absoluteExtensionDetermination(url: String): String? { @@ -122,6 +122,15 @@ object M3u8Helper2 { return result.lastOrNull() } + private fun selectWorst(qualities: List): M3u8Helper.M3u8Stream? { + val result = qualities.sortedBy { + if (it.quality != null && it.quality <= 1080) it.quality else 0 + }.filter { + listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl)) + } + return result.firstOrNull() + } + private fun getParentLink(uri: String): String { val split = uri.split("/").toMutableList() split.removeLast() @@ -173,14 +182,20 @@ object M3u8Helper2 { return list } + data class TsLink( + val url : String, + val time : Double?, + ) + data class LazyHlsDownloadData( private val encryptionData: ByteArray, private val encryptionIv: ByteArray, - private val isEncrypted: Boolean, - private val allTsLinks: List, - private val relativeUrl: String, - private val headers: Map, + val isEncrypted: Boolean, + val allTsLinks: List, + val relativeUrl: String, + val headers: Map, ) { + val size get() = allTsLinks.size suspend fun resolveLinkWhileSafe( @@ -228,9 +243,9 @@ object M3u8Helper2 { @Throws suspend fun resolveLink(index: Int): ByteArray { if (index < 0 || index >= size) throw IllegalArgumentException("index must be in the bounds of the ts") - val url = allTsLinks[index] + val ts = allTsLinks[index] - val tsResponse = app.get(url, headers = headers, verify = false) + val tsResponse = app.get(ts.url, headers = headers, verify = false) val tsData = tsResponse.body.bytes() if (tsData.isEmpty()) throw ErrorLoadingException("no data") @@ -244,15 +259,16 @@ object M3u8Helper2 { @Throws suspend fun hslLazy( - qualities: List + qualities: List, selectBest : Boolean = true ): LazyHlsDownloadData { if (qualities.isEmpty()) throw IllegalArgumentException("qualities must be non empty") - val selected = selectBest(qualities) ?: qualities.first() + val selected = if(selectBest) { selectBest(qualities) } else { selectWorst(qualities) } ?: qualities.first() val headers = selected.headers val streams = qualities.map { m3u8Generation(it, false) }.flatten() // this selects the best quality of the qualities offered, // due to the recursive nature of m3u8, we only go 2 depth - val secondSelection = selectBest(streams.ifEmpty { listOf(selected) }) + val innerStreams = streams.ifEmpty { listOf(selected) } + val secondSelection = if(selectBest) { selectBest(innerStreams) } else { selectWorst(innerStreams) } ?: throw IllegalArgumentException("qualities has no streams") val m3u8Response = @@ -285,12 +301,14 @@ object M3u8Helper2 { } val relativeUrl = getParentLink(secondSelection.streamUrl) val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts -> - val value = ts.groupValues[1] - if (isNotCompleteUrl(value)) { + val time = ts.groupValues[1] + val value = ts.groupValues[3] + val url = if (isNotCompleteUrl(value)) { "$relativeUrl/${value}" } else { value } + TsLink(url = url, time = time.toDoubleOrNull()) }.toList() if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty") From 462073bd747c62ab995ce200b5a55ddce18811ad Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 3 Oct 2023 08:56:38 -0600 Subject: [PATCH 248/570] Make search prefs account specific (#640) --- .../cloudstream3/ui/search/SearchFragment.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 845c36ef..bad78624 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 @@ -194,7 +194,7 @@ class SearchFragment : Fragment() { validAPIs.flatMap { api -> api.supportedTypes }.distinct() ) { list -> if (selectedSearchTypes.toSet() != list.toSet()) { - setKey(SEARCH_PREF_TAGS, selectedSearchTypes) + setKey("$currentAccount/$SEARCH_PREF_TAGS", selectedSearchTypes) selectedSearchTypes.clear() selectedSearchTypes.addAll(list) search(binding?.mainSearch?.query?.toString()) @@ -236,7 +236,7 @@ class SearchFragment : Fragment() { context?.let { ctx -> val validAPIs = ctx.filterProviderByPreferredMedia() selectedApis = ctx.getKey( - SEARCH_PREF_PROVIDERS, + "$currentAccount/$SEARCH_PREF_PROVIDERS", defVal = validAPIs.map { it.name } )!!.toMutableSet() } @@ -287,7 +287,7 @@ class SearchFragment : Fragment() { } fun updateList(types: List) { - setKey(SEARCH_PREF_TAGS, types.map { it.name }) + setKey("$currentAccount/$SEARCH_PREF_TAGS", types.map { it.name }) arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> @@ -312,7 +312,7 @@ class SearchFragment : Fragment() { arrayAdapter.notifyDataSetChanged() } - val selectedSearchTypes = getKey>(SEARCH_PREF_TAGS) + val selectedSearchTypes = getKey>("$currentAccount/$SEARCH_PREF_TAGS") ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } @@ -343,7 +343,7 @@ class SearchFragment : Fragment() { } dialog.setOnDismissListener { - context?.setKey(SEARCH_PREF_PROVIDERS, currentSelectedApis.toList()) + context?.setKey("$currentAccount/$SEARCH_PREF_PROVIDERS", currentSelectedApis.toList()) selectedApis = currentSelectedApis } updateList(selectedSearchTypes.toList()) @@ -354,7 +354,7 @@ class SearchFragment : Fragment() { val settingsManager = context?.let { PreferenceManager.getDefaultSharedPreferences(it) } val isAdvancedSearch = settingsManager?.getBoolean("advanced_search", true) ?: true - selectedSearchTypes = context?.getKey>(SEARCH_PREF_TAGS) + selectedSearchTypes = context?.getKey>("$currentAccount/$SEARCH_PREF_TAGS") ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } ?.toMutableList() ?: mutableListOf(TvType.Movie, TvType.TvSeries) From cc00e73e16459365bb7d412ee07422a354767ad0 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:25:31 -0600 Subject: [PATCH 249/570] Make homepage preferences account specific (#647) * Make homepage preferences account specific * Fix accidentally removed whitespace * Fix in setkey --- .../java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt | 4 ++-- .../com/lagradost/cloudstream3/ui/home/HomeViewModel.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) 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 0797e9a0..ebbb245c 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 @@ -377,7 +377,7 @@ class HomeFragment : Fragment() { var currentApiName = selectedApiName var currentValidApis: MutableList = mutableListOf() - val preSelectedTypes = this.getKey>(HOME_PREF_HOMEPAGE) + val preSelectedTypes = this.getKey>("${DataStoreHelper.currentAccount}/$HOME_PREF_HOMEPAGE") ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } ?.toMutableList() ?: mutableListOf(TvType.Movie, TvType.TvSeries) @@ -408,7 +408,7 @@ class HomeFragment : Fragment() { } fun updateList() { - this.setKey(HOME_PREF_HOMEPAGE, preSelectedTypes) + this.setKey("${DataStoreHelper.currentAccount}/$HOME_PREF_HOMEPAGE", preSelectedTypes) arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 13d34b59..a5ef2bb4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -171,7 +171,7 @@ class HomeViewModel : ViewModel() { if (currentWatchTypes.size <= 0) { setKey( - HOME_BOOKMARK_VALUE_LIST, + "${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST", intArrayOf() ) _availableWatchStatusTypes.postValue(setOf() to setOf()) @@ -182,7 +182,7 @@ class HomeViewModel : ViewModel() { val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first()) //if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(currentWatchTypes.first()) setKey( - HOME_BOOKMARK_VALUE_LIST, + "${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST", watchPrefNotNull.map { it.internalId }.toIntArray() ) _availableWatchStatusTypes.postValue( @@ -463,7 +463,7 @@ class HomeViewModel : ViewModel() { fun loadStoredData() { val list = EnumSet.noneOf(WatchType::class.java) - getKey(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let { + getKey("${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST")?.map { WatchType.fromInternalId(it) }?.let { list.addAll(it) } loadStoredData(list) From b5d4c3bd27cc70edaa461dd0a29c4c5828be8fcf Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:26:56 -0600 Subject: [PATCH 250/570] Make player preferences account specific (#646) --- .../cloudstream3/ui/player/AbstractPlayerFragment.kt | 5 +++-- .../java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 5 +++-- .../com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 862504a1..52974ff7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -48,6 +48,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI @@ -442,7 +443,7 @@ abstract class AbstractPlayerFragment( @SuppressLint("SetTextI18n", "UnsafeOptInUsageError") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - resizeMode = getKey(RESIZE_MODE_KEY) ?: 0 + resizeMode = getKey("$currentAccount/$RESIZE_MODE_KEY") ?: 0 resize(resizeMode, false) player.releaseCallbacks() @@ -573,7 +574,7 @@ abstract class AbstractPlayerFragment( @SuppressLint("UnsafeOptInUsageError") fun resize(resize: PlayerResize, showToast: Boolean) { - setKey(RESIZE_MODE_KEY, resize.ordinal) + setKey("$currentAccount/$RESIZE_MODE_KEY", resize.ordinal) val type = when (resize) { PlayerResize.Fill -> AspectRatioFrameLayout.RESIZE_MODE_FILL PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 6256bef6..49904f6a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -57,6 +57,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.DrmExtractorLink import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink @@ -536,12 +537,12 @@ class CS3IPlayer : IPlayer { **/ var preferredAudioTrackLanguage: String? = null get() { - return field ?: getKey(PREFERRED_AUDIO_LANGUAGE_KEY, field)?.also { + return field ?: getKey("$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", field)?.also { field = it } } set(value) { - setKey(PREFERRED_AUDIO_LANGUAGE_KEY, value) + setKey("$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", value) field = value } 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 e698191d..43e8aa0b 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 @@ -49,6 +49,7 @@ import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -356,7 +357,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun setPlayBackSpeed(speed: Float) { try { - setKey(PLAYBACK_SPEED_KEY, speed) + setKey("$currentAccount/$PLAYBACK_SPEED_KEY", speed) playerBinding?.playerSpeedBtt?.text = getString(R.string.player_speed_text_format).format(speed) .replace(".0x", "x") @@ -1194,7 +1195,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // init variables - setPlayBackSpeed(getKey(PLAYBACK_SPEED_KEY) ?: 1.0f) + setPlayBackSpeed(getKey("$currentAccount/$PLAYBACK_SPEED_KEY") ?: 1.0f) savedInstanceState?.getLong(SUBTITLE_DELAY_BUNDLE_KEY)?.let { subtitleDelay = it } From 3f5119525c3a17a13a3dce42bef6a9caffd5c86e Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:59:26 -0600 Subject: [PATCH 251/570] Make library preferences account specific (#649) --- .../lagradost/cloudstream3/ui/library/LibraryFragment.kt | 9 +++++---- .../cloudstream3/ui/library/LibraryViewModel.kt | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) 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 index 04ef3d96..d5fdc1aa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -35,6 +35,7 @@ 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.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount @@ -180,7 +181,7 @@ class LibraryFragment : Fragment() { val items = baseOptions.map { txt(it.stringRes).asString(this) } + availableProviders - val savedSelection = getKey(LIBRARY_FOLDER, key) + val savedSelection = getKey("$currentAccount/$LIBRARY_FOLDER", key) val selectedIndex = when { savedSelection == null -> 0 @@ -215,7 +216,7 @@ class LibraryFragment : Fragment() { } setKey( - LIBRARY_FOLDER, + "$currentAccount/$LIBRARY_FOLDER", key, savedData, ) @@ -262,8 +263,8 @@ class LibraryFragment : Fragment() { // 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 { + getKey("$currentAccount/$LIBRARY_FOLDER", syncName.name) + val savedSelection = getKey("$currentAccount/$LIBRARY_FOLDER", syncId).takeIf { it?.openType != LibraryOpenerType.Default } ?: savedListSelection 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 index 14d31356..25a5a0f8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -11,6 +11,7 @@ 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 com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount enum class ListSorting(@StringRes val stringRes: Int) { Query(R.string.none), @@ -35,12 +36,12 @@ class LibraryViewModel : ViewModel() { get() = SyncApis.filter { it.hasAccount() } var currentSyncApi = availableSyncApis.let { allApis -> - val lastSelection = getKey(LAST_SYNC_API_KEY) + val lastSelection = getKey("$currentAccount/$LAST_SYNC_API_KEY") availableSyncApis.firstOrNull { it.name == lastSelection } ?: allApis.firstOrNull() } private set(value) { field = value - setKey(LAST_SYNC_API_KEY, field?.name) + setKey("$currentAccount/$LAST_SYNC_API_KEY", field?.name) } val availableApiNames: List From 177b1e47f3f08110c07e3839a2d1935fe491a4da Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 4 Oct 2023 11:33:55 +0200 Subject: [PATCH 252/570] added extra logging --- .../cloudstream3/ui/player/PreviewGenerator.kt | 11 +++++++++-- .../com/lagradost/cloudstream3/utils/M3u8Helper.kt | 12 ++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index 53699782..946c1d33 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -81,6 +81,7 @@ class PreviewGenerator : IPreviewGenerator { } else -> { + Log.i("PreviewImg", "unsupported format for $link") currentGenerator.clear(keepCache) } } @@ -193,6 +194,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { // no support for encryption atm if (hsl.isEncrypted) { + Log.i(TAG, "m3u8 is encrypted") totalImages = 0 return@withContext } @@ -239,12 +241,13 @@ class M3u8PreviewGenerator : IPreviewGenerator { if (!isActive) { return@withContext } - val frame = retriever.getFrameAtTime(0) + val img = retriever.getFrameAtTime(0) if (!isActive) { return@withContext } + if(img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { - images[index] = frame + images[index] = img loadedImages += 1 } } catch (t: Throwable) { @@ -302,6 +305,9 @@ class Mp4PreviewGenerator : IPreviewGenerator { if (idx > loadedImages) { break } + if(images[idx] == null) { + continue + } val currentFraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) val diff = currentFraction.minus(fraction).absoluteValue @@ -382,6 +388,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { MediaMetadataRetriever.OPTION_CLOSEST_SYNC ) if (!scope.isActive) return + if(img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { images[idx] = img loadedImages = maxOf(loadedImages, idx) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index d3fe7162..298f1601 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -115,19 +115,19 @@ object M3u8Helper2 { private fun selectBest(qualities: List): M3u8Helper.M3u8Stream? { val result = qualities.sortedBy { - if (it.quality != null && it.quality <= 1080) it.quality else 0 - }.filter { + it.quality ?: Qualities.Unknown.value //if (it.quality != null && it.quality <= 1080) else 0 + }/*.filter { listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl)) - } + }*/ return result.lastOrNull() } private fun selectWorst(qualities: List): M3u8Helper.M3u8Stream? { val result = qualities.sortedBy { - if (it.quality != null && it.quality <= 1080) it.quality else 0 - }.filter { + it.quality ?: Qualities.Unknown.value //if (it.quality != null && it.quality <= 1080) else 0 + }/*.filter { listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl)) - } + }*/ return result.firstOrNull() } From 0a327ccbda8d3622127cae2e419a386357250b6e Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Fri, 6 Oct 2023 15:50:31 -0600 Subject: [PATCH 253/570] Reload library when reloading home (#656) So that library is reloaded when switching accounts. Fixes #650 --- .../cloudstream3/ui/library/LibraryViewModel.kt | 10 ++++++++++ .../lagradost/cloudstream3/utils/DataStoreHelper.kt | 1 - 2 files changed, 10 insertions(+), 1 deletion(-) 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 index 25a5a0f8..c104a7c3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -6,6 +6,7 @@ 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.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis @@ -101,4 +102,13 @@ class LibraryViewModel : ViewModel() { } } } + + init { + MainActivity.reloadHomeEvent += ::reloadPages + } + + override fun onCleared() { + MainActivity.reloadHomeEvent -= ::reloadPages + super.onCleared() + } } \ No newline at end of file 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 7bce1b6c..775cb718 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -131,7 +131,6 @@ object DataStoreHelper { // update UI setAccount(getDefaultAccount(context), true) - MainActivity.bookmarksUpdatedEvent(true) dialog?.dismissSafe() } From 77294dc68e6ba19c21017f4ac17847755572ad18 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 7 Oct 2023 01:39:30 +0200 Subject: [PATCH 254/570] cleanup --- .../lagradost/cloudstream3/MainActivity.kt | 40 +++--- .../cloudstream3/ui/home/HomeFragment.kt | 25 +--- .../cloudstream3/ui/home/HomeViewModel.kt | 22 ++- .../ui/player/AbstractPlayerFragment.kt | 11 +- .../ui/player/FullScreenPlayer.kt | 6 +- .../cloudstream3/ui/player/IPlayer.kt | 9 -- .../ui/player/PreviewGenerator.kt | 134 ++++++++++++------ .../cloudstream3/ui/search/SearchFragment.kt | 43 ++---- .../cloudstream3/utils/DataStoreHelper.kt | 68 +++++++++ .../main/res/layout/player_custom_layout.xml | 4 +- 10 files changed, 215 insertions(+), 147 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index d5187029..17823f7c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1122,23 +1122,25 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (isTvSettings()) { val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) setContentView(newLocalBinding.root) - TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) - newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> - // println("refocus $oldFocus -> $newFocus") - try { - val r = Rect(0, 0, 0, 0) - newFocus.getDrawingRect(r) - val x = r.centerX() - val y = r.centerY() - val dx = 0 //screenWidth / 2 - val dy = screenHeight / 2 - val r2 = Rect(x - dx, y - dy, x + dx, y + dy) - newFocus.requestRectangleOnScreen(r2, false) - // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) - } catch (_: Throwable) { - } - TvFocus.updateFocusView(newFocus) - /*var focus = newFocus + + if(isTrueTvSettings()) { + TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) + newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> + // println("refocus $oldFocus -> $newFocus") + try { + val r = Rect(0, 0, 0, 0) + newFocus.getDrawingRect(r) + val x = r.centerX() + val y = r.centerY() + val dx = 0 //screenWidth / 2 + val dy = screenHeight / 2 + val r2 = Rect(x - dx, y - dy, x + dx, y + dy) + newFocus.requestRectangleOnScreen(r2, false) + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_: Throwable) { + } + TvFocus.updateFocusView(newFocus) + /*var focus = newFocus while(focus != null) { if(focus is ScrollingView && focus.canScrollVertically()) { @@ -1149,7 +1151,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { else -> break } }*/ + } + } else { + newLocalBinding.focusOutline.isVisible = false } + newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) } 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 ebbb245c..4d940123 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 @@ -7,7 +7,6 @@ import android.content.DialogInterface import android.content.Intent import android.content.res.Configuration import android.net.Uri -import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -23,18 +22,12 @@ import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.button.MaterialButton import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CommonActivity.showToast -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.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.HomeEpisodesExpandedBinding import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding @@ -45,37 +38,26 @@ import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi -import com.lagradost.cloudstream3.ui.WatchType -import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.* import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings -import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable -import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult 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.ioSafe -import com.lagradost.cloudstream3.utils.DataStore.getKey -import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes - import java.util.* -const val HOME_BOOKMARK_VALUE_LIST = "home_bookmarked_last_list" -const val HOME_PREF_HOMEPAGE = "home_pref_homepage" - class HomeFragment : Fragment() { companion object { val configEvent = Event() @@ -377,10 +359,7 @@ class HomeFragment : Fragment() { var currentApiName = selectedApiName var currentValidApis: MutableList = mutableListOf() - val preSelectedTypes = this.getKey>("${DataStoreHelper.currentAccount}/$HOME_PREF_HOMEPAGE") - ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } - ?.toMutableList() - ?: mutableListOf(TvType.Movie, TvType.TvSeries) + val preSelectedTypes = DataStoreHelper.homePreference.toMutableList() binding.cancelBtt.setOnClickListener { dialog.dismissSafe() @@ -408,7 +387,7 @@ class HomeFragment : Fragment() { } fun updateList() { - this.setKey("${DataStoreHelper.currentAccount}/$HOME_PREF_HOMEPAGE", preSelectedTypes) + DataStoreHelper.homePreference = preSelectedTypes arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index a5ef2bb4..ad75aa9d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -12,7 +12,6 @@ import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse @@ -170,10 +169,7 @@ class HomeViewModel : ViewModel() { currentWatchTypes.remove(WatchType.NONE) if (currentWatchTypes.size <= 0) { - setKey( - "${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST", - intArrayOf() - ) + DataStoreHelper.homeBookmarkedList = intArrayOf() _availableWatchStatusTypes.postValue(setOf() to setOf()) _bookmarks.postValue(Pair(false, ArrayList())) return@launchSafe @@ -181,16 +177,14 @@ class HomeViewModel : ViewModel() { val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first()) //if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(currentWatchTypes.first()) - setKey( - "${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST", - watchPrefNotNull.map { it.internalId }.toIntArray() - ) + + DataStoreHelper.homeBookmarkedList = watchPrefNotNull.map { it.internalId }.toIntArray() _availableWatchStatusTypes.postValue( - Pair( - watchPrefNotNull, - currentWatchTypes, + + watchPrefNotNull to + currentWatchTypes, + ) - ) val list = withContext(Dispatchers.IO) { watchStatusIds.filter { watchPrefNotNull.contains(it.second) } @@ -463,7 +457,7 @@ class HomeViewModel : ViewModel() { fun loadStoredData() { val list = EnumSet.noneOf(WatchType::class.java) - getKey("${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST")?.map { WatchType.fromInternalId(it) }?.let { + DataStoreHelper.homeBookmarkedList.map { WatchType.fromInternalId(it) }.let { list.addAll(it) } loadStoredData(list) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 52974ff7..431e4fe1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -33,8 +33,6 @@ import androidx.preference.PreferenceManager import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import com.github.rubensousa.previewseekbar.PreviewBar import com.github.rubensousa.previewseekbar.media3.PreviewTimeBar -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode import com.lagradost.cloudstream3.CommonActivity.isInPIPMode import com.lagradost.cloudstream3.CommonActivity.keyEventListener @@ -48,7 +46,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus -import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI @@ -443,7 +441,7 @@ abstract class AbstractPlayerFragment( @SuppressLint("SetTextI18n", "UnsafeOptInUsageError") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - resizeMode = getKey("$currentAccount/$RESIZE_MODE_KEY") ?: 0 + resizeMode = DataStoreHelper.resizeMode resize(resizeMode, false) player.releaseCallbacks() @@ -466,7 +464,8 @@ abstract class AbstractPlayerFragment( var resume = false progressBar.addOnScrubListener(object : PreviewBar.OnScrubListener { override fun onScrubStart(previewBar: PreviewBar?) { - progressBar.isPreviewEnabled = player.hasPreview() + val hasPreview = player.hasPreview() + progressBar.isPreviewEnabled = hasPreview resume = player.getIsPlaying() if (resume) player.handleEvent( CSPlayerEvent.Pause, @@ -574,7 +573,7 @@ abstract class AbstractPlayerFragment( @SuppressLint("UnsafeOptInUsageError") fun resize(resize: PlayerResize, showToast: Boolean) { - setKey("$currentAccount/$RESIZE_MODE_KEY", resize.ordinal) + DataStoreHelper.resizeMode = resize.ordinal val type = when (resize) { PlayerResize.Fill -> AspectRatioFrameLayout.RESIZE_MODE_FILL PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT 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 43e8aa0b..819e50ba 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 @@ -49,7 +49,7 @@ import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData -import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -357,7 +357,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun setPlayBackSpeed(speed: Float) { try { - setKey("$currentAccount/$PLAYBACK_SPEED_KEY", speed) + DataStoreHelper.playBackSpeed = speed playerBinding?.playerSpeedBtt?.text = getString(R.string.player_speed_text_format).format(speed) .replace(".0x", "x") @@ -1195,7 +1195,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // init variables - setPlayBackSpeed(getKey("$currentAccount/$PLAYBACK_SPEED_KEY") ?: 1.0f) + setPlayBackSpeed(DataStoreHelper.playBackSpeed) savedInstanceState?.getLong(SUBTITLE_DELAY_BUNDLE_KEY)?.let { subtitleDelay = it } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index a08360ae..0e54e2cb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -199,17 +199,8 @@ data class CurrentTracks( class InvalidFileException(msg: String) : Exception(msg) //http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 -const val STATE_RESUME_WINDOW = "resumeWindow" -const val STATE_RESUME_POSITION = "resumePosition" -const val STATE_PLAYER_FULLSCREEN = "playerFullscreen" -const val STATE_PLAYER_PLAYING = "playerOnPlay" const val ACTION_MEDIA_CONTROL = "media_control" const val EXTRA_CONTROL_TYPE = "control_type" -const val PLAYBACK_SPEED = "playback_speed" -const val RESIZE_MODE_KEY = "resize_mode" // Last used resize mode -const val PLAYBACK_SPEED_KEY = "playback_speed" // Last used playback speed -const val PREFERRED_SUBS_KEY = "preferred_subtitles" // Last used resize mode -//const val PLAYBACK_FASTFORWARD = "playback_fastforward" // Last used resize mode /** Abstract Exoplayer logic, can be expanded to other players */ interface IPlayer { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index 946c1d33..ffb4751f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -28,86 +28,122 @@ const val MIN_LOD = 3 interface IPreviewGenerator { fun hasPreview(): Boolean fun getPreviewImage(fraction: Float): Bitmap? - fun clear(keepCache: Boolean = false) fun release() + + var durationMs: Long + var loadedImages: Int } +/** PreviewGenerator that hides the implementation details of the sub generators that is used, used for source switch cache */ class PreviewGenerator : IPreviewGenerator { + /** the most up to date generator, will always mirror the actual source in the player */ private var currentGenerator: IPreviewGenerator = NoPreviewGenerator() + /** the longest generated preview of the same episode */ + private var lastGenerator: IPreviewGenerator = NoPreviewGenerator() + /** always NoPreviewGenerator, used as a cache for nothing */ + private val dummy: IPreviewGenerator = NoPreviewGenerator() + + /** if the current generator is the same as the last by checking time */ + private fun isSameLength(): Boolean = + currentGenerator.durationMs.minus(lastGenerator.durationMs).absoluteValue < 10_000L + + /** use the backup if the current generator is init or if they have the same length */ + private val backupGenerator: IPreviewGenerator + get() { + if (currentGenerator.durationMs == 0L || isSameLength()) { + return lastGenerator + } + return dummy + } + override fun hasPreview(): Boolean { - return currentGenerator.hasPreview() + return currentGenerator.hasPreview() || backupGenerator.hasPreview() } override fun getPreviewImage(fraction: Float): Bitmap? { return try { - currentGenerator.getPreviewImage(fraction) + currentGenerator.getPreviewImage(fraction) ?: backupGenerator.getPreviewImage(fraction) } catch (t: Throwable) { logError(t) null } } - override fun clear(keepCache: Boolean) { - currentGenerator.clear(keepCache) + override fun release() { + lastGenerator.release() + currentGenerator.release() + lastGenerator = NoPreviewGenerator() + currentGenerator = NoPreviewGenerator() } - override fun release() { - currentGenerator.release() + override var durationMs: Long + get() = currentGenerator.durationMs + set(_) {} + override var loadedImages: Int + get() = currentGenerator.loadedImages + set(_) {} + + fun clear(keepCache: Boolean) { + if (keepCache) { + if (!isSameLength() || currentGenerator.loadedImages >= lastGenerator.loadedImages || lastGenerator.durationMs == 0L) { + // the current generator is better than the last generator, therefore keep the current + // or the lengths are not the same, therefore favoring the more recent selection + + // if they are the same we favor the current generator + lastGenerator.release() + lastGenerator = currentGenerator + } else { + // otherwise just keep the last generator and throw away the current generator + currentGenerator.release() + } + } else { + // we switched the episode, therefore keep nothing + lastGenerator.release() + lastGenerator = NoPreviewGenerator() + currentGenerator.release() + // we assume that we set currentGenerator right after this, so currentGenerator != NoPreviewGenerator + } } fun load(link: ExtractorLink, keepCache: Boolean) { - val gen = currentGenerator + clear(keepCache) + when (link.type) { ExtractorLinkType.M3U8 -> { - if (gen is M3u8PreviewGenerator) { - gen.load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) - } else { - currentGenerator.release() - currentGenerator = M3u8PreviewGenerator().apply { - load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) - } + currentGenerator = M3u8PreviewGenerator().apply { + load(url = link.url, headers = link.getAllHeaders()) } } ExtractorLinkType.VIDEO -> { - if (gen is Mp4PreviewGenerator) { - gen.load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) - } else { - currentGenerator.release() - currentGenerator = Mp4PreviewGenerator().apply { - load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) - } + currentGenerator = Mp4PreviewGenerator().apply { + load(url = link.url, headers = link.getAllHeaders()) } } else -> { Log.i("PreviewImg", "unsupported format for $link") - currentGenerator.clear(keepCache) } } } fun load(context: Context, link: ExtractorUri, keepCache: Boolean) { - val gen = currentGenerator - if (gen is Mp4PreviewGenerator) { - gen.load(keepCache = keepCache, context = context, uri = link.uri) - } else { - currentGenerator.release() - currentGenerator = Mp4PreviewGenerator().apply { - load(keepCache = keepCache, context = context, uri = link.uri) - } + clear(keepCache) + currentGenerator = Mp4PreviewGenerator().apply { + load(keepCache = keepCache, context = context, uri = link.uri) } } } -class NoPreviewGenerator : IPreviewGenerator { +private class NoPreviewGenerator : IPreviewGenerator { override fun hasPreview(): Boolean = false override fun getPreviewImage(fraction: Float): Bitmap? = null - override fun clear(keepCache: Boolean) = Unit override fun release() = Unit + override var durationMs: Long = 0L + override var loadedImages: Int = 0 } -class M3u8PreviewGenerator : IPreviewGenerator { +private class M3u8PreviewGenerator : IPreviewGenerator { // generated images 1:1 to idx of hsl private var images: Array = arrayOf() @@ -118,7 +154,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { private var prefixSum: Array = arrayOf() // how many images has been generated - private var loadedImages: Int = 0 + override var loadedImages: Int = 0 // how many images we can generate in total, == hsl.size ?: 0 private var totalImages: Int = 0 @@ -155,7 +191,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { }*/ } - override fun clear(keepCache: Boolean) { + private fun clear() { synchronized(images) { currentJob?.cancel() images = arrayOf() @@ -170,9 +206,11 @@ class M3u8PreviewGenerator : IPreviewGenerator { images = arrayOf() } + override var durationMs: Long = 0L + private var currentJob: Job? = null - fun load(keepCache: Boolean, url: String, headers: Map) { - clear(keepCache) + fun load(url: String, headers: Map) { + clear() currentJob?.cancel() currentJob = ioSafe { withContext(Dispatchers.IO) { @@ -201,6 +239,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { // total duration of the entire m3u8 in seconds val duration = hsl.allTsLinks.sumOf { it.time ?: 0.0 } + durationMs = (duration * 1000.0).toLong() val durationInv = 1.0 / duration // if the total duration is less then 10s then something is very wrong or @@ -245,7 +284,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { if (!isActive) { return@withContext } - if(img == null || img.width <= 1 || img.height <= 1) continue + if (img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { images[index] = img loadedImages += 1 @@ -269,11 +308,11 @@ class M3u8PreviewGenerator : IPreviewGenerator { } } -class Mp4PreviewGenerator : IPreviewGenerator { +private class Mp4PreviewGenerator : IPreviewGenerator { // lod = level of detail where the number indicates how many ones there is // 2^(lod-1) = images private var loadedLod = 0 - private var loadedImages = 0 + override var loadedImages = 0 private var images = Array((1 shl MAX_LOD) - 1) { null } @@ -305,7 +344,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { if (idx > loadedImages) { break } - if(images[idx] == null) { + if (images[idx] == null) { continue } val currentFraction = @@ -325,7 +364,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { // also check out https://github.com/wseemann/FFmpegMediaMetadataRetriever private val retriever: MediaMetadataRetriever = MediaMetadataRetriever() - override fun clear(keepCache: Boolean) { + private fun clear(keepCache: Boolean) { if (keepCache) return synchronized(images) { loadedLod = 0 @@ -335,11 +374,11 @@ class Mp4PreviewGenerator : IPreviewGenerator { } private var currentJob: Job? = null - fun load(keepCache: Boolean, url: String, headers: Map) { + fun load(url: String, headers: Map) { currentJob?.cancel() currentJob = ioSafe { Log.i(TAG, "Loading with url = $url headers = $headers") - clear(keepCache) + clear(true) retriever.setDataSource(url, headers) start(this) } @@ -360,6 +399,8 @@ class Mp4PreviewGenerator : IPreviewGenerator { clear(false) } + override var durationMs: Long = 0L + @Throws @WorkerThread private fun start(scope: CoroutineScope) { @@ -368,6 +409,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { val durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: throw IllegalArgumentException("Bad video duration") + this.durationMs = durationMs val durationUs = (durationMs * 1000L).toFloat() //val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: throw IllegalArgumentException("Bad video width") //val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: throw IllegalArgumentException("Bad video height") @@ -388,7 +430,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { MediaMetadataRetriever.OPTION_CLOSEST_SYNC ) if (!scope.isActive) return - if(img == null || img.width <= 1 || img.height <= 1) continue + if (img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { images[idx] = img loadedImages = maxOf(loadedImages, idx) 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 bad78624..0e994be8 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 @@ -22,17 +22,22 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.getApiSettings -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.AllLanguagesName +import com.lagradost.cloudstream3.AnimeSearchResponse +import com.lagradost.cloudstream3.HomePageList +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSearchBinding import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding import com.lagradost.cloudstream3.mvvm.Resource @@ -53,8 +58,7 @@ 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 -import com.lagradost.cloudstream3.utils.DataStore.getKey -import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -63,9 +67,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import java.util.concurrent.locks.ReentrantLock -const val SEARCH_PREF_TAGS = "search_pref_tags" -const val SEARCH_PREF_PROVIDERS = "search_pref_providers" - class SearchFragment : Fragment() { companion object { fun List.filterSearchResponse(): List { @@ -194,7 +195,7 @@ class SearchFragment : Fragment() { validAPIs.flatMap { api -> api.supportedTypes }.distinct() ) { list -> if (selectedSearchTypes.toSet() != list.toSet()) { - setKey("$currentAccount/$SEARCH_PREF_TAGS", selectedSearchTypes) + DataStoreHelper.searchPreferenceTags = list selectedSearchTypes.clear() selectedSearchTypes.addAll(list) search(binding?.mainSearch?.query?.toString()) @@ -233,13 +234,7 @@ class SearchFragment : Fragment() { //searchMagIcon.scaleX = 0.65f //searchMagIcon.scaleY = 0.65f - context?.let { ctx -> - val validAPIs = ctx.filterProviderByPreferredMedia() - selectedApis = ctx.getKey( - "$currentAccount/$SEARCH_PREF_PROVIDERS", - defVal = validAPIs.map { it.name } - )!!.toMutableSet() - } + selectedApis = DataStoreHelper.searchPreferenceProviders.toMutableSet() binding?.searchFilter?.setOnClickListener { searchView -> searchView?.context?.let { ctx -> @@ -287,7 +282,7 @@ class SearchFragment : Fragment() { } fun updateList(types: List) { - setKey("$currentAccount/$SEARCH_PREF_TAGS", types.map { it.name }) + DataStoreHelper.searchPreferenceTags = types arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> @@ -312,12 +307,7 @@ class SearchFragment : Fragment() { arrayAdapter.notifyDataSetChanged() } - val selectedSearchTypes = getKey>("$currentAccount/$SEARCH_PREF_TAGS") - ?.mapNotNull { listName -> - TvType.values().firstOrNull { it.name == listName } - } - ?.toMutableList() - ?: mutableListOf(TvType.Movie, TvType.TvSeries) + val selectedSearchTypes = DataStoreHelper.searchPreferenceTags bindChips( binding.tvtypesChipsScroll.tvtypesChips, @@ -343,7 +333,7 @@ class SearchFragment : Fragment() { } dialog.setOnDismissListener { - context?.setKey("$currentAccount/$SEARCH_PREF_PROVIDERS", currentSelectedApis.toList()) + DataStoreHelper.searchPreferenceProviders = currentSelectedApis.toList() selectedApis = currentSelectedApis } updateList(selectedSearchTypes.toList()) @@ -354,10 +344,7 @@ class SearchFragment : Fragment() { val settingsManager = context?.let { PreferenceManager.getDefaultSharedPreferences(it) } val isAdvancedSearch = settingsManager?.getBoolean("advanced_search", true) ?: true - selectedSearchTypes = context?.getKey>("$currentAccount/$SEARCH_PREF_TAGS") - ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } - ?.toMutableList() - ?: mutableListOf(TvType.Movie, TvType.TvSeries) + selectedSearchTypes = DataStoreHelper.searchPreferenceTags.toMutableList() if (isTrueTvSettings()) { binding?.searchFilter?.isFocusable = true 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 775cb718..10c0546f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -10,7 +10,9 @@ import androidx.core.widget.doOnTextChanged import com.fasterxml.jackson.annotation.JsonProperty import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey @@ -31,6 +33,8 @@ import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe +import kotlin.reflect.KClass +import kotlin.reflect.KProperty const val VIDEO_POS_DUR = "video_pos_dur" const val VIDEO_WATCH_STATE = "video_watch_state" @@ -44,6 +48,28 @@ const val RESULT_EPISODE = "result_episode" const val RESULT_SEASON = "result_season" const val RESULT_DUB = "result_dub" + +class UserPreferenceDelegate( + private val key: String, private val default: T //, private val klass: KClass +) { + private val klass: KClass = default::class + private val realKey get() = "${DataStoreHelper.currentAccount}/$key" + operator fun getValue(self: Any?, property: KProperty<*>) = + AcraApplication.getKeyClass(realKey, klass.java) ?: default + + operator fun setValue( + self: Any?, + property: KProperty<*>, + t: T? + ) { + if (t == null) { + removeKey(realKey) + } else { + AcraApplication.setKeyClass(realKey, t) + } + } +} + object DataStoreHelper { // be aware, don't change the index of these as Account uses the index for the art private val profileImages = arrayOf( @@ -56,6 +82,48 @@ object DataStoreHelper { R.drawable.profile_bg_teal ) + private var searchPreferenceProvidersStrings : List by UserPreferenceDelegate( + /** java moment right here, as listOf()::class.java != List(0) { "" }::class.java */ + "search_pref_providers", List(0) { "" } + ) + + private fun serializeTv(data : List) : List = data.map { it.name } + + private fun deserializeTv(data : List) : List { + return data.mapNotNull { listName -> + TvType.values().firstOrNull { it.name == listName } + } + } + + var searchPreferenceProviders : List + get() { + val ret = searchPreferenceProvidersStrings + return ret.ifEmpty { + context?.filterProviderByPreferredMedia()?.map { it.name } ?: emptyList() + } + } set(value) { + searchPreferenceProvidersStrings = value + } + + private var searchPreferenceTagsStrings : List by UserPreferenceDelegate("search_pref_tags", listOf(TvType.Movie, TvType.TvSeries).map { it.name }) + var searchPreferenceTags : List + get() = deserializeTv(searchPreferenceTagsStrings) + set(value) { + searchPreferenceTagsStrings = serializeTv(value) + } + + + private var homePreferenceStrings : List by UserPreferenceDelegate("home_pref_homepage", listOf(TvType.Movie, TvType.TvSeries).map { it.name }) + var homePreference : List + get() = deserializeTv(homePreferenceStrings) + set(value) { + homePreferenceStrings = serializeTv(value) + } + + var homeBookmarkedList : IntArray by UserPreferenceDelegate("home_bookmarked_last_list", IntArray(0)) + var playBackSpeed : Float by UserPreferenceDelegate("playback_speed", 1.0f) + var resizeMode : Int by UserPreferenceDelegate("resize_mode", 0) + data class Account( @JsonProperty("keyIndex") val keyIndex: Int, diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 0f76e4dd..38df4c5b 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -349,14 +349,15 @@ android:layout_width="150dp" android:layout_height="40dp" android:layout_marginEnd="100dp" + android:layout_marginTop="60dp" android:backgroundTint="@color/skipOpTransparent" android:maxLines="1" android:padding="10dp" android:textColor="@color/white" android:visibility="gone" app:cornerRadius="@dimen/rounded_button_radius" - app:layout_constraintBottom_toTopOf="@+id/bottom_player_bar" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@+id/player_top_holder" app:strokeColor="@color/white" app:strokeWidth="1dp" tools:text="Skip Opening" @@ -435,6 +436,7 @@ android:id="@+id/player_video_bar" android:layout_width="match_parent" android:layout_height="wrap_content" + tools:visibility="visible" android:layoutDirection="ltr" android:orientation="horizontal"> From f14557fe6a3268b07ea1421df109367e7d040da0 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 7 Oct 2023 01:54:34 +0200 Subject: [PATCH 255/570] lib fix --- .../lagradost/cloudstream3/ui/library/LibraryFragment.kt | 8 ++++++++ .../lagradost/cloudstream3/ui/library/LibraryViewModel.kt | 2 ++ 2 files changed, 10 insertions(+) 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 index d5fdc1aa..a3cc16c9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -15,6 +15,7 @@ import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayoutMediator import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.allProviders @@ -229,6 +230,7 @@ class LibraryFragment : Fragment() { } binding?.viewpager?.setPageTransformer(LibraryScrollTransformer()) + binding?.viewpager?.adapter = binding?.viewpager?.adapter ?: ViewpagerAdapter( mutableListOf(), @@ -357,6 +359,7 @@ class LibraryFragment : Fragment() { 0, viewpager.adapter?.itemCount ?: 0 ) + binding?.viewpager?.setCurrentItem(libraryViewModel.currentPage, false) // Only stop loading after 300ms to hide the fade effect the viewpager produces when updating // Without this there would be a flashing effect: @@ -415,6 +418,11 @@ class LibraryFragment : Fragment() { } } } + binding?.viewpager?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + libraryViewModel.currentPage = position + } + }) } override fun onConfigurationChanged(newConfig: Configuration) { 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 index c104a7c3..e590b151 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -30,6 +30,8 @@ class LibraryViewModel : ViewModel() { private val _pages: MutableLiveData>> = MutableLiveData(null) val pages: LiveData>> = _pages + var currentPage : Int = 0 + private val _currentApiName: MutableLiveData = MutableLiveData("") val currentApiName: LiveData = _currentApiName From 33eb3a3b29a1dd7dd21a0847222bac1e6cc04bf5 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 7 Oct 2023 21:48:24 +0200 Subject: [PATCH 256/570] lib fix2 --- .../ui/library/LibraryViewModel.kt | 32 +++++++++++++------ .../cloudstream3/utils/DataStoreHelper.kt | 2 ++ 2 files changed, 25 insertions(+), 9 deletions(-) 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 index e590b151..b44913d9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -12,6 +12,7 @@ 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 com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount enum class ListSorting(@StringRes val stringRes: Int) { @@ -30,7 +31,7 @@ class LibraryViewModel : ViewModel() { private val _pages: MutableLiveData>> = MutableLiveData(null) val pages: LiveData>> = _pages - var currentPage : Int = 0 + var currentPage: Int = 0 private val _currentApiName: MutableLiveData = MutableLiveData("") val currentApiName: LiveData = _currentApiName @@ -62,13 +63,21 @@ class LibraryViewModel : ViewModel() { reloadPages(true) } - fun sort(method: ListSorting, query: String? = null) { - val currentList = pages.value ?: return + fun sort(method: ListSorting, query: String? = null) = ioSafe { + val value = _pages.value ?: return@ioSafe + if (value is Resource.Success) { + sort(method, query, value.value) + } + } + + private fun sort(method: ListSorting, query: String? = null, items: List) { currentSortingMethod = method - (currentList as? Resource.Success)?.value?.forEachIndexed { _, page -> + DataStoreHelper.librarySortingMode = method.ordinal + + items.forEach { page -> page.sort(method, query) } - _pages.postValue(currentList) + _pages.postValue(Resource.Success(items)) } fun reloadPages(forceReload: Boolean) { @@ -89,8 +98,6 @@ class LibraryViewModel : ViewModel() { val library = (libraryResource as? Resource.Success)?.value ?: return@let sortingMethods = library.supportedListSorting.toList() - currentSortingMethod = null - repo.requireLibraryRefresh = false val pages = library.allLibraryLists.map { @@ -100,11 +107,18 @@ class LibraryViewModel : ViewModel() { ) } - _pages.postValue(Resource.Success(pages)) + val desiredSortingMethod = + ListSorting.values().getOrNull(DataStoreHelper.librarySortingMode) + if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) { + sort(desiredSortingMethod, null, pages) + } else { + // null query = no sorting + sort(ListSorting.Query, null, pages) + } } } } - + init { MainActivity.reloadHomeEvent += ::reloadPages } 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 10c0546f..952422a4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -26,6 +26,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WhoIsWatchingAdapter +import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState @@ -123,6 +124,7 @@ object DataStoreHelper { var homeBookmarkedList : IntArray by UserPreferenceDelegate("home_bookmarked_last_list", IntArray(0)) var playBackSpeed : Float by UserPreferenceDelegate("playback_speed", 1.0f) var resizeMode : Int by UserPreferenceDelegate("resize_mode", 0) + var librarySortingMode : Int by UserPreferenceDelegate("library_sorting_mode", ListSorting.AlphabeticalA.ordinal) data class Account( @JsonProperty("keyIndex") From d277d8a9aabce833b6384948716a5c3d8c680807 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:56:30 +0200 Subject: [PATCH 257/570] bump upstream --- app/build.gradle.kts | 36 +++++++++++++++--------------- app/src/main/res/values/styles.xml | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9f484c48..639932c4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,16 +50,16 @@ android { } } - compileSdk = 33 + compileSdk = 34 buildToolsVersion = "34.0.0" defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 33 + targetSdk = 34 versionCode = 62 - versionName = "4.2.0" + versionName = "4.2.1" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") @@ -154,18 +154,18 @@ repositories { dependencies { implementation("com.google.android.mediahome:video:1.0.0") implementation("androidx.test.ext:junit-ktx:1.1.5") - testImplementation("org.json:json:20180813") + testImplementation("org.json:json:20230618") - implementation("androidx.core:core-ktx:1.10.1") + implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") // need target 32 for 1.5.0 // dont change this to 1.6.0 it looks ugly af - implementation("com.google.android.material:material:1.5.0") + implementation("com.google.android.material:material:1.10.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("androidx.navigation:navigation-fragment-ktx:2.6.0") - implementation("androidx.navigation:navigation-ui-ktx:2.6.0") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.4") + implementation("androidx.navigation:navigation-ui-ktx:2.7.4") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") @@ -173,9 +173,9 @@ dependencies { // implementation("io.karn:khttp-android:0.1.2") //okhttp instead // implementation("org.jsoup:jsoup:1.13.1") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2") - implementation("androidx.preference:preference-ktx:1.2.0") + implementation("androidx.preference:preference-ktx:1.2.1") implementation("com.github.bumptech.glide:glide:4.13.1") kapt("com.github.bumptech.glide:compiler:4.13.1") @@ -200,14 +200,14 @@ dependencies { implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Bug reports - implementation("ch.acra:acra-core:5.11.0") - implementation("ch.acra:acra-toast:5.11.0") + implementation("ch.acra:acra-core:5.11.2") + implementation("ch.acra:acra-toast:5.11.2") - compileOnly("com.google.auto.service:auto-service-annotations:1.0") + compileOnly("com.google.auto.service:auto-service-annotations:1.1.1") //either for java sources: - annotationProcessor("com.google.auto.service:auto-service:1.0") + annotationProcessor("com.google.auto.service:auto-service:1.1.1") //or for kotlin sources (requires kapt gradle plugin): - kapt("com.google.auto.service:auto-service:1.0") + kapt("com.google.auto.service:auto-service:1.1.1") // subtitle color picker implementation("com.jaredrummler:colorpicker:1.1.0") @@ -229,7 +229,7 @@ dependencies { // implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") implementation("com.github.Blatzar:NiceHttp:0.4.3") // To fix SSL fuckery on android 9 - implementation("org.conscrypt:conscrypt-android:2.2.1") + implementation("org.conscrypt:conscrypt-android:2.5.2") // Util to skip the URI file fuckery 🙏 implementation("com.github.LagradOst:SafeFile:0.0.5") diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e2f11221..5f7adea4 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -388,7 +388,7 @@ true @null @null - @color/transparent + ?attr/primaryBlackBackground @drawable/rounded_dialog 512dp From 5b4fd8d77de24dbc012d6c2814e35db26d251fa9 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 10 Oct 2023 09:05:34 -0600 Subject: [PATCH 258/570] Fix issue where DownloadedPlayerActivity interferes with MainActivity (#674) * Fix issue where DownloadedPlayerActivity interferes with MainActivity --- app/src/main/AndroidManifest.xml | 4 ++- .../lagradost/cloudstream3/CommonActivity.kt | 27 ++++++++++++------- .../lagradost/cloudstream3/MainActivity.kt | 2 ++ .../ui/player/DownloadedPlayerActivity.kt | 5 ++++ 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 15767d7b..503cd76b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -61,7 +61,9 @@ android:exported="true" android:resizeableActivity="true" android:screenOrientation="userLandscape" - android:supportsPictureInPicture="true"> + android:supportsPictureInPicture="true" + android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer" + android:launchMode="singleTask"> diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index a7d899b6..16a438b3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -65,6 +65,11 @@ object CommonActivity { _activity = WeakReference(value) } + @MainThread + fun setActivityInstance(newActivity: Activity?) { + activity = newActivity + } + @MainThread fun Activity?.getCastSession(): CastSession? { return (this as MainActivity?)?.mSessionManager?.currentCastSession @@ -203,23 +208,25 @@ object CommonActivity { setLocale(this, localeCode) } - fun init(act: ComponentActivity?) { - if (act == null) return - activity = act + fun init(act: Activity) { + setActivityInstance(act) + + val componentActivity = activity as? ComponentActivity ?: return + //https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission //https://developer.android.com/guide/topics/ui/picture-in-picture canShowPipMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT - act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN - act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS + componentActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN + componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS - act.updateLocale() - act.updateTv() + componentActivity.updateLocale() + componentActivity.updateTv() NewPipe.init(DownloaderTestImpl.getInstance()) for (resumeApp in resumeApps) { resumeApp.launcher = - act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val resultCode = result.resultCode val data = result.data if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) { @@ -236,11 +243,11 @@ object CommonActivity { // Ask for notification permissions on Android 13 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission( - act, + componentActivity, Manifest.permission.POST_NOTIFICATIONS ) != PackageManager.PERMISSION_GRANTED ) { - val requestPermissionLauncher = act.registerForActivityResult( + val requestPermissionLauncher = componentActivity.registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> Log.d(TAG, "Notification permission: $isGranted") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 17823f7c..5595c377 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -67,6 +67,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint import com.lagradost.cloudstream3.CommonActivity.screenHeight +import com.lagradost.cloudstream3.CommonActivity.setActivityInstance import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale import com.lagradost.cloudstream3.databinding.ActivityMainBinding @@ -590,6 +591,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { override fun onResume() { super.onResume() afterPluginsLoadedEvent += ::onAllPluginsLoaded + setActivityInstance(this) try { if (isCastApiAvailable()) { //mCastSession = mSessionManager.currentCastSession diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index d181e175..4c3376bb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -110,4 +110,9 @@ class DownloadedPlayerActivity : AppCompatActivity() { return } } + + override fun onResume() { + super.onResume() + CommonActivity.setActivityInstance(this) + } } \ No newline at end of file From b120a7bce206a1c7f537dc686446be3f0255eac6 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 10 Oct 2023 18:16:35 +0300 Subject: [PATCH 259/570] Library on TV (#663) * implementation for Library on TV --- .../cloudstream3/ExampleInstrumentedTest.kt | 4 + .../lagradost/cloudstream3/MainActivity.kt | 6 +- .../ui/library/LibraryFragment.kt | 98 +++++++-- .../ui/library/ViewpagerAdapter.kt | 5 +- app/src/main/res/layout/fragment_library.xml | 17 +- .../main/res/layout/fragment_library_tv.xml | 200 ++++++++++++++++++ .../res/layout/library_viewpager_page.xml | 2 + 7 files changed, 305 insertions(+), 27 deletions(-) create mode 100644 app/src/main/res/layout/fragment_library_tv.xml diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index df41ef91..a84b2457 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -9,6 +9,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding +import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding +import com.lagradost.cloudstream3.databinding.FragmentLibraryTvBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding import com.lagradost.cloudstream3.databinding.FragmentResultBinding @@ -120,6 +122,8 @@ class ExampleInstrumentedTest { testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent) testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent) + testAllLayouts(activity, R.layout.fragment_library_tv, R.layout.fragment_library) + testAllLayouts(activity, R.layout.fragment_library_tv, R.layout.fragment_library) } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 5595c377..f9fff88c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -543,9 +543,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { navRailView.isVisible = isNavVisible && landscape // Hide library on TV since it is not supported yet :( - val isTrueTv = isTrueTvSettings() - navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv - navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + //val isTrueTv = isTrueTvSettings() + //navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + //navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv // Hide downloads on TV //navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv 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 index a3cc16c9..85f0aedd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -1,38 +1,52 @@ package com.lagradost.cloudstream3.ui.library +import android.annotation.SuppressLint 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 android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.FOCUS_AFTER_DESCENDANTS +import android.view.ViewGroup.FOCUS_BLOCK_DESCENDANTS import android.view.animation.AlphaAnimation +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast import androidx.annotation.StringRes import androidx.appcompat.widget.SearchView +import androidx.core.view.allViews import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.viewpager2.widget.ViewPager2 +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.tabs.TabLayout 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.CommonActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.debugAssert +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.ui.AutofitRecyclerView 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.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity @@ -80,9 +94,21 @@ class LibraryFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val localBinding = FragmentLibraryBinding.inflate(inflater, container, false) - binding = localBinding - return localBinding.root + val layout = + if (SettingsFragment.isTvSettings()) R.layout.fragment_library_tv else R.layout.fragment_library + val root = inflater.inflate(layout, container, false) + binding = try { + FragmentLibraryBinding.bind(root) + } catch (t: Throwable) { + CommonActivity.showToast( + txt(R.string.unable_to_inflate, t.message ?: ""), + Toast.LENGTH_LONG + ) + logError(t) + null + } + + return root //return inflater.inflate(R.layout.fragment_library, container, false) } @@ -99,24 +125,16 @@ class LibraryFragment : Fragment() { super.onSaveInstanceState(outState) } + @SuppressLint("ResourceType", "CutPasteId") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) fixPaddingStatusbar(binding?.searchStatusBarPadding) - binding?.sortFab?.setOnClickListener { - val methods = libraryViewModel.sortingMethods.map { - txt(it.stringRes).asString(view.context) - } + binding?.sortFab?.setOnClickListener(sortChangeClickListener) + binding?.librarySort?.setOnClickListener(sortChangeClickListener) - 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) - }) + binding?.libraryRoot?.findViewById(R.id.search_src_text)?.apply { + tag = "tv_no_focus_tag" } binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { @@ -266,7 +284,10 @@ class LibraryFragment : Fragment() { // selects the whole list opener val savedListSelection = getKey("$currentAccount/$LIBRARY_FOLDER", syncName.name) - val savedSelection = getKey("$currentAccount/$LIBRARY_FOLDER", syncId).takeIf { + val savedSelection = getKey( + "$currentAccount/$LIBRARY_FOLDER", + syncId + ).takeIf { it?.openType != LibraryOpenerType.Default } ?: savedListSelection @@ -354,6 +375,12 @@ class LibraryFragment : Fragment() { } (viewpager.adapter as? ViewpagerAdapter)?.pages = pages + //fix focus on the viewpager itself + (viewpager.getChildAt(0) as RecyclerView).apply { + tag = "tv_no_focus_tag" + //isFocusable = false + } + // Using notifyItemRangeChanged keeps the animations when sorting viewpager.adapter?.notifyItemRangeChanged( 0, @@ -396,6 +423,9 @@ class LibraryFragment : Fragment() { viewpager, ) { tab, position -> tab.text = pages.getOrNull(position)?.title?.asStringNull(context) + tab.view.tag = "tv_no_focus_tag" + tab.view.nextFocusDownId = R.id.search_result_root + tab.view.setOnClickListener { val currentItem = binding?.viewpager?.currentItem ?: return@setOnClickListener @@ -418,17 +448,45 @@ class LibraryFragment : Fragment() { } } } - binding?.viewpager?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + binding?.viewpager?.registerOnPageChangeCallback(object : + ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { - libraryViewModel.currentPage = position + val all = binding?.viewpager?.allViews?.toList() + ?.filterIsInstance() + + all?.forEach { view -> + view.isVisible = view.tag == position + view.isFocusable = view.tag == position + + if (view.tag == position) + view.descendantFocusability = FOCUS_AFTER_DESCENDANTS + else + view.descendantFocusability = FOCUS_BLOCK_DESCENDANTS + } + super.onPageSelected(position) } }) } - override fun onConfigurationChanged(newConfig: Configuration) { (binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind() super.onConfigurationChanged(newConfig) } + + private val sortChangeClickListener = View.OnClickListener { view -> + 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) + }) + } } class MenuSearchView(context: Context) : SearchView(context) { 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 index 95fefcbe..76028487 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -25,7 +25,7 @@ class ViewpagerAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is PageViewHolder -> { - holder.bind(pages[position], unbound.remove(position)) + holder.bind(pages[position], position, unbound.remove(position)) } } } @@ -43,7 +43,8 @@ class ViewpagerAdapter( inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(page: SyncAPI.Page, rebind: Boolean) { + fun bind(page: SyncAPI.Page, position: Int, rebind: Boolean) { + binding.pageRecyclerview.tag = position binding.pageRecyclerview.apply { spanCount = this@PageViewHolder.itemView.context.getSpanCount() ?: 3 diff --git a/app/src/main/res/layout/fragment_library.xml b/app/src/main/res/layout/fragment_library.xml index 985d055d..879ddbd9 100644 --- a/app/src/main/res/layout/fragment_library.xml +++ b/app/src/main/res/layout/fragment_library.xml @@ -41,6 +41,20 @@ android:src="@drawable/ic_baseline_extension_24" app:tint="?attr/textColor" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/library_viewpager_page.xml b/app/src/main/res/layout/library_viewpager_page.xml index 7d278cff..aa9745fb 100644 --- a/app/src/main/res/layout/library_viewpager_page.xml +++ b/app/src/main/res/layout/library_viewpager_page.xml @@ -5,5 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" + android:focusable="false" + android:tag="tv_no_focus_tag" tools:listitem="@layout/home_result_grid_expanded" /> From abbad1bc949588009bb1cf4c22405e33d790d09c Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 10 Oct 2023 18:17:18 +0300 Subject: [PATCH 260/570] Delete Focus frame from empty Downloads list & Search TV Layout (#675) * Delete Focus frame in search TV layout * Delete focus frame for empty Downloads list * Chip rounded stroke frame --- .../com/lagradost/cloudstream3/ui/search/SearchFragment.kt | 3 ++- app/src/main/res/layout/fragment_downloads.xml | 1 + app/src/main/res/layout/fragment_search_tv.xml | 2 ++ app/src/main/res/values/styles.xml | 5 +++-- 4 files changed, 8 insertions(+), 3 deletions(-) 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 0e994be8..ce92d723 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 @@ -11,6 +11,7 @@ import android.widget.AbsListView import android.widget.ArrayAdapter import android.widget.ImageView import android.widget.ListView +import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible @@ -221,7 +222,7 @@ class SearchFragment : Fragment() { SearchHelper.handleSearchClickCallback(callback) } - + searchRoot.findViewById(R.id.search_src_text)?.tag = "tv_no_focus_tag" searchAutofitResults.adapter = adapter searchLoadingBar.alpha = 0f } diff --git a/app/src/main/res/layout/fragment_downloads.xml b/app/src/main/res/layout/fragment_downloads.xml index 65f36209..5623bc7e 100644 --- a/app/src/main/res/layout/fragment_downloads.xml +++ b/app/src/main/res/layout/fragment_downloads.xml @@ -137,6 +137,7 @@ android:background="?attr/primaryBlackBackground" android:descendantFocusability="afterDescendants" android:nextFocusLeft="@id/nav_rail_view" + android:tag = "@string/tv_no_focus_tag" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:listitem="@layout/download_header_episode" /> diff --git a/app/src/main/res/layout/fragment_search_tv.xml b/app/src/main/res/layout/fragment_search_tv.xml index b3f88cd2..e34b0ac3 100644 --- a/app/src/main/res/layout/fragment_search_tv.xml +++ b/app/src/main/res/layout/fragment_search_tv.xml @@ -85,6 +85,7 @@ android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" android:nextFocusDown="@id/tvtypes_chips_scroll" + android:tag = "@string/tv_no_focus_tag" android:src="@drawable/ic_baseline_tune_24" app:tint="?attr/textColor" /> @@ -141,6 +142,7 @@ android:nextFocusLeft="@id/nav_rail_view" android:nextFocusUp="@id/tvtypes_chips" android:nextFocusDown="@id/search_clear_call_history" + android:tag = "@string/tv_no_focus_tag" android:paddingBottom="50dp" android:visibility="visible" tools:listitem="@layout/search_history_item" /> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5f7adea4..c00970d3 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -86,8 +86,8 @@ From 91b195241e1f6a07a037732376c7007e4cbc3f30 Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Tue, 10 Oct 2023 15:19:27 +0000 Subject: [PATCH 261/570] Automatic backups (#592) Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> --- .../lagradost/cloudstream3/MainActivity.kt | 2 +- .../services/BackupWorkManager.kt | 96 +++++++++++++++++++ .../ui/settings/SettingsUpdates.kt | 37 ++++++- .../cloudstream3/utils/BackupUtils.kt | 52 +++++----- .../lagradost/cloudstream3/utils/UIHelper.kt | 2 +- app/src/main/res/values/array.xml | 22 ++++- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/settings_updates.xml | 15 ++- 8 files changed, 193 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index f9fff88c..51032a6e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1110,7 +1110,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (appVer != lastAppAutoBackup) { setKey("VERSION_NAME", BuildConfig.VERSION_NAME) normalSafeApiCall { - backup() + backup(this) } normalSafeApiCall { // Recompile oat on new version diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt new file mode 100644 index 00000000..6ed7a447 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt @@ -0,0 +1,96 @@ +package com.lagradost.cloudstream3.services + +import android.content.Context +import androidx.core.app.NotificationCompat +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ForegroundInfo +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.BackupUtils +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import java.util.concurrent.TimeUnit + +const val BACKUP_CHANNEL_ID = "cloudstream3.backups" +const val BACKUP_WORK_NAME = "work_backup" +const val BACKUP_CHANNEL_NAME = "Backups" +const val BACKUP_CHANNEL_DESCRIPTION = "Notifications for background backups" +const val BACKUP_NOTIFICATION_ID = 938712898 // Random unique + +class BackupWorkManager(val context: Context, workerParams: WorkerParameters) : + CoroutineWorker(context, workerParams) { + companion object { + fun enqueuePeriodicWork(context: Context?, intervalHours: Long) { + if (context == null) return + + if (intervalHours == 0L) { + WorkManager.getInstance(context).cancelUniqueWork(BACKUP_WORK_NAME) + return + } + + val constraints = Constraints.Builder() + .setRequiresStorageNotLow(true) + .build() + + val periodicSyncDataWork = + PeriodicWorkRequest.Builder( + BackupWorkManager::class.java, + intervalHours, + TimeUnit.HOURS + ) + .addTag(BACKUP_WORK_NAME) + .setConstraints(constraints) + .build() + + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + BACKUP_WORK_NAME, + ExistingPeriodicWorkPolicy.UPDATE, + periodicSyncDataWork + ) + + // Uncomment below for testing + +// val oneTimeBackupWork = +// OneTimeWorkRequest.Builder(BackupWorkManager::class.java) +// .addTag(BACKUP_WORK_NAME) +// .setConstraints(constraints) +// .build() +// +// WorkManager.getInstance(context).enqueue(oneTimeBackupWork) + } + } + + private val backupNotificationBuilder = + NotificationCompat.Builder(context, BACKUP_CHANNEL_ID) + .setColorized(true) + .setOnlyAlertOnce(true) + .setSilent(true) + .setAutoCancel(true) + .setContentTitle(context.getString(R.string.pref_category_backup)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setColor(context.colorFromAttribute(R.attr.colorPrimary)) + .setSmallIcon(R.drawable.ic_cloudstream_monochrome_big) + + override suspend fun doWork(): Result { + context.createNotificationChannel( + BACKUP_CHANNEL_ID, + BACKUP_CHANNEL_NAME, + BACKUP_CHANNEL_DESCRIPTION + ) + + setForeground( + ForegroundInfo( + BACKUP_NOTIFICATION_ID, + backupNotificationBuilder.build() + ) + ) + + BackupUtils.backup(context) + + return Result.success() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index 62e46c08..2f796801 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -19,14 +19,16 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.databinding.LogcatBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.network.initClient +import com.lagradost.cloudstream3.services.BackupWorkManager import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.BackupUtils.backup +import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.VideoDownloadManager @@ -48,7 +50,30 @@ class SettingsUpdates : PreferenceFragmentCompat() { //val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) getPref(R.string.backup_key)?.setOnPreferenceClickListener { - activity?.backup() + BackupUtils.backup(activity) + return@setOnPreferenceClickListener true + } + + getPref(R.string.automatic_backup_key)?.setOnPreferenceClickListener { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) + + val prefNames = resources.getStringArray(R.array.periodic_work_names) + val prefValues = resources.getIntArray(R.array.periodic_work_values) + val current = settingsManager.getInt(getString(R.string.automatic_backup_key), 0) + + activity?.showDialog( + prefNames.toList(), + prefValues.indexOf(current), + getString(R.string.backup_frequency), + true, + {}) { index -> + settingsManager.edit() + .putInt(getString(R.string.automatic_backup_key), prefValues[index]).apply() + BackupWorkManager.enqueuePeriodicWork( + context ?: AcraApplication.context, + prefValues[index].toLong() + ) + } return@setOnPreferenceClickListener true } @@ -65,7 +90,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { val builder = AlertDialog.Builder(pref.context, R.style.AlertDialogCustom) - val binding = LogcatBinding.inflate(layoutInflater,null,false ) + val binding = LogcatBinding.inflate(layoutInflater, null, false) builder.setView(binding.root) val dialog = builder.create() @@ -176,7 +201,8 @@ class SettingsUpdates : PreferenceFragmentCompat() { val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context) val prefNames = resources.getStringArray(R.array.auto_download_plugin) - val prefValues = enumValues().sortedBy { x -> x.value }.map { x -> x.value } + val prefValues = + enumValues().sortedBy { x -> x.value }.map { x -> x.value } val current = settingsManager.getInt(getString(R.string.auto_download_plugins_key), 0) @@ -186,7 +212,8 @@ class SettingsUpdates : PreferenceFragmentCompat() { getString(R.string.automatic_plugin_download_mode_title), true, {}) { - settingsManager.edit().putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() + settingsManager.edit() + .putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } return@setOnPreferenceClickListener true 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 96593769..e50131fe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -10,6 +10,7 @@ import androidx.annotation.WorkerThread import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.module.kotlin.readValue +import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError @@ -90,9 +91,11 @@ object BackupUtils { ) @Suppress("UNCHECKED_CAST") - fun Context.getBackup(): BackupFile { - val allData = getSharedPrefs().all.filter { it.key.isTransferable() } - val allSettings = getDefaultSharedPrefs().all.filter { it.key.isTransferable() } + private fun getBackup(context: Context?): BackupFile? { + if (context == null) return null + + val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() } + val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() } val allDataSorted = BackupVars( allData.filter { it.value is Boolean } as? Map, @@ -119,46 +122,50 @@ object BackupUtils { } @WorkerThread - fun Context.restore( + fun restore( + context: Context?, backupFile: BackupFile, restoreSettings: Boolean, restoreDataStore: Boolean ) { + if (context == null) return if (restoreSettings) { - restoreMap(backupFile.settings._Bool, true) - restoreMap(backupFile.settings._Int, true) - restoreMap(backupFile.settings._String, true) - restoreMap(backupFile.settings._Float, true) - restoreMap(backupFile.settings._Long, true) - restoreMap(backupFile.settings._StringSet, true) + context.restoreMap(backupFile.settings._Bool, true) + context.restoreMap(backupFile.settings._Int, true) + context.restoreMap(backupFile.settings._String, true) + context.restoreMap(backupFile.settings._Float, true) + context.restoreMap(backupFile.settings._Long, true) + context.restoreMap(backupFile.settings._StringSet, true) } if (restoreDataStore) { - restoreMap(backupFile.datastore._Bool) - restoreMap(backupFile.datastore._Int) - restoreMap(backupFile.datastore._String) - restoreMap(backupFile.datastore._Float) - restoreMap(backupFile.datastore._Long) - restoreMap(backupFile.datastore._StringSet) + context.restoreMap(backupFile.datastore._Bool) + context.restoreMap(backupFile.datastore._Int) + context.restoreMap(backupFile.datastore._String) + context.restoreMap(backupFile.datastore._Float) + context.restoreMap(backupFile.datastore._Long) + context.restoreMap(backupFile.datastore._StringSet) } } @SuppressLint("SimpleDateFormat") - fun FragmentActivity.backup() = ioSafe { + fun backup(context: Context?) = ioSafe { + if (context == null) return@ioSafe + var fileStream: OutputStream? = null var printStream: PrintWriter? = null try { - if (!checkWrite()) { + if (!context.checkWrite()) { showToast(R.string.backup_failed, Toast.LENGTH_LONG) - requestRW() + context.getActivity()?.requestRW() return@ioSafe } val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) val ext = "txt" val displayName = "CS3_Backup_${date}" - val backupFile = getBackup() - val stream = setupStream(this@backup, displayName, null, ext, false) + val backupFile = getBackup(context) + val stream = setupStream(context, displayName, null, ext, false) fileStream = stream.openNew() printStream = PrintWriter(fileStream) @@ -198,7 +205,8 @@ object BackupUtils { val restoredValue = mapper.readValue(input) - activity.restore( + restore( + activity, restoredValue, restoreSettings = true, restoreDataStore = true 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 038a2f11..9b40e70e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -71,7 +71,7 @@ object UIHelper { val Int.toDp: Int get() = (this / Resources.getSystem().displayMetrics.density).toInt() val Float.toDp: Float get() = (this / Resources.getSystem().displayMetrics.density) - fun Activity.checkWrite(): Boolean { + fun Context.checkWrite(): Boolean { return (ContextCompat.checkSelfPermission( this, Manifest.permission.WRITE_EXTERNAL_STORAGE diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index 1df7b9d6..b8f0cbf8 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -41,7 +41,7 @@ 0 1 - + @string/disable @@ -126,6 +126,26 @@ 30min + + @string/none + 3h + 6h + 12h + 24h + 3d + 7d + + + + 0 + 3 + 6 + 12 + 24 + 72 + 168 + + 0 60 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13251c7c..c722f33f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ primary_color_key restore_key backup_key + automatic_backup_key prefer_media_type_key_2 app_theme_key episode_sync_enabled_key @@ -229,6 +230,7 @@ Automatically sync your current episode progress Restore data from backup Back up data + Backup frequency Loaded backup file Failed to restore data from file %s Data stored diff --git a/app/src/main/res/xml/settings_updates.xml b/app/src/main/res/xml/settings_updates.xml index 9989e47b..e3b36648 100644 --- a/app/src/main/res/xml/settings_updates.xml +++ b/app/src/main/res/xml/settings_updates.xml @@ -9,7 +9,7 @@ android:summaryOn="@string/bug_report_settings_on" android:title="@string/pref_disable_acra" /> - - + + - - + - Date: Tue, 10 Oct 2023 18:05:31 +0200 Subject: [PATCH 262/570] reverted to instant outline --- .../lagradost/cloudstream3/MainActivity.kt | 55 +++++---- app/src/main/res/drawable/outline.xml | 4 +- .../main/res/drawable/outline_drawable.xml | 4 +- .../res/drawable/outline_drawable_less.xml | 2 +- app/src/main/res/layout/fragment_result.xml | 2 +- app/src/main/res/layout/result_episode.xml | 4 +- .../main/res/layout/result_episode_both.xml | 9 -- .../res/layout/result_episode_both_old.xml | 14 +++ .../res/layout/result_episode_both_tv.xml | 21 ---- .../res/layout/result_episode_both_tv_old.xml | 21 ++++ .../main/res/layout/result_episode_large.xml | 1 + .../res/layout/result_episode_large_tv.xml | 111 ----------------- .../layout/result_episode_large_tv_old.xml | 112 ++++++++++++++++++ app/src/main/res/layout/result_episode_tv.xml | 60 ---------- .../main/res/layout/result_episode_tv_old.xml | 59 +++++++++ app/src/main/res/values/styles.xml | 9 +- 16 files changed, 246 insertions(+), 242 deletions(-) delete mode 100644 app/src/main/res/layout/result_episode_both.xml create mode 100644 app/src/main/res/layout/result_episode_both_old.xml delete mode 100644 app/src/main/res/layout/result_episode_both_tv.xml create mode 100644 app/src/main/res/layout/result_episode_both_tv_old.xml delete mode 100644 app/src/main/res/layout/result_episode_large_tv.xml create mode 100644 app/src/main/res/layout/result_episode_large_tv_old.xml delete mode 100644 app/src/main/res/layout/result_episode_tv.xml create mode 100644 app/src/main/res/layout/result_episode_tv_old.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 51032a6e..4e0d93c9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -283,6 +283,7 @@ var app = Requests(responseParser = object : ResponseParser { class MainActivity : AppCompatActivity(), ColorPickerDialogListener { companion object { const val TAG = "MAINACT" + const val ANIMATED_OUTLINE : Boolean = false var lastError: String? = null /** @@ -1070,7 +1071,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } - + private fun centerView(view : View?) { + if(view == null) return + try { + Log.v(TAG, "centerView: $view") + val r = Rect(0, 0, 0, 0) + view.getDrawingRect(r) + val x = r.centerX() + val y = r.centerY() + val dx = r.width() / 2 //screenWidth / 2 + val dy = screenHeight / 2 + val r2 = Rect(x - dx, y - dy, x + dx, y + dy) + view.requestRectangleOnScreen(r2, false) + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_: Throwable) { + } + } override fun onCreate(savedInstanceState: Bundle?) { app.initClient(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) @@ -1125,43 +1141,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) setContentView(newLocalBinding.root) - if(isTrueTvSettings()) { + if(isTrueTvSettings() && ANIMATED_OUTLINE) { TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) + newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { + TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) + } newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> - // println("refocus $oldFocus -> $newFocus") - try { - val r = Rect(0, 0, 0, 0) - newFocus.getDrawingRect(r) - val x = r.centerX() - val y = r.centerY() - val dx = 0 //screenWidth / 2 - val dy = screenHeight / 2 - val r2 = Rect(x - dx, y - dy, x + dx, y + dy) - newFocus.requestRectangleOnScreen(r2, false) - // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) - } catch (_: Throwable) { - } TvFocus.updateFocusView(newFocus) - /*var focus = newFocus - - while(focus != null) { - if(focus is ScrollingView && focus.canScrollVertically()) { - focus.scrollBy() - } - when(focus.parent) { - is View -> focus = newFocus - else -> break - } - }*/ } } else { newLocalBinding.focusOutline.isVisible = false } - newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { - TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) + if(isTrueTvSettings()) { + newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> + centerView(newFocus) + } } + + ActivityMainBinding.bind(newLocalBinding.root) // this may crash } else { val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false) diff --git a/app/src/main/res/drawable/outline.xml b/app/src/main/res/drawable/outline.xml index 30077a98..7b436c7d 100644 --- a/app/src/main/res/drawable/outline.xml +++ b/app/src/main/res/drawable/outline.xml @@ -2,11 +2,9 @@ - - \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_drawable.xml b/app/src/main/res/drawable/outline_drawable.xml index 8eec2d0b..16eba83c 100644 --- a/app/src/main/res/drawable/outline_drawable.xml +++ b/app/src/main/res/drawable/outline_drawable.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_drawable_less.xml b/app/src/main/res/drawable/outline_drawable_less.xml index db74a092..aa3a8d0d 100644 --- a/app/src/main/res/drawable/outline_drawable_less.xml +++ b/app/src/main/res/drawable/outline_drawable_less.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 87de7186..9d748c5a 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -875,7 +875,7 @@ android:descendantFocusability="afterDescendants" android:paddingBottom="100dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - tools:listitem="@layout/result_episode_both_tv" /> + tools:listitem="@layout/result_episode" /> diff --git a/app/src/main/res/layout/result_episode.xml b/app/src/main/res/layout/result_episode.xml index 80ff4bec..b56cdb1d 100644 --- a/app/src/main/res/layout/result_episode.xml +++ b/app/src/main/res/layout/result_episode.xml @@ -11,7 +11,9 @@ android:nextFocusRight="@id/download_button" app:cardBackgroundColor="@color/transparent" app:cardCornerRadius="@dimen/rounded_image_radius" - app:cardElevation="0dp"> + app:cardElevation="0dp" + android:foreground="@drawable/outline_drawable" + > diff --git a/app/src/main/res/layout/result_episode_both.xml b/app/src/main/res/layout/result_episode_both.xml deleted file mode 100644 index 61102e84..00000000 --- a/app/src/main/res/layout/result_episode_both.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_both_old.xml b/app/src/main/res/layout/result_episode_both_old.xml new file mode 100644 index 00000000..6472ecc1 --- /dev/null +++ b/app/src/main/res/layout/result_episode_both_old.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_both_tv.xml b/app/src/main/res/layout/result_episode_both_tv.xml deleted file mode 100644 index 13888b7e..00000000 --- a/app/src/main/res/layout/result_episode_both_tv.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_both_tv_old.xml b/app/src/main/res/layout/result_episode_both_tv_old.xml new file mode 100644 index 00000000..f273a118 --- /dev/null +++ b/app/src/main/res/layout/result_episode_both_tv_old.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index 75292965..76e8c434 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -8,6 +8,7 @@ android:layout_height="wrap_content" android:layout_marginBottom="10dp" + android:foreground="@drawable/outline_drawable" android:nextFocusRight="@id/download_button" app:cardBackgroundColor="?attr/boxItemBackground" diff --git a/app/src/main/res/layout/result_episode_large_tv.xml b/app/src/main/res/layout/result_episode_large_tv.xml deleted file mode 100644 index 5a9dee30..00000000 --- a/app/src/main/res/layout/result_episode_large_tv.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_large_tv_old.xml b/app/src/main/res/layout/result_episode_large_tv_old.xml new file mode 100644 index 00000000..3a7cef3c --- /dev/null +++ b/app/src/main/res/layout/result_episode_large_tv_old.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_tv.xml b/app/src/main/res/layout/result_episode_tv.xml deleted file mode 100644 index 53590b6b..00000000 --- a/app/src/main/res/layout/result_episode_tv.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_tv_old.xml b/app/src/main/res/layout/result_episode_tv_old.xml new file mode 100644 index 00000000..62546cf9 --- /dev/null +++ b/app/src/main/res/layout/result_episode_tv_old.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + \ 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 c00970d3..c047c749 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -86,8 +86,8 @@ + + + + + + + + + + + + @@ -506,8 +506,8 @@ ?attr/colorPrimary - @android:dimen/dialog_min_width_major - @android:dimen/dialog_min_width_minor + @dimen/abc_dialog_min_width_major + @dimen/abc_dialog_min_width_minor @drawable/dialog__window_background From 35e38a53ad7638ef9407c8f710966ea09d6dac8b Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 25 Mar 2024 05:29:55 +0530 Subject: [PATCH 442/570] refactor: format build date and time and make it copyable (#1002) --- app/build.gradle.kts | 6 +++--- .../ui/settings/SettingsFragment.kt | 17 ++++++++++++----- app/src/main/res/layout/main_settings.xml | 13 +++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c2ba2907..7ba682be 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -70,9 +70,9 @@ android { val localProperties = gradleLocalProperties(rootDir) buildConfigField( - "String", - "BUILDDATE", - "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));" + "long", + "BUILD_DATE", + "${System.currentTimeMillis()}" ) buildConfigField( "String", diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 72e22269..dfa84998 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -30,6 +30,11 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import java.io.File +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone class SettingsFragment : Fragment() { companion object { @@ -180,12 +185,14 @@ class SettingsFragment : Fragment() { val appVersion = getString(R.string.app_version) val commitInfo = getString(R.string.commit_hash) - val buildDate = BuildConfig.BUILDDATE + val buildTimestamp = SimpleDateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, + Locale.getDefault() + ).apply { timeZone = TimeZone.getTimeZone("UTC") + }.format(Date(BuildConfig.BUILD_DATE)).replace("UTC", "") - binding?.buildDate?.text = buildDate - - binding?.appVersionInfo?.setOnLongClickListener{ - clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo") + binding?.buildDate?.text = buildTimestamp + binding?.appVersionInfo?.setOnLongClickListener { + clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo $buildTimestamp") true } } diff --git a/app/src/main/res/layout/main_settings.xml b/app/src/main/res/layout/main_settings.xml index c3bdc17d..2c90d958 100644 --- a/app/src/main/res/layout/main_settings.xml +++ b/app/src/main/res/layout/main_settings.xml @@ -112,9 +112,9 @@ android:orientation="horizontal"> @@ -123,8 +123,6 @@ android:id="@+id/delimiter0" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center" - android:padding="10dp" android:text="•" android:textColor="?attr/textColor" /> @@ -132,7 +130,6 @@ android:id="@+id/commit_hash" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center" android:padding="10dp" android:text="@string/commit_hash" android:textColor="?attr/textColor" /> @@ -141,9 +138,6 @@ android:id="@+id/delimiter1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_weight="1" - android:gravity="center" - android:padding="10dp" android:text="•" android:textColor="?attr/textColor" /> @@ -151,10 +145,9 @@ android:id="@+id/build_date" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_weight="1" - android:gravity="center" android:padding="10dp" - android:textColor="?attr/textColor" /> + android:textColor="?attr/textColor" + tools:text="21/03/2024 09:02 pm"/> From 22937424fa7e96119a665bb10668df8cb89f7d35 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 25 Mar 2024 05:33:04 +0530 Subject: [PATCH 443/570] feat(ui): authenticate first when enabling security settings (#991) --- app/build.gradle.kts | 2 +- .../lagradost/cloudstream3/MainActivity.kt | 14 ++-- .../ui/account/AccountSelectActivity.kt | 14 ++-- .../ui/settings/SettingsAccount.kt | 67 ++++++++++++++----- .../utils/BiometricAuthenticator.kt | 33 ++++----- app/src/main/res/values/strings.xml | 4 +- build.gradle.kts | 8 +-- 7 files changed, 91 insertions(+), 51 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7ba682be..02946e85 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -154,7 +154,7 @@ repositories { dependencies { // Testing testImplementation("junit:junit:4.13.2") - testImplementation("org.json:json:20231013") + testImplementation("org.json:json:20240303") androidTestImplementation("androidx.test:core") implementation("androidx.test.ext:junit-ktx:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 67bf19fb..7baac71c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -135,7 +135,10 @@ import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -1231,18 +1234,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, changeStatusBarState(isLayout(EMULATOR)) /** Biometric stuff for users without accounts **/ - val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) val noAccounts = settingsManager.getBoolean( getString(R.string.skip_startup_account_select_key), false ) || accounts.count() <= 1 - if (isLayout(PHONE) && authEnabled && noAccounts) { + if (isLayout(PHONE) && isAuthEnabled(this) && noAccounts) { if (deviceHasPasswordPinLock(this)) { startBiometricAuthentication(this, R.string.biometric_authentication_title, false) - BiometricAuthenticator.promptInfo?.let { promt -> - BiometricAuthenticator.biometricPrompt?.authenticate(promt) + promptInfo?.let { prompt -> + biometricPrompt?.authenticate(prompt) } // hide background while authenticating, Sorry moms & dads 🙏 @@ -1825,6 +1827,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, binding?.navHostFragment?.isInvisible = false } + override fun onAuthenticationError() { + finish() + } + private var backPressedCallback: OnBackPressedCallback? = null private fun attachBackPressedCallback() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt index 41aef176..0b0d83db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -23,7 +23,10 @@ import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex @@ -48,7 +51,6 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet ) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) val skipStartup = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false ) || accounts.count() <= 1 @@ -56,7 +58,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet fun askBiometricAuth() { - if (isLayout(PHONE) && authEnabled) { + if (isLayout(PHONE) && isAuthEnabled(this)) { if (deviceHasPasswordPinLock(this)) { startBiometricAuthentication( this, @@ -64,8 +66,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet false ) - BiometricAuthenticator.promptInfo?.let { promt -> - BiometricAuthenticator.biometricPrompt?.authenticate(promt) + promptInfo?.let { prompt -> + biometricPrompt?.authenticate(prompt) } } } @@ -189,4 +191,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet override fun onAuthenticationSuccess() { Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity") } + + override fun onAuthenticationError() { + finish() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 298431ee..f0d402da 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -12,6 +12,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager +import androidx.preference.SwitchPreferenceCompat import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent @@ -30,6 +31,7 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref @@ -38,13 +40,20 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setTool import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.BackupUtils +import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage -class SettingsAccount : PreferenceFragmentCompat() { +class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback { companion object { /** Used by nginx plugin too */ fun showLoginInfo( @@ -252,6 +261,31 @@ class SettingsAccount : PreferenceFragmentCompat() { } } + private fun updateAuthPreference(enabled: Boolean) { + val biometricKey = getString(R.string.biometric_key) + + PreferenceManager.getDefaultSharedPreferences(context ?: return).edit() + .putBoolean(biometricKey, enabled).apply() + findPreference(biometricKey)?.isChecked = enabled + } + + override fun onAuthenticationError() { + updateAuthPreference(!isAuthEnabled(context ?: return)) + } + + override fun onAuthenticationSuccess() { + if (isAuthEnabled(context?: return)) { + updateAuthPreference(true) + BackupUtils.backup(activity) + activity?.showBottomDialogText( + getString(R.string.biometric_setting), + getString(R.string.biometric_warning).html() + ) { onDialogDismissedEvent } + } else { + updateAuthPreference(false) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setUpToolbar(R.string.category_account) @@ -263,22 +297,25 @@ class SettingsAccount : PreferenceFragmentCompat() { hideKeyboard() setPreferencesFromResource(R.xml.settings_account, rootKey) - getPref(R.string.biometric_key)?.setOnPreferenceClickListener { - val authEnabled = PreferenceManager.getDefaultSharedPreferences( - context ?: return@setOnPreferenceClickListener false - ) - .getBoolean(getString(R.string.biometric_key), false) + // hide preference on tvs and emulators + getPref(R.string.biometric_key)?.isEnabled = isLayout(PHONE) - if (authEnabled) { - BackupUtils.backup(activity) - val title = activity?.getString(R.string.biometric_setting) - val warning = activity?.getString(R.string.biometric_warning) - activity?.showBottomDialogText( - title as String, - warning.html() - ) { onDialogDismissedEvent } + getPref(R.string.biometric_key)?.setOnPreferenceClickListener { + val ctx = context ?: return@setOnPreferenceClickListener false + + if (deviceHasPasswordPinLock(ctx)) { + startBiometricAuthentication( + activity?: return@setOnPreferenceClickListener false, + R.string.biometric_authentication_title, + false + ) + promptInfo?.let { + authCallback = this + biometricPrompt?.authenticate(it) + } } - true + + false } val syncApis = diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt index de9b9963..c57600ee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt @@ -12,20 +12,20 @@ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.getString import androidx.fragment.app.FragmentActivity +import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R object BiometricAuthenticator { + const val TAG = "cs3Auth" private const val MAX_FAILED_ATTEMPTS = 3 private var failedAttempts = 0 - const val TAG = "cs3Auth" - private var biometricManager: BiometricManager? = null var biometricPrompt: BiometricPrompt? = null var promptInfo: BiometricPrompt.PromptInfo? = null - var authCallback: BiometricAuthCallback? = null // listen to authentication success private fun initializeBiometrics(activity: Activity) { @@ -37,20 +37,12 @@ object BiometricAuthenticator { activity as FragmentActivity, executor, object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) showToast("$errString") Log.e(TAG, "$errorCode") - failedAttempts++ - - if (failedAttempts >= MAX_FAILED_ATTEMPTS) { - failedAttempts = 0 - activity.finish() - } else { - failedAttempts = 0 - activity.finish() - } + authCallback?.onAuthenticationError() + //activity.finish() } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { @@ -89,7 +81,6 @@ object BiometricAuthenticator { .setDescription(description) .setAllowedAuthenticators(authFlag) .build() - } else { // for apis < 30 promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -98,7 +89,6 @@ object BiometricAuthenticator { .setDeviceCredentialAllowed(true) .build() } - } else { // fallback for A12+ when both fingerprint & Face unlock is absent but PIN is set promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -114,7 +104,6 @@ object BiometricAuthenticator { var result = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - when (biometricManager?.canAuthenticate( DEVICE_CREDENTIAL or BIOMETRIC_STRONG or BIOMETRIC_WEAK )) { @@ -126,7 +115,6 @@ object BiometricAuthenticator { BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> result = true BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> result = false } - } else { @Suppress("DEPRECATION") when (biometricManager?.canAuthenticate()) { @@ -153,12 +141,11 @@ object BiometricAuthenticator { // function to start authentication in any fragment or activity fun startBiometricAuthentication(activity: Activity, title: Int, setDeviceCred: Boolean) { initializeBiometrics(activity) - + authCallback = activity as? BiometricAuthCallback if (isBiometricHardWareAvailable()) { authCallback = activity as? BiometricAuthCallback authenticationDialog(activity, title, setDeviceCred) promptInfo?.let { biometricPrompt?.authenticate(it) } - } else { if (deviceHasPasswordPinLock(activity)) { authCallback = activity as? BiometricAuthCallback @@ -171,7 +158,15 @@ object BiometricAuthenticator { } } + fun isAuthEnabled(ctx: Context):Boolean { + return ctx.let { + PreferenceManager.getDefaultSharedPreferences(ctx) + .getBoolean(getString(ctx, R.string.biometric_key), false) + } + } + interface BiometricAuthCallback { fun onAuthenticationSuccess() + fun onAuthenticationError() } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b5dae57b..ab56a849 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -249,7 +249,7 @@ Search Library Accounts and Security - Updates and backup + Updates and Backup Info Advanced Search Gives you the search results separated by provider @@ -611,7 +611,7 @@ Tracks Audio tracks Video tracks - Apply on Restart + Restart the app to see changes. Restart Stop Safe mode on diff --git a/build.gradle.kts b/build.gradle.kts index 06af44d0..801a3c0f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,3 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() @@ -6,12 +5,9 @@ buildscript { } dependencies { - classpath("com.android.tools.build:gradle:8.2.1") + classpath("com.android.tools.build:gradle:8.2.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle.kts files } } @@ -23,7 +19,7 @@ allprojects { } plugins { - id("com.google.devtools.ksp") version "1.9.22-1.0.16" apply false + id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false } tasks.register("clean") { From 7db7742c734421e2e350448b6e08c0b4e8cfb1d0 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 00:04:49 +0000 Subject: [PATCH 444/570] chore(locales): fix locale issues --- app/src/main/res/values-af/strings.xml | 2 +- app/src/main/res/values-ajp/strings.xml | 2 +- app/src/main/res/values-am/strings.xml | 2 +- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-ars/strings.xml | 2 +- app/src/main/res/values-bg/strings.xml | 2 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-el/strings.xml | 2 +- app/src/main/res/values-eo/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-gl/strings.xml | 2 +- app/src/main/res/values-hi/strings.xml | 2 +- app/src/main/res/values-hr/strings.xml | 2 +- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-iw/strings.xml | 2 +- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-kn/strings.xml | 2 +- app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values-lt/strings.xml | 2 +- app/src/main/res/values-lv/strings.xml | 2 +- app/src/main/res/values-mk/strings.xml | 2 +- app/src/main/res/values-ml/strings.xml | 2 +- app/src/main/res/values-my/strings.xml | 2 +- app/src/main/res/values-ne/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-nn/strings.xml | 2 +- app/src/main/res/values-no/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-qt/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-sk/strings.xml | 2 +- app/src/main/res/values-so/strings.xml | 2 +- app/src/main/res/values-sv/strings.xml | 2 +- app/src/main/res/values-ta/strings.xml | 2 +- app/src/main/res/values-tl/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-ur/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- 50 files changed, 50 insertions(+), 50 deletions(-) diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 5c19185c..45e9a1d4 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -106,4 +106,4 @@ Voer lettertipes in deur dit in %s te plaas Rolverdeling: %s Nuwe episode notifikasie - \ No newline at end of file + diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index eb2bf74a..4d1fc074 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -614,4 +614,4 @@ في ارور بالنسخ. پليز نسوخ الـLogcat 🐈 وبعته ل المسؤولين عن دعم الآپ. هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. \nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج. - \ No newline at end of file + diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml index 63f28ba8..7fd3274b 100644 --- a/app/src/main/res/values-am/strings.xml +++ b/app/src/main/res/values-am/strings.xml @@ -108,4 +108,4 @@ ተጨማሪ መረጃ ዓይነቶችን በመጠቀም ይፈልጉ ቅርጸ-ቁምፊዎችን በ%s ውስጥ በማስቀመጥ ያጫኑ - \ No newline at end of file + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 2ce9fd22..3140afeb 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -641,4 +641,4 @@ خطأ في الوصول الي حافظة النسخ، برجاء المحاولة مرة اخرى. تم النسخ! خطأ في عملية النسخ، برجاء نسخ ال logcat و ارساله الى مسؤولين دعم التطبيق. - \ No newline at end of file + diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index 530b07c9..f3811d3d 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -352,4 +352,4 @@ وثائقي موقع عنوان مشغل الفيديو بحد أقصى لعدد الأحرف - \ 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 4a67f8c5..2be08369 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -601,4 +601,4 @@ Покажи предложения Добавя опция за промяна на скоростта в плеъра Този тест е направен за програмисти и не проверява работата на никакви добавки. - \ No newline at end of file + diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index aa3def8f..867dd4ed 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -229,4 +229,4 @@ আপনার বর্তমান পর্বের অগ্রগতি স্বয়ংক্রিয়ভাবে সিঙ্ক করুন প্লাগইন ডাউনলোড ফিল্টার করতে মোড নির্বাচন করুন লিঙ্ক পুনরায় লোড হয়েছে - \ 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 1a81021a..0cf1bb2c 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -631,4 +631,4 @@ Erro ao acessar a área de transferência. Tente novamente. Nome e URL do repositório Erro ao copiar. Copie o logcat e entre em contato com o suporte do aplicativo. - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8a11823e..519b05b6 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -633,4 +633,4 @@ Chyba při kopírování, zkopírujte prosím protokol a kontaktujte podporu aplikace. Zkopírováno! Chyba při přístupu ke schránce, zkuste to prosím znovu. - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7c56787c..5a871217 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -607,4 +607,4 @@ Beim kopieren ist ein Fehler aufgetreten, bitte kopieren sie logical und wenden sich an den Support. Fehler beim zugriff auf die Zwischenablage, bitte erneut versuchen. Repository Name und URL - \ No newline at end of file + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 69bc390b..a539f374 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -546,4 +546,4 @@ Επιλέξτε κατάσταση για φιλτράρισμα επεκτάσεων για λήψη Απενεργοποιημένο Τέλος - \ No newline at end of file + diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 35b04402..275a4bfb 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -127,4 +127,4 @@ Elŝutite Elŝutante Elŝuto Malsukcesite - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d3e90a41..055fc06b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -609,4 +609,4 @@ ¡Copiado! Error al copiar. Por favor, copie el logcat y comuníquese con el soporte de la aplicación. Error al acceder al portapapeles. Inténtelo de nuevo. - \ No newline at end of file + diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 85b65919..486f7a00 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -128,4 +128,4 @@ به پایان رسیده باز کردن در مرورگر برنامه‌ریزی برای تماشا - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 35b93ac6..17f6a667 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -595,4 +595,4 @@ Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. Copié ! Nom du dépôt et adresse internet - \ No newline at end of file + diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 2efe1991..ae3105cf 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -164,4 +164,4 @@ Selecciona o modo para filtrar a descarga dos complementos Instala automáticamente todos os complementos aínda non instalados dos repositorios engadidos. Mostrar actualizacións da aplicación - \ No newline at end of file + diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 68bd645e..8ce224b3 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -192,4 +192,4 @@ लिंक पुन्ह खुली वर्तमान पिन दर्ज करें नेटवर्क स्ट्रीम - \ No newline at end of file + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 4c31c274..ea6a80eb 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -629,4 +629,4 @@ Lozinka/PIN autentifikacija Ovaj uređaj ne podržava biometrijsku autentifikaciju Ovaj je ekran zatvoren zbog višestrukih neuspjelih pokušaja. Pokrenite aplikaciju ponovo. - \ No newline at end of file + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index b27f9df4..5533cdc0 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -592,4 +592,4 @@ A PIN 4 karakter hosszú kell legyen Auto elforgatás Az automatikus videó orientáció alapján való képernyő elforgatás bekapcsolása - \ 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 6079d47d..d9a10c61 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -630,4 +630,4 @@ Gagal mengakses Papan Klip, mohon coba lagi. disalin! Gagal menyalin, mohon salin logcat dan hubungi pengembang aplikasi. - \ 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 08a05c35..7b958ad3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -629,4 +629,4 @@ copiato! Errore durante l\'accesso agli Appunti. Riprova. Errore durante la copia. Copia logcat e contatta il supporto dell\'app. - \ 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 79c9e276..da2952a0 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -550,4 +550,4 @@ \nיגרמו לעדיפות הסרטון להיות 10. \n \nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! - \ No newline at end of file + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 5c80d77e..acb2cfc3 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -242,4 +242,4 @@ 現在のエピソードが終了したら次のエピソードを開始する 長押しするとデフォルトにリセットされます ダウンロードを再開 - \ 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 1c9d4e4c..f3fb665d 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -130,4 +130,4 @@ Brightness ಅಥವಾ volume ಬದಲಾಯಿಸಲು ಎಡ ಅಥವಾ ಬಲಭಾಗದಲ್ಲಿ ಮೇಲಕ್ಕೆ ಅಥವಾ ಕೆಳಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ ಈಗಿನ ಎಪಿಸೋಡ್ ಮುಗಿದಾಗ ಮುಂದಿನ ಎಪಿಸೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸ್ವೈಪ್ ಮಾಡಿ - \ No newline at end of file + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index cb60b51c..1a63050a 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -527,4 +527,4 @@ 구독중 구독 %s 구독 취소 %s - \ No newline at end of file + diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index cf951ab9..f61bcfc0 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -260,4 +260,4 @@ Ar tikrai norite išeiti\? Pašalinti iš žiūrimų Garso takelis - \ No newline at end of file + diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index deacfdca..49b333e3 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -527,4 +527,4 @@ Abonēto šovu atjaunināšana Abonēts Abonēts %s - \ 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 814a5ed3..fe82a90b 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -591,4 +591,4 @@ Зачестеност на зачувување на бекап Овозможете автоматско префрлување на ориентацијата на екранот врз основа на видео ориентација Автоматска ротација - \ No newline at end of file + diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index b4180f23..0ddd8577 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -245,4 +245,4 @@ ഉറവിട പിശക് നിലവിലെ പിൻ നൽകുക ഓഡിയോ ട്രാക്കുകൾ - \ No newline at end of file + diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index b29ca920..ef796f9f 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -550,4 +550,4 @@ သင်နဂိုတည်းကသတ်မှတ်ပြီး လိုက်ဘရီရွေးချယ်ရန် ဖြင့်ဖွင့်မည် - \ No newline at end of file + diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 97bda0a3..1e23f8af 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -85,4 +85,4 @@ स्रोतहरू स्वचालित बग रिपोर्टिङ असक्षम गर्नुहोस् लागू गर्नुहोस् - \ No newline at end of file + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 0844c7ec..fc537837 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -608,4 +608,4 @@ Link opnieuw geladen Autoroteer Roteer - \ No newline at end of file + diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 4835bcfb..95c527f9 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -195,4 +195,4 @@ Bilde i bilde Fortsett å sjå Prøv tilkopling på nytt… - \ 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 e599c2b0..724f4a63 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -538,4 +538,4 @@ Bruk Hjelp Profilbakgrunn - \ 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 c2e1000a..3e22ba16 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -610,4 +610,4 @@ Błąd dostępu do schowka. Spróbuj ponownie. skopiowano! Błąd podczas kopiowania. Skopiuj logcat i skontaktuj się z pomocą techniczną aplikacji. - \ No newline at end of file + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index ff0f952f..b3180fee 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -607,4 +607,4 @@ Desbloqueie a aplicação com impressão digital, ID facial, PIN, padrão e palavra-passe. Esta janela fechar-se-á após algumas tentativas falhadas. Terá de reiniciar a aplicação. Foi feita uma cópia de segurança dos seus dados CloudStream, embora a probabilidade deste caso raro seja muito baixa, mas todos os dispositivos se comportam de forma diferente. No caso de ficar impedido de aceder à aplicação, na pior das hipóteses, limpe totalmente os dados da aplicação e restaure a cópia de segurança. Lamentamos profundamente qualquer inconveniente. - \ No newline at end of file + diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index 583c6e0e..5de97c7d 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -247,4 +247,4 @@ oooooh uuaagh @string/home_play oouuhhh ahhooo-ahah - \ 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 224ba880..d7da44b4 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -593,4 +593,4 @@ Adaugă o opțiune de viteză la player Favoriți/te Frecvența de backup - \ 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 77defab5..16f4449b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -593,4 +593,4 @@ Этот тест предназначен только для разработчиков и не подтверждает или не опровергает работоспособность провайдеров. Добавление настроек скорости в плеер Протестировать всех провайдеров - \ 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 index 1734b39f..ebaaa2ae 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -355,4 +355,4 @@ Maximálny počet znakov v názve prehrávača Spôsobuje problémy, ak je nastavená príliš vysoko v zariadeniach s malým ukladacím priestorom, ako je napríklad Android TV. Frekvencia zálohovania - \ 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 index a1739399..7b0d2870 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -485,4 +485,4 @@ 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 3c57956e..76508c43 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -604,4 +604,4 @@ Ta bort från favoriter %s \nkvarstår - \ No newline at end of file + diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 788afc34..e981d05a 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -119,4 +119,4 @@ போஸ்டர் பிரதான போஸ்டர் %1$s Ep %2$d - \ No newline at end of file + diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 0bdc57c1..b4308eb7 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -265,4 +265,4 @@ Mga Subtitle ng Chromecast Mga setting ng mga subtitle ng Chromecast Maglaro ng Trailer - \ No newline at end of file + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 06ff7498..7005fd95 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -656,4 +656,4 @@ kopyalandı! Panoya erişimde hata oluştu. Lütfen tekrar deneyin. Kopyalama hatası. Lütfen logcat\'i kopyalayın ve uygulama desteğiyle iletişime geçin. - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 19424fda..130e50af 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -609,4 +609,4 @@ Назва репозиторію та URL Помилка копіювання, будь ласка, скопіюйте logcat й зверніться до служби підтримки застосунку. Помилка доступу до буфера обміну, спробуйте ще раз. - \ No newline at end of file + diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index c462a7d7..0bcad1cf 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -588,4 +588,4 @@ آغاز پر اکاؤنٹ کا انتخاب چھوڑ دیں ویڈیو واقفیت کی بنیاد پر اسکرین کی سمت بندی کی خودکار سوئچنگ کو فعال کریں خود بخود گھومنا - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 3853f1c8..ad60f597 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -606,4 +606,4 @@ Hiển thị nút xoay màn hình Kích hoạt chế độ xoay màn hình tự động Tự động xoay - \ 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 3dc282c0..0e5e34ea 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -637,4 +637,4 @@ 旋轉 根據影片方向自動切換畫面方向 連結已重新載入 - \ No newline at end of file + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index a7c4ebc3..2360a7eb 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -651,4 +651,4 @@ \n剩余 测试所有扩展 已复制! - \ No newline at end of file + From 51d91bf9a79be692dab6964ef84c15fd83497b99 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 25 Mar 2024 05:48:26 +0530 Subject: [PATCH 445/570] feat(ui): add ignore battery optimisation dialog for uniterrupted downloads and notifications (#915) --- .../ui/result/ResultFragmentPhone.kt | 12 ++- .../ui/settings/SettingsGeneral.kt | 19 +++- .../cloudstream3/utils/PowerManagerAPI.kt | 86 +++++++++++++++++++ app/src/main/res/drawable/ic_battery.xml | 12 +++ app/src/main/res/values/strings.xml | 11 +++ app/src/main/res/xml/settings_general.xml | 22 +++-- 6 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt create mode 100644 app/src/main/res/drawable/ic_battery.xml 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 8d0ca37b..fb5160a7 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 @@ -30,7 +30,7 @@ import com.google.android.gms.cast.framework.CastState import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.updateHasTrailers -import com.lagradost.cloudstream3.CommonActivity +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent @@ -61,6 +61,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.openBrowser +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.openBatteryOptimizationSettings import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant @@ -442,8 +443,9 @@ open class ResultFragmentPhone : FullScreenPlayer() { val name = (viewModel.page.value as? Resource.Success)?.value?.title ?: txt(R.string.no_data).asStringNull(context) ?: "" - CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + showToast(txt(message, name), Toast.LENGTH_SHORT) } + context?.let { openBatteryOptimizationSettings(it) } } resultFavorite.setOnClickListener { viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? -> @@ -457,7 +459,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { val name = (viewModel.page.value as? Resource.Success)?.value?.title ?: txt(R.string.no_data).asStringNull(context) ?: "" - CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + showToast(txt(message, name), Toast.LENGTH_SHORT) } } mediaRouteButton.apply { @@ -465,7 +467,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { alpha = if (chromecastSupport) 1f else 0.3f if (!chromecastSupport) { setOnClickListener { - CommonActivity.showToast( + showToast( R.string.no_chromecast_support_toast, Toast.LENGTH_LONG ) @@ -640,6 +642,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { ), null ) { click -> + context?.let { openBatteryOptimizationSettings(it) } + when (click.action) { DOWNLOAD_ACTION_DOWNLOAD -> { viewModel.handleAction( 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 6cf00375..c3d84867 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 @@ -27,11 +27,15 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.ui.EasterEggMonke +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.isAppRestricted +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.showBatteryOptimizationDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog @@ -45,7 +49,6 @@ import com.lagradost.safefile.SafeFile // Change local language settings in the app. fun getCurrentLocale(context: Context): String { - // val dm = res.displayMetrics val res = context.resources val conf = res.configuration @@ -204,6 +207,20 @@ class SettingsGeneral : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + // disable preference on tvs and emulators + getPref(R.string.battery_optimisation_key)?.isEnabled = isLayout(PHONE) + getPref(R.string.battery_optimisation_key)?.setOnPreferenceClickListener { + val ctx = context ?: return@setOnPreferenceClickListener false + + if (isAppRestricted(ctx)) { + showBatteryOptimizationDialog(ctx) + } else { + showToast(R.string.app_unrestricted_toast) + } + + true + } + fun showAdd() { val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } } activity?.showDialog( diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt new file mode 100644 index 00000000..27609730 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt @@ -0,0 +1,86 @@ +package com.lagradost.cloudstream3.utils + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.PowerManager +import android.provider.Settings +import android.util.Log +import androidx.appcompat.app.AlertDialog +import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout + +const val packageName = BuildConfig.APPLICATION_ID +const val TAG = "PowerManagerAPI" + +object BatteryOptimizationChecker { + + fun isAppRestricted(context: Context?): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context != null) { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + return !powerManager.isIgnoringBatteryOptimizations(context.packageName) + } + + return false // below Marshmallow, it's always unrestricted when app is in background + } + + fun openBatteryOptimizationSettings(context: Context) { + if (shouldShowBatteryOptimizationDialog(context)) { + showBatteryOptimizationDialog(context) + } + } + + fun showBatteryOptimizationDialog(context: Context) { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) + + try { + context.let { + AlertDialog.Builder(it) + .setTitle(R.string.battery_dialog_title) + .setIcon(R.drawable.ic_battery) + .setMessage(R.string.battery_dialog_message) + .setPositiveButton(R.string.ok) { _, _ -> + intentOpenAppInfo(it) + } + .setNegativeButton(R.string.cancel) { _, _ -> + settingsManager.edit() + .putBoolean(context.getString(R.string.battery_optimisation_key), false) + .apply() + } + .show() + } + } catch (t: Throwable) { + Log.e(TAG, "Error showing battery optimization dialog", t) + } + } + + private fun shouldShowBatteryOptimizationDialog(context: Context): Boolean { + val isRestricted = isAppRestricted(context) + val isOptimizedNotShown = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.battery_optimisation_key), true) + return isRestricted && isOptimizedNotShown && isLayout(PHONE) + } + + private fun intentOpenAppInfo(context: Context) { + val intent = Intent() + try { + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", packageName, null)) + context.startActivity(intent, Bundle()) + } catch (t: Throwable) { + Log.e(TAG, "Unable to invoke any intent", t) + if (t is ActivityNotFoundException) { + showToast("Exception: Activity Not Found") + } else { + showToast(R.string.app_info_intent_error) + } + } + } +} diff --git a/app/src/main/res/drawable/ic_battery.xml b/app/src/main/res/drawable/ic_battery.xml new file mode 100644 index 00000000..24d0a77f --- /dev/null +++ b/app/src/main/res/drawable/ic_battery.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d88489a4..378fa16a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -654,6 +654,17 @@ Are you sure you want to exit\? Yes No + OK + Disable Battery optimization + To ensure uninterrupted downloads and notifications for subscribed + TV shows, CloudStream needs permission to run in background. By pressing "OK", you\'ll be directed to App info. + There, scroll to 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 and set battery usage to 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Please note, this permission + does not mean CS3 will drain your battery. It will only operate in the background when necessary, such as + when receiving notifications or downloading videos from official extensions. If you choose to cancel, + you can adjust this setting later in 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. + battery_optimisation + App battery usage is already set to unrestricted + Unable to open CloudStream\'s App info. Downloading app update… Installing app update… Could not install the new version of the app diff --git a/app/src/main/res/xml/settings_general.xml b/app/src/main/res/xml/settings_general.xml index c4900bca..cdda6d85 100644 --- a/app/src/main/res/xml/settings_general.xml +++ b/app/src/main/res/xml/settings_general.xml @@ -6,10 +6,7 @@ android:title="@string/app_language" android:icon="@drawable/ic_baseline_language_24" /> - + + android:title="@string/title_downloads"> + + + + + + + + Date: Mon, 25 Mar 2024 01:38:39 +0100 Subject: [PATCH 446/570] New TvTypes + General fixes --- .../com/lagradost/cloudstream3/MainAPI.kt | 6 ++- .../lagradost/cloudstream3/ui/BaseAdapter.kt | 2 +- .../ui/home/HomeParentItemAdapter.kt | 4 ++ .../ui/result/ResultViewModel2.kt | 37 ++++++++++++------- app/src/main/res/values/strings.xml | 3 ++ 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 273e267b..ecbdcbbc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -865,7 +865,11 @@ enum class TvType(value: Int?) { AsianDrama(9), Live(10), NSFW(11), - Others(12) + Others(12), + Music(13), + AudioBook(14), + /** Wont load the built in player, make your own interaction */ + CustomMedia(15), } public enum class AutoDownloadMode(val value: Int) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt index 7439bfdf..d90177f5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt @@ -85,7 +85,7 @@ abstract class BaseAdapter< AsyncDifferConfig.Builder(diffCallback).build() ) - fun submitList(list: List?) { + open fun submitList(list: List?) { // deep copy at least the top list, because otherwise adapter can go crazy mDiffer.submitList(list?.let { CopyOnWriteArrayList(it) }) } 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 fb75e772..4b0360d7 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 @@ -63,6 +63,10 @@ open class ParentItemAdapter( } } + override fun submitList(list: List?) { + super.submitList(list?.sortedBy { it.list.list.isEmpty() }) + } + override fun onUpdateContent( holder: ViewHolderState, item: HomeViewModel.ExpandableHomepageList, 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 c90e01d0..8bf743be 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 @@ -246,6 +246,9 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { TvType.Live -> R.string.live_singular TvType.Others -> R.string.other_singular TvType.NSFW -> R.string.nsfw_singular + TvType.Music -> R.string.music_singlar + TvType.AudioBook -> R.string.audio_book_singular + TvType.CustomMedia -> R.string.custom_media_singluar } ), yearText = txt(year?.toString()), @@ -1759,20 +1762,28 @@ class ResultViewModel2 : ViewModel() { val data = currentResponse?.syncData?.toList() ?: emptyList() val list = HashMap().apply { putAll(data) } - - activity?.navigate( - R.id.global_to_navigation_player, - GeneratorPlayer.newInstance( - generator?.also { - it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work - ?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id } - ?.let { index -> - if (index >= 0) - it.goto(index) - } - } ?: return, list + generator?.also { + it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work + ?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id } + ?.let { index -> + if (index >= 0) + it.goto(index) + } + } + if (currentResponse?.type == TvType.CustomMedia) { + generator?.generateLinks( + clearCache = true, + LoadType.Unknown, + callback = {}, + subtitleCallback = {}) + } else { + activity?.navigate( + R.id.global_to_navigation_player, + GeneratorPlayer.newInstance( + generator ?: return, list + ) ) - ) + } } ACTION_MARK_AS_WATCHED -> { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 378fa16a..c2136b4d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -764,4 +764,7 @@ Unlock the app with Fingerprint, Face ID, PIN, Pattern and Password. This screen was closed due to multiple failed attempts. Please restart the application. Your CloudStream data has been backed up now. Although the possibility of this is very low, all devices can behave differently. In the rare case, that you get locked out from accessing the app, clear the app data completely and restore from a backup. We are very sorry for any inconvenience arising from this. + Music + Audio Book + Media \ No newline at end of file From 0a24661e4cf301fac304ce6f28f32b718fdc81b1 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 25 Mar 2024 01:48:23 +0100 Subject: [PATCH 447/570] fix latest commit --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 3 +++ 1 file changed, 3 insertions(+) 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 8bf743be..84b8cf48 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 @@ -630,6 +630,9 @@ class ResultViewModel2 : ViewModel() { TvType.Live -> "LiveStreams" TvType.NSFW -> "NSFW" TvType.Others -> "Others" + TvType.Music -> "Music" + TvType.AudioBook -> "AudioBooks" + TvType.CustomMedia -> "Media" } } From a74563d003ba0d4e5f2cc48b527b53c801b90ee4 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 8 Apr 2024 04:02:02 +0200 Subject: [PATCH 448/570] Translated using Weblate (Russian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Russian) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Russian) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Vietnamese) Currently translated at 75.0% (3 of 4 strings) Translated using Weblate (Vietnamese) Currently translated at 98.7% (688 of 697 strings) Translated using Weblate (French) Currently translated at 96.8% (675 of 697 strings) Translated using Weblate (Arabic (Levantine)) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Persian) Currently translated at 34.7% (242 of 697 strings) Translated using Weblate (Vietnamese) Currently translated at 98.8% (689 of 697 strings) Translated using Weblate (Russian) Currently translated at 97.1% (677 of 697 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Malayalam) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Malayalam) Currently translated at 48.4% (338 of 697 strings) Translated using Weblate (Indonesian) Currently translated at 99.8% (696 of 697 strings) Translated using Weblate (Maltese) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Maltese) Currently translated at 32.1% (224 of 697 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (697 of 697 strings) Added translation using Weblate (Maltese) Translated using Weblate (Spanish) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 98.1% (684 of 697 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Polish) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Italian) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Czech) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (697 of 697 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Spanish) Currently translated at 99.4% (690 of 694 strings) Translated using Weblate (Odia) Currently translated at 37.5% (258 of 688 strings) Co-authored-by: Andre Costa Co-authored-by: Anonymous Co-authored-by: Argo Carpathians Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com> Co-authored-by: Colgrave Co-authored-by: Fjuro Co-authored-by: Fqwe1 Co-authored-by: Gnkalk Co-authored-by: Herderson Riker Co-authored-by: Hosted Weblate Co-authored-by: Joshua Joseph Co-authored-by: Long Kim Co-authored-by: Massimo Pissarello Co-authored-by: Matthaiks Co-authored-by: Michael John Scerri Co-authored-by: Mika Co-authored-by: Pizza Party Co-authored-by: Rex_sa Co-authored-by: Thanh Co-authored-by: aleksej0R Co-authored-by: gallegonovato Co-authored-by: kaajjo Co-authored-by: maxim Co-authored-by: samwiaba Co-authored-by: Ömer Faruk Sancak Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/apc/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fa/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ml/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/or/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ml/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/mt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ru/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/vi/ Translation: Cloudstream/App Translation: Cloudstream/Fastlane --- app/src/main/res/values-ajp/strings.xml | 14 +- app/src/main/res/values-ar/strings.xml | 13 +- app/src/main/res/values-bp/strings.xml | 18 ++- app/src/main/res/values-cs/strings.xml | 12 +- app/src/main/res/values-es/strings.xml | 12 +- app/src/main/res/values-fa/strings.xml | 75 ++++++++++- app/src/main/res/values-fr/strings.xml | 4 +- app/src/main/res/values-in/strings.xml | 14 +- app/src/main/res/values-it/strings.xml | 12 +- app/src/main/res/values-ml/strings.xml | 63 +++++++-- app/src/main/res/values-mt/strings.xml | 126 ++++++++++++++++++ app/src/main/res/values-or/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 16 ++- app/src/main/res/values-pt/strings.xml | 18 ++- app/src/main/res/values-ru/strings.xml | 33 ++++- app/src/main/res/values-tr/strings.xml | 14 +- app/src/main/res/values-uk/strings.xml | 12 +- app/src/main/res/values-vi/strings.xml | 39 ++++-- app/src/main/res/values-zh/strings.xml | 10 +- .../metadata/android/ml-IN/changelogs/2.txt | 1 + .../android/ml-IN/full_description.txt | 10 ++ .../android/ml-IN/short_description.txt | 1 + fastlane/metadata/android/ml-IN/title.txt | 1 + fastlane/metadata/android/mt/changelogs/2.txt | 1 + .../metadata/android/mt/full_description.txt | 10 ++ .../metadata/android/mt/short_description.txt | 1 + fastlane/metadata/android/mt/title.txt | 1 + .../metadata/android/ru-RU/changelogs/2.txt | 1 + .../android/ru-RU/full_description.txt | 10 ++ .../android/ru-RU/short_description.txt | 1 + fastlane/metadata/android/ru-RU/title.txt | 1 + fastlane/metadata/android/vi/title.txt | 2 +- 32 files changed, 477 insertions(+), 71 deletions(-) create mode 100644 app/src/main/res/values-mt/strings.xml create mode 100644 fastlane/metadata/android/ml-IN/changelogs/2.txt create mode 100644 fastlane/metadata/android/ml-IN/full_description.txt create mode 100644 fastlane/metadata/android/ml-IN/short_description.txt create mode 100644 fastlane/metadata/android/ml-IN/title.txt create mode 100644 fastlane/metadata/android/mt/changelogs/2.txt create mode 100644 fastlane/metadata/android/mt/full_description.txt create mode 100644 fastlane/metadata/android/mt/short_description.txt create mode 100644 fastlane/metadata/android/mt/title.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/2.txt create mode 100644 fastlane/metadata/android/ru-RU/full_description.txt create mode 100644 fastlane/metadata/android/ru-RU/short_description.txt create mode 100644 fastlane/metadata/android/ru-RU/title.txt diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index 4d1fc074..e4e17942 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -143,7 +143,7 @@ لودينگ… شيل بَعِد مَعلومات - التجديد والنسخات الاحتياطية + التجديدات والنسخات الاحتياطية خبي هيدي الجودات من نتائج التنبيش موقف موقتًا كلود ستريم @@ -448,7 +448,7 @@ التلخيص إِيه الرئيسي - طبّق وقتما سكّر الآپ + سكر الآپ حتى تطبق التغيرات ساعدوني عوز هيدا إذا عم بتبين الترجمة %d ميلي ثانية بعدما لازم ما نلاقا الآپ @@ -614,4 +614,12 @@ في ارور بالنسخ. پليز نسوخ الـLogcat 🐈 وبعته ل المسؤولين عن دعم الآپ. هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. \nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج. - + أوكي + وقف اپتميزايشن بطارية جهازك + بطارية الآپ اصلًا محطوطة ع «غير مقيد» \"Unrestricted\" + ما قدرنا نفتح معلومات الآپ تبع \"كلود ستريم\". + موسيقى + أوديو بوك + الميديا + لتضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يلي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي بـ الباكگروند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". ملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآن بـ الباكگروند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ بـ«الإعدادات العامة» \"General settings\"، إزا غيرت رأيك. + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 3140afeb..2ac4d973 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -440,7 +440,7 @@ المسارات مسار الصوت مسار الفيديو - تطبيق بعد إعادة التشغيل + أعد تشغيل التطبيق لرؤية التغييرات. الوضع الآمن قيد التشغيل تم إيقاف تشغيل جميع الملحقات بسبب عطل لمساعدتك في العثور على الإضافة التي تسبب مشكلة. عرض بيانات الاعطال @@ -641,4 +641,13 @@ خطأ في الوصول الي حافظة النسخ، برجاء المحاولة مرة اخرى. تم النسخ! خطأ في عملية النسخ، برجاء نسخ ال logcat و ارساله الى مسؤولين دعم التطبيق. - + تعطيل تحسين البطارية + تم ضبط استخدام بطارية التطبيق بالفعل على غير مقيد + غير قادر على فتح معلومات تطبيق CloudStream. + كتاب صوتي + حسناً + لضمان عدم انقطاع التنزيلات والإشعارات للبرامج التلفزيونية المشتركة، يحتاج CloudStream إلى إذن للتشغيل في الخلفية. بالضغط على موافق، سيتم توجيهك إلى معلومات التطبيق. هناك، انتقل إلى استخدام بطارية التطبيق +\nواضبط استخدام البطارية على غير مقيد. يرجى ملاحظة أن هذا الإذن لا يعني أن CS3 سوف يستنزف البطارية. ولن يعمل إلا في الخلفية عند الضرورة، كما هو الحال عند تلقي الإشعارات أو تنزيل مقاطع الفيديو من الملحقات الرسمية. إذا اخترت الإلغاء، فيمكنك ضبط هذا الإعداد لاحقًا في الإعدادات العامة. + موسيقى + الوسائط + \ 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 0cf1bb2c..e4f47749 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -50,7 +50,7 @@ Fontes Legendas Tentando conectar novamente… - Voltar + Volte Reproduzir episódio Download @@ -145,7 +145,7 @@ Erro no backup de %s Procurar Contas e Segurança - Atualizações e backup + Atualizações e Backup Info Procura Avançada Mostrar resultados separados por fornecedor @@ -473,7 +473,7 @@ Conteúdo +18 Ajuda Processo de configuração de Redo - Não pudemos instalar a nova versão do App + Não foi possível instalar a nova versão do aplicativo instalador de pacotes Organizar por Votação (Alta para Baixa) @@ -541,7 +541,7 @@ MPV Abrindo mistura VLC - Aplicar quando reiniciar + Reinicie o aplicativo para ver as alterações. Visualização info de crash Faixas de áudio Adicionado em (novo para antigo) @@ -631,4 +631,12 @@ Erro ao acessar a área de transferência. Tente novamente. Nome e URL do repositório Erro ao copiar. Copie o logcat e entre em contato com o suporte do aplicativo. - + Para garantir downloads e notificações ininterruptos para programas de TV assinados, o CloudStream precisa de permissão para ser executado em segundo plano. Ao pressionar OK, você será direcionado para as informações do aplicativo. Lá, vá até Uso da bateria do aplicativo e defina o uso da bateria como Irrestrito. Observe que esta permissão não significa que o CS3 irá descarregar sua bateria. Ele só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se você optar por cancelar, poderá ajustar essa configuração posteriormente nas Configurações Gerais. + Ok + Desativar otimização de bateria + O uso da bateria do app já está definido como irrestrito + Não foi possível abrir as informações do aplicativo CloudStream. + Música + Áudio-livro + Mídia + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 519b05b6..cc357373 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -383,7 +383,7 @@ Staženo: %d Zvukové stopy Videostopy - Použít při restartu + Restartujte aplikaci pro použití změn. Bezpečný režim povolen Velikost Autoři @@ -633,4 +633,12 @@ Chyba při kopírování, zkopírujte prosím protokol a kontaktujte podporu aplikace. Zkopírováno! Chyba při přístupu ke schránce, zkuste to prosím znovu. - + OK + Využití baterie aplikací je již nastaveno na neomezené + Nepodařilo se otevřít informace o aplikaci CloudStream. + Hudba + Média + Zakažte optimalizace baterie + Aby bylo zajištěno nepřetržité stahování a upozornění na odebírané seriály, potřebuje aplikace CloudStream povolení ke spuštění na pozadí. Stisknutím tlačítka OK budete přesměrováni na informace o aplikaci. Tam přejděte na položku Využití baterie aplikací a nastavte možnost Využití baterie na hodnotu Neomezené. Upozorňujeme, že toto povolení neznamená, že CS3 bude vybíjet baterii. Na pozadí bude pracovat pouze v případě potřeby, například při přijímání oznámení nebo stahování videí z oficiálních rozšíření. Pokud se rozhodnete toto nastavení zrušit, můžete jej později upravit v Obecných nastaveních. + Audiokniha + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 055fc06b..bcff5139 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -281,7 +281,7 @@ Omitir configuración Qué quieres ver Poner en MAYÚSCULAS todos los subtítulos - Se aplicarán los cambios al reiniciar la App + Se aplicarán los cambios al reiniciar la App. Reproductor interno Idioma Legacy (método antiguo) @@ -609,4 +609,12 @@ ¡Copiado! Error al copiar. Por favor, copie el logcat y comuníquese con el soporte de la aplicación. Error al acceder al portapapeles. Inténtelo de nuevo. - + De acuerdo + Desactivar optimización de batería + Música + El uso de la batería de la aplicación está configurado sin restricciones + No se puede abrir la información de la aplicación CloudStream. + Media + Audiolibro + Para garantizar descargas y notificaciones ininterrumpidas para programas de televisión suscritos, CloudStream necesita permiso para ejecutarse en segundo plano. Al presionar OK, se le dirigirá a información de la aplicación. Allí, desplácese hasta Uso de la batería de la aplicación y establezca el uso de la batería en Sin restricciones. Tenga en cuenta que este permiso no significa que CS3 agotará su batería. Solo funcionará en segundo plano cuando sea necesario, como cuando reciba notificaciones o descargue videos de extensiones oficiales. Si decide cancelar, puede ajustar esta configuración más adelante en los ajustes generales. + \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 486f7a00..e9847af6 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -33,16 +33,16 @@ %1$dساعت %2$dدقیقه %dدقیقه پوستر اصلی - تورنت + تورنت‌ها آزاد - مستند ها + مستند‌ها انیمیشن ویدیویی اصلی حداکثر فیلم‌ها سریال های تلویزیونی - درام های آسیایی + درام‌های آسیایی انیمه - کارتونها + کارتون‌ها استفاده شده برنامه بازگشت @@ -52,7 +52,7 @@ اطلاعات بیشتر شرح زبان زیرنویس - زیرنویس + زیرنویس‌ها حذف بارگیری آغاز شد غیرفعال کردن گذارش باگ خودکار @@ -128,4 +128,67 @@ به پایان رسیده باز کردن در مرورگر برنامه‌ریزی برای تماشا - + برای بازنشانی به پیشفرض نگه‌دارید + کتابخانه + درادامه + این فرآیند بطور کامل %s را حذف می‌کند +\nآیا از این کار اطمینان دارید؟ + نام مخزن و نشانی + رونویسی شد! + درباره + قلم + اندازه قلم + برای تغییر تنظیمات بکشید + برای تغییر میزان روشنایی یا صدا در سمت چپ و راست به بالا یا پایین بکشید + بروزرسانی خودکار افزونه + آغاز + زبان برنامه + پخش قسمت + سال + فیلم + سریال + انیمه + رنگ حاشیه متن + دکمه تغییر‌اندازه پخش‌کننده + افزودن گزینه سرعت در پخش‌کننده + بروزرسانی‌ و پشتیبانی + نمایش پوستر از طریق Kitsu + جستجوی پیشرفته + فصل + قسمت + ف + ق + ویدئو + خطای منبع + گزارش + حذف حاشیه سیاه + تنظیمات زیرنویس پخش‌کننده + حساب‌ها و امنیت + نمایش گزارش‌پیوسته 🐈 + پیوند در بریده‌دان رونویسی شد + هیچ پیوندی یافت‌نشد + درام آسیایی + بارگیری خودکار افزونه‌ها + مستند + سرعت پخش + هیچ قسمتی یافت‌نشد + امتیاز: %.1f + قلم‌ها را با گذاشتن در %s وارد کنید + ادامه + بازگردانی به مقدار پیشفرض + −۳۰ + +۳۰ + حذف پرونده + نمایش پیش‌پرده‌ها + قسمت‌ها + %dد +\nباقی‌مانده + گیتهاب + نهان کردن کیفیت ویدئو انتخابی در نتایج جستجو + لغو + %s +\nباقی‌مانده + پیش‌فرض + کارتون + تورنت + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 17f6a667..1370ff2b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -279,7 +279,7 @@ Permissions de stockage manquantes. Veuillez réessayer. Erreur de sauvegarde %s Recherche - Comptes + Comptes et Sécurité Mises à jour et sauvegarde Info Recherche avancée @@ -595,4 +595,4 @@ Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. Copié ! Nom du dépôt et adresse internet - + \ 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 d9a10c61..c3b55ba2 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -138,7 +138,7 @@ Error saat mencadang %s Cari Akun dan Keamanan - Update dan cadangan + Update dan Cadangan Info Pencarian Lanjutan Memberikan hasil pencarian yang dipisahkan berdasarkan provider @@ -432,7 +432,7 @@ Semua Umur %s (Tidak aktif) Trek - Terapkan saat dimuat ulang + Terapkan saat dimuat ulang untuk melihat perubahan. Keterangan Versi Status @@ -630,4 +630,12 @@ Gagal mengakses Papan Klip, mohon coba lagi. disalin! Gagal menyalin, mohon salin logcat dan hubungi pengembang aplikasi. - + Oke + Matikan pengoptimalan Baterai + Pemakaian baterai untuk aplikasi ini sudah diatur menjadi tidak dibatasi + Gagal membuka info aplikasi CloudStream. + Musik + Buku Audio + Media + Untuk memastikan unduhan dan pemberitahuan tanpa gangguan untuk acara TV berlangganan, CloudStream memerlukan izin untuk berjalan di latar belakang. Dengan menekan OK, Anda akan diarahkan ke Info aplikasi. Di sana, gulir ke Penggunaan baterai aplikasi dan atur penggunaan baterai ke Tidak Terbatas. Harap dicatat, izin ini tidak berarti CS3 akan menguras baterai Anda. Ini hanya akan beroperasi di latar belakang ketika diperlukan, seperti ketika menerima pemberitahuan atau mengunduh video dari ekstensi resmi. Jika Anda memilih untuk membatalkannya, Anda dapat menyesuaikan pengaturan ini nanti di Pengaturan Umum. + \ 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 7b958ad3..01031297 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -432,7 +432,7 @@ Tracce Traccia audio Traccia video - Applica al riavvio + Riavvia app per visualizzare le modifiche. Safe mode attiva Tutte le estensioni sono state disabilitate a causa di un arresto anomalo per aiutarti a trovare l\'estensione che causa il problema. Vedi informazioni del crash @@ -629,4 +629,12 @@ copiato! Errore durante l\'accesso agli Appunti. Riprova. Errore durante la copia. Copia logcat e contatta il supporto dell\'app. - + OK + Disabilita ottimizzazione della batteria + Impossibile aprire le informazioni sull\'app CloudStream. + Media + Per garantire download e notifiche ininterrotti per i programmi TV sottoscritti, CloudStream necessita dell\'autorizzazione per l\'esecuzione in background. Premendo OK, verrai indirizzato alle informazioni sull\'app. Successivamente, scorri fino a \"Utilizzo della batteria\" e imposta l\'utilizzo della batteria su \"Senza restrizioni\". Tieni presente che questa autorizzazione non significa che CS3 scaricherà la batteria. Funzionerà in background solo quando necessario, ad esempio quando si ricevono notifiche o si scaricano video da estensioni ufficiali. Se scegli di annullare, puoi modificare questa impostazione più tardi in \"Impostazioni generali\". + L\'utilizzo della batteria dell\'app è già impostato su \"Senza restrizioni\" + Musica + Audiolibro + \ No newline at end of file diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 0ddd8577..a26f902b 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -3,14 +3,14 @@ വേഗം (%.2fx) റേറ്റിംഗ്: %.1f - പുതിയ അപ്ഡേറ്റ് + പുതിയ അപ്ഡേറ്റ്! \n%1$s -> %2$s - CloudStream + ക്ലൗഡ് സ്ട്രീം ഹോം തിരയുക ഡൗൺലോഡ്സ് സെറ്റിങ്‌സ് - തിരയുക + തിരയുക… ടാറ്റ ലഭ്യമല്ല കൂടുതൽ ഓപ്ഷൻസ് അടുത്ത എപ്പിസോഡ് @@ -167,11 +167,11 @@ ഔചിത്യ വീഡിയോ ക്വാളിറ്റി ചരിത്രം കണ്ടതാണെന്ന് അടയാളപ്പെടുത്തുക - %1$d%2$d - yg5t4r%dujyhtg + %d ദിവസങ്ങൾ %d മണിക്കൂർ %d മിനിറ്റ് + അധ്യായം%dൽ റിലീസ് ചെയ്യും %1$d മണിക്കൂർ %2$d മിനിറ്റ് - %1$sghj%2$d - rtf:% + %1$sഅധ്യാ%2$d + കാസ്റ്റ്:%s അക്കൗണ്ട് ഉണ്ടാക്കുക പുറത്ത്പോകുന്നതോടുകൂടി ആപ് അപ്ഡേറ്റ് ആവുന്നതാണ് ലൈബ്രറി തിരഞ്ഞെടുക്കുക @@ -179,8 +179,8 @@ ട്രെയിലർ പ്ലേ ചെയ്യുക ലൈവ് സ്ട്രീം പ്ലേ ചെയ്യുക ഫില്ലർ - %d min - ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് കളിക്കുക + %d മിനിറ്റ് + ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് പ്രവർത്തിപ്പിക്കുക അടുത്ത ക്രമരഹിതമായ എപ്പിസോഡ് പോസ്റ്റർ അപ്ഡേറ്റ് ആരംഭിച്ചു @@ -188,7 +188,7 @@ പോസ്റ്റർ ലോഡിംഗ് ഒഴിവാക്കുക തിരയുക %s… - %dm + %dമിനിറ്റ് മടങ്ങിപ്പോവുക പശ്ചാത്തല പ്രിവ്യൂ പോസ്റ്റർ @@ -202,8 +202,6 @@ പൊതു പട്ടിക CloudStream-ന് സ്ഥിരസ്ഥിതിയായി സൈറ്റുകളൊന്നും ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. നിങ്ങൾ റിപ്പോസിറ്ററികളിൽ നിന്ന് സൈറ്റുകൾ ഇൻസ്റ്റാൾ ചെയ്യേണ്ടതുണ്ട്. \n -\nസ്‌കൈ യുകെ ലിമിറ്റഡിലെ ഡോഗ്‌ഷിറ്റ് ആളുകളിൽ നിന്ന് DMCA നീക്കം ചെയ്‌തതിനാൽ 🤮 ഞങ്ങൾക്ക് ആപ്പിൽ റിപ്പോസിറ്ററി സൈറ്റ് ലിങ്ക് ചെയ്യാൻ കഴിയില്ല. -\n \nഞങ്ങളുടെ ഡിസ്കോർഡിൽ ചേരുക അല്ലെങ്കിൽ ഓൺലൈനിൽ തിരയുക. പകർത്തുക എല്ലാ സബ്‌ടൈറ്റിലുകളും വലിയക്ഷരമാക്കുക @@ -214,7 +212,7 @@ അക്കൗണ്ടുകൾ കൈകാര്യം ചെയ്യുക ഉടൻ വരുന്നു… പുനരാരംഭിക്കുമ്പോൾ പ്രയോഗിക്കുക - അക്കൗണ്ട് എഡിറ്റ് ചെയ്യുക + അക്കൗണ്ട് തിരുത്തുക തെറ്റായ പിൻ. ദയവായി വീണ്ടും ശ്രമിക്കുക. നിർത്തുക ട്രാക്കുകൾ @@ -245,4 +243,41 @@ ഉറവിട പിശക് നിലവിലെ പിൻ നൽകുക ഓഡിയോ ട്രാക്കുകൾ - + ചിത്രം-ഇൻ-ചിത്രം + പുതുക്കിയത് (പഴയത് മുതൽ പുതിയത് വരെ) + റേറ്റിംഗ് (ഉയർന്നത് മുതൽ താഴ്ന്നത്) + പാരമ്പര്യം + വിൻഡോ നിറം + ക്ലിയർ + ലോഗ് + ശുപാർശകൾ കാണിക്കുക + %s ആയി ലോഗിൻ ചെയ്തു + ഇങ്ങനെ അടുക്കുക + അടുക്കുക + തിരുത്തുക + പുതുക്കിയത് (പുതിയത് മുതൽ പഴയത് വരെ) + NSFW + ആപ്പ് അപ്ഡേറ്റ് ഇൻസ്റ്റാൾ ചെയ്യുന്നു… + അപ്ഡേറ്റുകളും ഒപ്പം ബാക്കപ്പും + %s(അപ്രാപ്തമാക്കി) + റേറ്റിംഗ് (താഴ്ന്നത് മുതൽ ഉയർന്നത് വരെ) + വാചക നിറം + ആപ്പിൻ്റെ പുതിയ പതിപ്പ് ഇൻസ്റ്റാൾ ചെയ്യാനായില്ല + പാക്കേജ് ഇൻസ്റ്റാളർ + അക്ഷരമാലാക്രമം (A മുതൽ Z വരെ) + അക്ഷരമാലാക്രമം (Z മുതൽ A വരെ) + ഈ ലിസ്റ്റ് ശൂന്യമാണ്. മറ്റൊന്നിലേക്ക് മാറാൻ ശ്രമിക്കുക. + ചരിത്രം മായ്ക്കുക + ലോഗ്കാറ്റ് കാണിക്കുക 🐈 + നിങ്ങളുടെ ലൈബ്രറി ശൂന്യമാണ് :( +\nഒരു ലൈബ്രറി അക്കൗണ്ടിൽ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ നിങ്ങളുടെ പ്രാദേശിക ലൈബ്രറിയിലേക്ക് ഷോകൾ ചേർക്കുക. + വീഡിയോ + റിപ്പോസിറ്ററി നാമവും URL ഉം + പകർത്തി! + പുതിയ എപ്പിസോഡ് അറിയിപ്പ് + മറ്റ് വിപുലീകരണങ്ങളിൽ തിരയുക + ഉപശീർഷകം ക്രമീകരണങ്ങൾ + എഡ്ജ് തരം + ഔട്ട്ലൈൻ നിറം + പശ്ചാത്തല നിറം + \ No newline at end of file diff --git a/app/src/main/res/values-mt/strings.xml b/app/src/main/res/values-mt/strings.xml new file mode 100644 index 00000000..356a2caa --- /dev/null +++ b/app/src/main/res/values-mt/strings.xml @@ -0,0 +1,126 @@ + + + Preferenzi tas-sottotitli + Kulur tal-kitba + Kulur tat-Tieqa + Fittex bl-użu ta \'tipi + Importa fonts billi tpoġġihom ġo %s + Dan il-fornitur huwa torrent, VPN huwa rakkomandat + Atturi: %s + L-episodju %d ha johrog fil + %1$dh %2$dm + %dm + Kartellun + Kartellun + Kartellun tal-episodju + Kartellun Principali + Li jmiss bl\'addoċċ + Ibdel Il-fornitur + veloċità (%.2fx) + Klassifikazzjoni: %.1f + Aġġornament ġdid misjub! +\n%1$s -> %2$s + %d min + CloudStream + Ara bil-CloudStream + Dar + Fittex + Imnizzel + Preferenzi + Fittex… + Fittex%s… + Bla dejta + Iktar Preferenzi + L-episodju li\'jmiss + Ġeneri + Aqsam + Iftah fil-brawser + Brawser + Aqbez it-tagħbija + Tagħbija… + Jaraw + Stenna ftit + Lest + Imwaqqa + Pjana biex tara + Terġa\' tara + Ibda t-trejler + Ibda l-livestream + Stream Torrent + Sorsi + Erġa\' pprova l-konnessjoni… + Mur lura + Ibda l-episodju + Tniżżila ppawzata + Qed jinżlu + Imniżżel + Tniżżil ikkanċellat + Lest it-tniżżil + Beda l-aġġornament + Network stream + Tagħbija tal-Links falliet + Links regaw gew mogħbija + Ħażna Interna + Dub + Ibda + Info + Issettja l-istatus ta-rajtux + Applika + Ikkopja + Għalaq + Neħħi + Issevja + Isem tar-repożitorju u URL + Ikkupjat! + Notifika ta\' episodju ġdid + Fittex f\'estensjonijiet oħra + Uri r-rakkomandazzjonijiet + Veloċità tal-Plejer + Kulur tal-Kontorn + Kulur tal-Isfond + Tip tat-tarf + Elevazzjoni tas-Sottotitolu + Font + Daqs tal-font + Fittex bl-użu ta\' fornituri + %d Benenes mogħtija lil devs + Ebda Benenes mogħtija + Agħżel il-Lingwa Awtomatikament + Niżżel Lingwi + Lingwa tas-sottotitolu + Żomm biex tirrisettja għal default + Kompli Ara + Neħħi + Iktar informazzjoni + \@string/home_play + Jista\' jkun hemm bżonn ta\' VPN biex dan il-fornitur jaħdem b\'mod korrett + Il-metadata mhix ipprovduta mis-sit, it-tagħbija tal-vidjo se tfalli jekk ma teżistix fuq is-sit. + Deskrizzjoni + Lebda Plot misjub + Lebda Deskrizzjoni misjuba + Uri Logcat 🐈 + ġurnal + Stampa f-istampa + Ikompli d-daqq fi player minjatura fuq apps oħra + %1$s Ep %2$d + %1$dd %2$dh %3$dm + Mur Lura + Ara l\'isfond + Mili + Ibda l-film + Sottotitli + Sut + Ibda l-fajl + Niżżel + Hassar il-fajl + Kompli Nizzel + Ieqaf Nizzel + Iddiżattiva r-rappurtar awtomatiku tal-bugs + Iktar Informazzjoni + Aħbi + Iffiltra l-Bookmarks + Beda t-tniżżil + Bookmarks + Neħħi + Falla t-tniżżil + \ No newline at end of file diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index bdc55780..4bf0f565 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -159,4 +159,4 @@ କୌଣସି ତଥ୍ୟ ନାହିଁ %1$s ଅ %2$d ଆଦ୍ୟ ବାଦ୍ ଦିଅ - + \ 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 3e22ba16..b80c1b79 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -137,7 +137,7 @@ Błąd tworzenia kopii zapasowej %s Szukaj Konta i zabezpieczenia - Aktualizacje i kopia zapasowa + Aktualizacje i kopie zapasowe Informacje Zaawansowane wyszukiwanie Szukaj z podziałem na źródła @@ -278,7 +278,7 @@ Pokaż przycisk do losowania na stronie głównej i w bibliotece Języki rozszerzeń Układ aplikacji - Preferowane media + Preferowane multimedia Włącz NSFW w obsługiwanych rozszerzeniach Kodowanie napisów Źródła @@ -405,7 +405,7 @@ Ścieżki Ścieżki audio Ścieżki wideo - Zastosuj po ponownym uruchomieniu + Uruchom ponownie aplikację, aby zobaczyć zmiany. Tryb bezpieczny włączony Z powodu wystąpienia błędu wszystkie rozszerzenia zostały wyłączone, aby ułatwić wykrycie tego wadliwego. Wyświetl informacje o błędzie @@ -610,4 +610,12 @@ Błąd dostępu do schowka. Spróbuj ponownie. skopiowano! Błąd podczas kopiowania. Skopiuj logcat i skontaktuj się z pomocą techniczną aplikacji. - + Wyłącz optymalizację akumulatora + Nie można otworzyć informacji o aplikacji CloudStream. + Muzyka + Audiobook + OK + Multimedia + Użycie akumulatora przez aplikację jest już ustawione na nieograniczone + Aby zapewnić nieprzerwane pobieranie i powiadomienia o subskrybowanych programach telewizyjnych, CloudStream potrzebuje pozwolenia na działanie w tle. Naciskając OK, zostaniesz przekierowany do informacji o aplikacji. Tam przewiń do użycia akumulatora przez aplikację i ustaw je na nieograniczone. Pamiętaj, że to pozwolenie nie oznacza, że CS3 będzie zużywać akumulator. Będzie działać w tle tylko wtedy, gdy będzie to konieczne, na przykład podczas odbierania powiadomień lub pobierania filmów z oficjalnych rozszerzeń. Jeśli zdecydujesz się anulować, możesz dostosować to ustawienie później w ustawieniach głównych. + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b3180fee..82054b6f 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -143,7 +143,7 @@ Erro no backup de %s Procurar Contas e segurança - Atualizações e backup + Atualizações e cópias de segurança Info Procura Avançada Mostra resultados separados por fornecedor @@ -458,7 +458,7 @@ Inscrição cancelada em %s Final misto Avaliações (Decrescente) - Aplicar ao reiniciar + Reinicie a aplicação para ver as alterações. Referenciador (opcional) Player oculto - Quantidade de Busca Proxy do GitHub @@ -605,6 +605,14 @@ Autenticação por palavra-passe/PIN A autenticação biométrica não é suportada neste dispositivo Desbloqueie a aplicação com impressão digital, ID facial, PIN, padrão e palavra-passe. - Esta janela fechar-se-á após algumas tentativas falhadas. Terá de reiniciar a aplicação. - Foi feita uma cópia de segurança dos seus dados CloudStream, embora a probabilidade deste caso raro seja muito baixa, mas todos os dispositivos se comportam de forma diferente. No caso de ficar impedido de aceder à aplicação, na pior das hipóteses, limpe totalmente os dados da aplicação e restaure a cópia de segurança. Lamentamos profundamente qualquer inconveniente. - + Este ecrã foi encerrado devido a várias tentativas falhadas. Reinicie a aplicação. + Os dados do seu CloudStream já foram copiados. Embora a possibilidade de isto acontecer ser muito baixa, todos os dispositivos podem comportar-se de forma diferente. No caso raro de ficar impedido de aceder à aplicação, limpe completamente os dados da aplicação e restaure a partir de uma cópia de segurança. Lamentamos qualquer incómodo causado por esta situação. + OK + A utilização da bateria da aplicação já está definida como sem restrições + Não é possível abrir a informação da aplicação CloudStream. + Música + Livro Aúdio + Multimédia + Desativar a otimização da bateria + Para garantir descarregamentos ininterruptos e notificações de programas de TV subscritos, o CloudStream precisa de permissão para ser executado em segundo plano. Ao premir OK, será direcionado para informações da aplicação. Aí, desloque-se para utilização da bateria da aplicação e defina a utilização da bateria para sem restrições. Tenha em atenção que esta permissão não significa que o CS3 irá esgotar a sua bateria. Este só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se optar por cancelar, pode ajustar esta definição mais tarde em definições gerais. + \ 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 16f4449b..a71cc71b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -39,7 +39,7 @@ Заполнитель CloudStream Убирать - %1$s Серия %2$d + %1$s Ep %2$d Смотреть с CloudStream Главная Поиск @@ -148,8 +148,8 @@ Загружена резервная копия Не удалось восстановить данные из %s Отсутствует разрешение на хранение. Пожалуйста попробуйте снова. - Аккаунты - Обновления и резервное копирование + Аккаунты и Безопасность + Обновления и Резервное копирование Информация Расширенный поиск Показывать трейлеры @@ -457,7 +457,7 @@ Загрузить из интернета Загрузка обновления приложения… Недопустимый URL - Будет применено при перезапуске + Перезапустите приложение, чтобы увидеть изменения. Отчеты ошибках Что вы хотите увидеть Смотрите видео на этих языках @@ -593,4 +593,27 @@ Этот тест предназначен только для разработчиков и не подтверждает или не опровергает работоспособность провайдеров. Добавление настроек скорости в плеер Протестировать всех провайдеров - + скопировано! + ОК + Имя репозитория и URL адрес + Ошибка доступа к буферу обмена, пожалуйста, попробуйте ещё раз + Ошибка при копировании, пожалуйста, скопируйте лог и свяжитесь с технической поддержкой. + Нелюбимое + Разблокировать CloudStream + Любимое + Использование батареи приложением уже настроено на неограниченное + Не удается открыть информацию о приложении CloudStream. + Заблокировать биометрией + Музыка + Аудиокнига + Медиа + Разблокируйте приложение с помощью отпечатка пальца, Face ID, PIN-кода, шаблона и пароля. + %s +\nосталось + Отключить оптимизацию батареи + Аутентификация по паролю/PIN-коду + Биометрическая аутентификация на этом устройстве не поддерживается + Этот экран был закрыт из-за нескольких неудачных попыток. Пожалуйста, перезапустите приложение. + Ваши данные в CloudStream были скопированы. Хотя вероятность этого очень мала, все устройства могут вести себя по-разному. В редких случаях, когда доступ к приложению заблокирован, полностью удалите данные приложения и восстановите их из резервной копии. Мы приносим свои извинения за любые неудобства, связанные с этим. + Чтобы обеспечить бесперебойную загрузку и получение уведомлений о телепередачах, на которые вы подписаны, CloudStream необходимо разрешение на запуск в фоновом режиме. Нажав OK, вы перейдете к информации о приложении. Там перейдите к разделу 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и установите значение \"Использование батареи\" 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Пожалуйста, обратите внимание, что это разрешение не означает, что CS3 разрядит вашу батарею. Он будет работать в фоновом режиме только при необходимости, например, при получении уведомлений или загрузке видео с официальных расширений. Если вы решите отменить, вы можете изменить эту настройку позже в 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 7005fd95..3a5170a3 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -162,7 +162,7 @@ %s yedeklenirken hata Ara Hesaplar ve Güvenlik - Güncellemeler ve yedekleme + Güncellemeler ve Yedekleme Bilgi Gelişmiş arama Arama sonuçlarını sağlayıcıya göre ayırır @@ -466,7 +466,7 @@ Parçalar Ses parçaları Video parçaları - Yeniden başlatmada uygula + Değişiklikleri görmek için uygulamayı yeniden başlatın. Güvenli mod açık Çöküşe neden olan eklentiyi bulmaya yardımcı olabilmek için tüm eklentiler kapatıldı. Çökme bilgisini göster @@ -656,4 +656,12 @@ kopyalandı! Panoya erişimde hata oluştu. Lütfen tekrar deneyin. Kopyalama hatası. Lütfen logcat\'i kopyalayın ve uygulama desteğiyle iletişime geçin. - + Tamam + Pil optimizasyonunu devre dışı bırak + CloudStream\'in Uygulama bilgileri açılamıyor. + Müzik + Sesli Kitap + Medya + Abone olunan TV şovları için kesintisiz indirmeleri ve bildirimleri sağlamak için, CloudStream\'in arka planda çalışmasına izin vermeniz gerekmektedir. Tamam\'a basarak Uygulama bilgilerine yönlendirileceksiniz. Orada, 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 (Uygulama pil kullanımı) kısmına gidip pil kullanımını 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙 (Sınırsız) olarak ayarlayın. Bu iznin CS3\'ün pilinizi hızlıca tüketeceği anlamına gelmediğini lütfen unutmayın. Sadece gerektiğinde, resmi eklentilerden bildirim almak veya videoları indirmek gibi durumlarda arka planda çalışacaktır. İptal etmeyi seçerseniz, bu ayarı daha sonra 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨 (Genel Ayarlar) bölümünden ayarlayabilirsiniz. + Uygulama pil kullanımı zaten sınırsız olarak ayarlanmış + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 130e50af..981ac19b 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -433,7 +433,7 @@ Усі субтитри у верхньому регістрі %s (Вимкнено) Відео доріжки - Застосується при перезавантаженні + Перезапустіть застосунок, щоб побачити зміни. Переглянути інформацію про збій Рейтинг: %s Опис @@ -609,4 +609,12 @@ Назва репозиторію та URL Помилка копіювання, будь ласка, скопіюйте logcat й зверніться до служби підтримки застосунку. Помилка доступу до буфера обміну, спробуйте ще раз. - + Добре + Вимкнути оптимізацію батареї + Щоб забезпечити безперебійне завантаження та повідомлення про підписані телепередачі, CloudStream потребує дозволу на роботу у фоновому режимі. Натиснувши \"Добре\", вас буде перенаправлено до інформації про застосунок. Там прокрутіть до \"Споживання батареї застосунком\" й встановіть використання батареї на \"Без обмежень\". Зверніть увагу, що цей дозвіл не означає, що CS3 розряджатиме вашу батарею. Він працюватиме у фоновому режимі лише за необхідності, наприклад, під час отримання повідомлень або завантаження відео з офіційних розширень. Якщо ви вирішите скасувати дозвіл, ви зможете змінити це налаштування пізніше в загальних налаштуваннях. + Споживання батареї застосунком вже налаштовано на без обмежень + Не вдається відкрити інформацію про застосунок CloudStream. + Аудіо книга + Музика + Медіа + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index ad60f597..202af75c 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -123,7 +123,7 @@ Cài đặt phụ đề Phụ đề Chromecast Cài đặt phụ đề Chromecast - Chỉnh tốc độ phim + Tốc độ phát Vuốt để tua nhanh Vuốt sang trái hoặc phải để tua video Vuốt để chỉnh độ sáng và âm lượng @@ -147,8 +147,8 @@ 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 - Cập nhật và sao lưu + Tài khoản và Bảo mật + Cập nhật và Sao lưu Thông tin Tìm kiếm nâng cao Cho phép tìm kiếm theo bộ lọc từng nhà cung cấp @@ -286,7 +286,7 @@ Disclaimer Tổng quan Nút ngẫu nhiên - Hiện nút ngẫu nhiên trên trang chủ + Hiện nút ngẫu nhiên trên Trang chủ và Thư viện Ngôn ngữ nguồn phim Giao diện App Thể loại ưu tiên @@ -307,7 +307,7 @@ Tài khoản Email 127.0.0.1 - Địa chỉ trang web + Địa chỉ trang web­ https://example.com Mã ngôn ngữ (vi) %1$s %2$s @@ -431,7 +431,7 @@ Thêm Âm thanh Chất lượng Video - Áp dụng khi khởi động lại + Áp dụng khi khởi động lại. Chế độ an toàn được bật Đã xảy ra sự cố và chúng tôi đã tự động tắt tất cả các tiện ích mở rộng, hãy tìm và xóa tiện ích mở rộng đang gây ra sự cố. Xem thông tin sự cố @@ -535,7 +535,7 @@ \nKhông tải bất cứ tiện ích mở rộng nào khi khởi động cho đến khi loại bỏ tệp. Đảo ngược lại Đang cập nhật các phim đã đăng kí - Bỏ qua chặn GitHub bằng cách dùng jsDelivr. Có thể gây ra việc cập nhật bị chậm vài ngày. + Bỏ qua chặn đường link GitHub bằng cách dùng jsDelivr. Có thể gây ra việc cập nhật bị chậm vài ngày. Lượng tua thêm được sử dụng khi trình phát ẩn Lượng tua thêm Lượng tua thêm được sử dụng khi trình phát hiện lên @@ -606,4 +606,27 @@ Hiển thị nút xoay màn hình Kích hoạt chế độ xoay màn hình tự động Tự động xoay - + đã sao chép! + Vấn đề truy cập Bảng ghi tạm, Hãy thử lại. + Lỗi sao chép, Hãy sao chép logcat và liên hệ hỗ trợ ứng dụng. + Yêu thích + OK + Vô hiệu Tối ưu pin + Không thể mở thông tin ứng dụng của CloudStream. + Không thích + Mở khóa Cloudstream + Nhạc + Sách nói + Khóa với sinh trắc học + %s +\ncòn lại + Xác thực bằng sinh trắc học không được hỗ trợ trên thiết bị này + Mật khẩu/PIN Xác thực + Dữ liệu CloudStream của bạn đã được sao lưu. Dù khả năng rất thấp, nhưng mỗi thiết bị có thể hoạt động khác nhau. Trong trường hợp thiểu số, bạn sẽ bị khóa khỏi ứng dụng, hãy xóa dữ liệu ứng dụng và khởi tạo từ bản sao lưu. Chúng tôi rất xin lỗi vì bất kỳ sự bất tiện nào. + Mở khóa ứng dụng bằng Vân tay, Khuôn mặt, PIN, Hình vẽ và Mật khẩu. + Màn hình bị đóng sau nhiều lần thử thất bại. Hãy khởi động lại ứng dụng. + Phần kiểm thử này chỉ dành cho nhà phát triển và không xác nhận hay từ chối việc hoạt động của nguồn phim. + Chế độ tiêu thụ pin của ứng dụng đã được đặt ở mức không giới hạn + …  +\n——— + \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 2360a7eb..4423f20f 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -651,4 +651,12 @@ \n剩余 测试所有扩展 已复制! - + 访问剪贴板出错,请重试。 + 应用程序电池使用量已设置为不受限制 + 有声书 + 媒体 + 禁用电池最佳化 + 音乐 + 无法打开 CloudStream 的应用程序信息。 + 使用指纹、面部 ID、PIN 码、图案和密码解锁应用程序。 + \ No newline at end of file diff --git a/fastlane/metadata/android/ml-IN/changelogs/2.txt b/fastlane/metadata/android/ml-IN/changelogs/2.txt new file mode 100644 index 00000000..f7523831 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/changelogs/2.txt @@ -0,0 +1 @@ +-ചേഞ്ച്ലോഗ് ചേർത്തു! diff --git a/fastlane/metadata/android/ml-IN/full_description.txt b/fastlane/metadata/android/ml-IN/full_description.txt new file mode 100644 index 00000000..218f9f98 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/full_description.txt @@ -0,0 +1,10 @@ +ക്ലൗഡ് സ്ട്രീം-3 സിനിമകൾ, ടിവി സീരീസ്, ആനിമേഷൻ എന്നിവ സ്ട്രീം ചെയ്യാനും ഡൗൺലോഡ് ചെയ്യാനും നിങ്ങളെ അനുവദിക്കുന്നു. + +പരസ്യങ്ങളും അനലിറ്റിക്‌സും കൂടാതെ ആപ്പ് വരുന്നു ഒപ്പം +ഒന്നിലധികം ട്രെയിലർ, മൂവി സൈറ്റുകൾ എന്നിവയും മറ്റും പിന്തുണയ്ക്കുന്നു, ഉദാഹരണം + +ബുക്ക്മാർക്കുകൾ + +ഉപശീർഷകം ഡൗൺലോഡുകൾ + +ക്രോംകാസ്റ്റ് പിന്തുണ diff --git a/fastlane/metadata/android/ml-IN/short_description.txt b/fastlane/metadata/android/ml-IN/short_description.txt new file mode 100644 index 00000000..f12fe5b5 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/short_description.txt @@ -0,0 +1 @@ +സ്ട്രീം ഒപ്പം ഡൗൺലോഡ് സിനിമകളും, ടിവി സീരീസുകളും, ആനിമേഷനും . diff --git a/fastlane/metadata/android/ml-IN/title.txt b/fastlane/metadata/android/ml-IN/title.txt new file mode 100644 index 00000000..8e89348a --- /dev/null +++ b/fastlane/metadata/android/ml-IN/title.txt @@ -0,0 +1 @@ +ക്ലൗഡ് സ്ട്രീം diff --git a/fastlane/metadata/android/mt/changelogs/2.txt b/fastlane/metadata/android/mt/changelogs/2.txt new file mode 100644 index 00000000..66bbca8f --- /dev/null +++ b/fastlane/metadata/android/mt/changelogs/2.txt @@ -0,0 +1 @@ +- Changelog miżjud! diff --git a/fastlane/metadata/android/mt/full_description.txt b/fastlane/metadata/android/mt/full_description.txt new file mode 100644 index 00000000..da507aae --- /dev/null +++ b/fastlane/metadata/android/mt/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 iħallik tistrimja u tniżżel Films, Serje TV u Anime. + +L-app tiġi mingħajr reklami u analytics u +jappoġġja siti multipli ta' trejlers u films, u aktar, eż. + +Bookmarks + +Downloads tas-sottotitli + +Appoġġ tal-Chromecast diff --git a/fastlane/metadata/android/mt/short_description.txt b/fastlane/metadata/android/mt/short_description.txt new file mode 100644 index 00000000..542b8614 --- /dev/null +++ b/fastlane/metadata/android/mt/short_description.txt @@ -0,0 +1 @@ +Tistrimja u tniżżel films, serje tat-TV u Anime. diff --git a/fastlane/metadata/android/mt/title.txt b/fastlane/metadata/android/mt/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/mt/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/ru-RU/changelogs/2.txt b/fastlane/metadata/android/ru-RU/changelogs/2.txt new file mode 100644 index 00000000..4b9464b6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/2.txt @@ -0,0 +1 @@ +- Добавлен список изменений! diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt new file mode 100644 index 00000000..1790888e --- /dev/null +++ b/fastlane/metadata/android/ru-RU/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 позволяет транслировать и скачивать фильмы, сериалы и аниме. + +Приложение поставляется без рекламы и аналитики и +поддерживает множество сайтов с трейлерами и фильмами, а также многое другое, например + +Книжные закладки + +Загрузка субтитров + +Поддержка Chromecast diff --git a/fastlane/metadata/android/ru-RU/short_description.txt b/fastlane/metadata/android/ru-RU/short_description.txt new file mode 100644 index 00000000..a43bc8a1 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/short_description.txt @@ -0,0 +1 @@ +Транслируйте и скачивайте фильмы, сериалы и аниме. diff --git a/fastlane/metadata/android/ru-RU/title.txt b/fastlane/metadata/android/ru-RU/title.txt new file mode 100644 index 00000000..3c0406a6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/title.txt @@ -0,0 +1 @@ +Облачный поток diff --git a/fastlane/metadata/android/vi/title.txt b/fastlane/metadata/android/vi/title.txt index dde89d58..0afff90c 100644 --- a/fastlane/metadata/android/vi/title.txt +++ b/fastlane/metadata/android/vi/title.txt @@ -1 +1 @@ -CloudStream +double_tap_seek_time_key2 From d8f89df16363b17945b24c77f5777bdeb5d068bc Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 10 Apr 2024 17:14:47 +0200 Subject: [PATCH 449/570] Show player controls on pressing Pad Down (#1031) --- .../com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt | 1 + 1 file changed, 1 insertion(+) 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 d79c44b7..56983190 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 @@ -1159,6 +1159,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } + KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_UP -> { if (!isShowing) { onClickChange() From ff0dea3fbb4d17e05d0077db55407981b8b83abd Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 10 Apr 2024 17:16:04 +0200 Subject: [PATCH 450/570] Fix focus for Tracks selection on TV (#1030) --- .../main/res/layout/player_select_tracks.xml | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/app/src/main/res/layout/player_select_tracks.xml b/app/src/main/res/layout/player_select_tracks.xml index d32e1b4e..94e09d60 100644 --- a/app/src/main/res/layout/player_select_tracks.xml +++ b/app/src/main/res/layout/player_select_tracks.xml @@ -38,21 +38,20 @@ android:requiresFadingEdge="vertical" android:id="@+id/video_tracks_list" android:layout_width="match_parent" - android:layout_height="match_parent" android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" - android:nextFocusLeft="@id/sort_subtitles" - android:nextFocusRight="@id/apply_btt" + android:nextFocusRight="@id/audio_tracks_holder" + tools:listitem="@layout/sort_bottom_single_choice" /> + android:id="@+id/audio_tracks_holder" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="50" + android:orientation="vertical"> @@ -107,17 +106,16 @@ + android:requiresFadingEdge="vertical" + android:id="@+id/auto_tracks_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_rowWeight="1" + android:background="?attr/primaryBlackBackground" + android:nextFocusRight="@id/apply_btt" + android:nextFocusLeft="@id/video_tracks_list" + tools:listfooter="@layout/sort_bottom_footer_add_choice" + tools:listitem="@layout/sort_bottom_single_choice" /> @@ -132,11 +130,12 @@ + style="@style/WhiteButton" + android:layout_gravity="center_vertical|end" + android:text="@string/sort_apply" + android:id="@+id/apply_btt" + android:nextFocusLeft="@id/auto_tracks_list" + android:layout_width="wrap_content" /> Date: Wed, 10 Apr 2024 20:54:15 +0530 Subject: [PATCH 451/570] Created vtbe and EPlay Extractor (#1014) --- .../cloudstream3/extractors/EPlay.kt | 28 +++++++++++++ .../lagradost/cloudstream3/extractors/Vtbe.kt | 40 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 6 ++- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt new file mode 100644 index 00000000..565a2680 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt @@ -0,0 +1,28 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson + +open class EPlayExtractor : ExtractorApi() { + override var name = "EPlay" + override var mainUrl = "https://eplayvid.net" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?): List? { + val response = app.get(url).document + val trueUrl = response.select("source").attr("src") + return listOf( + ExtractorLink( + this.name, + this.name, + trueUrl, + mainUrl, + getQualityFromName(""), // this needs to be auto + false + ) + ) + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt new file mode 100644 index 00000000..65af01ec --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt @@ -0,0 +1,40 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.JsUnpacker +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.getQualityFromName +import java.net.URI + + +open class Vtbe : ExtractorApi() { + override var name = "Vtbe" + override var mainUrl = "https://vtbe.to" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?): List? { + val response = app.get(url,referer=mainUrl).document + val extractedpack =response.selectFirst("script:containsData(function(p,a,c,k,e,d))")?.data().toString() + JsUnpacker(extractedpack).unpack()?.let { unPacked -> + Regex("sources:\\[\\{file:\"(.*?)\"").find(unPacked)?.groupValues?.get(1)?.let { link -> + return listOf( + ExtractorLink( + this.name, + this.name, + link, + referer ?: "", + Qualities.Unknown.value, + URI(link).path.endsWith(".m3u8") + ) + ) + } + } + return null + } +} 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 637f65b9..e5d82d39 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -217,6 +217,8 @@ import com.lagradost.cloudstream3.extractors.Zorofile import com.lagradost.cloudstream3.extractors.Zplayer import com.lagradost.cloudstream3.extractors.ZplayerV2 import com.lagradost.cloudstream3.extractors.Ztreamhub +import com.lagradost.cloudstream3.extractors.EPlayExtractor +import com.lagradost.cloudstream3.extractors.Vtbe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay @@ -864,7 +866,9 @@ val extractorApis: MutableList = arrayListOf( Megacloud(), VidhideExtractor(), StreamWishExtractor(), - EmturbovidExtractor() + EmturbovidExtractor(), + Vtbe(), + EPlayExtractor() ) From ffa7b0248a86b8e2dcc8aa13c742741d7ff99b6d Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 15:26:36 +0000 Subject: [PATCH 452/570] chore(locales): fix locale issues --- .../com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 1 + app/src/main/res/values-ajp/strings.xml | 2 +- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-ml/strings.xml | 2 +- app/src/main/res/values-mt/strings.xml | 4 ++-- app/src/main/res/values-or/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- 20 files changed, 21 insertions(+), 20 deletions(-) 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 c3d84867..ff891c43 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 @@ -98,6 +98,7 @@ val appLanguages = arrayListOf( Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), Triple("", "bahasa Melayu", "ms"), + Triple("", "Malti", "mt"), Triple("", "ဗမာစာ", "my"), Triple("", "नेपाली", "ne"), Triple("", "Nederlands", "nl"), diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index e4e17942..734d5644 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -622,4 +622,4 @@ أوديو بوك الميديا لتضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يلي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي بـ الباكگروند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". ملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآن بـ الباكگروند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ بـ«الإعدادات العامة» \"General settings\"، إزا غيرت رأيك. - \ No newline at end of file + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 2ac4d973..8681398d 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -650,4 +650,4 @@ \nواضبط استخدام البطارية على غير مقيد. يرجى ملاحظة أن هذا الإذن لا يعني أن CS3 سوف يستنزف البطارية. ولن يعمل إلا في الخلفية عند الضرورة، كما هو الحال عند تلقي الإشعارات أو تنزيل مقاطع الفيديو من الملحقات الرسمية. إذا اخترت الإلغاء، فيمكنك ضبط هذا الإعداد لاحقًا في الإعدادات العامة. موسيقى الوسائط - \ 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 e4f47749..40847edf 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -639,4 +639,4 @@ Música Áudio-livro Mídia - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index cc357373..0a8cf997 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -641,4 +641,4 @@ Zakažte optimalizace baterie Aby bylo zajištěno nepřetržité stahování a upozornění na odebírané seriály, potřebuje aplikace CloudStream povolení ke spuštění na pozadí. Stisknutím tlačítka OK budete přesměrováni na informace o aplikaci. Tam přejděte na položku Využití baterie aplikací a nastavte možnost Využití baterie na hodnotu Neomezené. Upozorňujeme, že toto povolení neznamená, že CS3 bude vybíjet baterii. Na pozadí bude pracovat pouze v případě potřeby, například při přijímání oznámení nebo stahování videí z oficiálních rozšíření. Pokud se rozhodnete toto nastavení zrušit, můžete jej později upravit v Obecných nastaveních. Audiokniha - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index bcff5139..20484cd9 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -617,4 +617,4 @@ Media Audiolibro Para garantizar descargas y notificaciones ininterrumpidas para programas de televisión suscritos, CloudStream necesita permiso para ejecutarse en segundo plano. Al presionar OK, se le dirigirá a información de la aplicación. Allí, desplácese hasta Uso de la batería de la aplicación y establezca el uso de la batería en Sin restricciones. Tenga en cuenta que este permiso no significa que CS3 agotará su batería. Solo funcionará en segundo plano cuando sea necesario, como cuando reciba notificaciones o descargue videos de extensiones oficiales. Si decide cancelar, puede ajustar esta configuración más adelante en los ajustes generales. - \ No newline at end of file + diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index e9847af6..db432a61 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -191,4 +191,4 @@ پیش‌فرض کارتون تورنت - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1370ff2b..77c3db15 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -595,4 +595,4 @@ Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. Copié ! Nom du dépôt et adresse internet - \ 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 c3b55ba2..d537a1d5 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -638,4 +638,4 @@ Buku Audio Media Untuk memastikan unduhan dan pemberitahuan tanpa gangguan untuk acara TV berlangganan, CloudStream memerlukan izin untuk berjalan di latar belakang. Dengan menekan OK, Anda akan diarahkan ke Info aplikasi. Di sana, gulir ke Penggunaan baterai aplikasi dan atur penggunaan baterai ke Tidak Terbatas. Harap dicatat, izin ini tidak berarti CS3 akan menguras baterai Anda. Ini hanya akan beroperasi di latar belakang ketika diperlukan, seperti ketika menerima pemberitahuan atau mengunduh video dari ekstensi resmi. Jika Anda memilih untuk membatalkannya, Anda dapat menyesuaikan pengaturan ini nanti di Pengaturan Umum. - \ 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 01031297..040b0f31 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -637,4 +637,4 @@ L\'utilizzo della batteria dell\'app è già impostato su \"Senza restrizioni\" Musica Audiolibro - \ No newline at end of file + diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index a26f902b..279f5511 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -280,4 +280,4 @@ എഡ്ജ് തരം ഔട്ട്ലൈൻ നിറം പശ്ചാത്തല നിറം - \ No newline at end of file + diff --git a/app/src/main/res/values-mt/strings.xml b/app/src/main/res/values-mt/strings.xml index 356a2caa..b2c0356a 100644 --- a/app/src/main/res/values-mt/strings.xml +++ b/app/src/main/res/values-mt/strings.xml @@ -92,7 +92,7 @@ Kompli Ara Neħħi Iktar informazzjoni - \@string/home_play + @string/home_play Jista\' jkun hemm bżonn ta\' VPN biex dan il-fornitur jaħdem b\'mod korrett Il-metadata mhix ipprovduta mis-sit, it-tagħbija tal-vidjo se tfalli jekk ma teżistix fuq is-sit. Deskrizzjoni @@ -123,4 +123,4 @@ Bookmarks Neħħi Falla t-tniżżil - \ No newline at end of file + diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 4bf0f565..bdc55780 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -159,4 +159,4 @@ କୌଣସି ତଥ୍ୟ ନାହିଁ %1$s ଅ %2$d ଆଦ୍ୟ ବାଦ୍ ଦିଅ - \ 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 b80c1b79..c61f0104 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -618,4 +618,4 @@ Multimedia Użycie akumulatora przez aplikację jest już ustawione na nieograniczone Aby zapewnić nieprzerwane pobieranie i powiadomienia o subskrybowanych programach telewizyjnych, CloudStream potrzebuje pozwolenia na działanie w tle. Naciskając OK, zostaniesz przekierowany do informacji o aplikacji. Tam przewiń do użycia akumulatora przez aplikację i ustaw je na nieograniczone. Pamiętaj, że to pozwolenie nie oznacza, że CS3 będzie zużywać akumulator. Będzie działać w tle tylko wtedy, gdy będzie to konieczne, na przykład podczas odbierania powiadomień lub pobierania filmów z oficjalnych rozszerzeń. Jeśli zdecydujesz się anulować, możesz dostosować to ustawienie później w ustawieniach głównych. - \ No newline at end of file + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 82054b6f..06e2352c 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -615,4 +615,4 @@ Multimédia Desativar a otimização da bateria Para garantir descarregamentos ininterruptos e notificações de programas de TV subscritos, o CloudStream precisa de permissão para ser executado em segundo plano. Ao premir OK, será direcionado para informações da aplicação. Aí, desloque-se para utilização da bateria da aplicação e defina a utilização da bateria para sem restrições. Tenha em atenção que esta permissão não significa que o CS3 irá esgotar a sua bateria. Este só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se optar por cancelar, pode ajustar esta definição mais tarde em definições gerais. - \ 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 a71cc71b..cf456f56 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -616,4 +616,4 @@ Этот экран был закрыт из-за нескольких неудачных попыток. Пожалуйста, перезапустите приложение. Ваши данные в CloudStream были скопированы. Хотя вероятность этого очень мала, все устройства могут вести себя по-разному. В редких случаях, когда доступ к приложению заблокирован, полностью удалите данные приложения и восстановите их из резервной копии. Мы приносим свои извинения за любые неудобства, связанные с этим. Чтобы обеспечить бесперебойную загрузку и получение уведомлений о телепередачах, на которые вы подписаны, CloudStream необходимо разрешение на запуск в фоновом режиме. Нажав OK, вы перейдете к информации о приложении. Там перейдите к разделу 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и установите значение \"Использование батареи\" 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Пожалуйста, обратите внимание, что это разрешение не означает, что CS3 разрядит вашу батарею. Он будет работать в фоновом режиме только при необходимости, например, при получении уведомлений или загрузке видео с официальных расширений. Если вы решите отменить, вы можете изменить эту настройку позже в 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. - \ No newline at end of file + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 3a5170a3..c3e5959a 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -664,4 +664,4 @@ Medya Abone olunan TV şovları için kesintisiz indirmeleri ve bildirimleri sağlamak için, CloudStream\'in arka planda çalışmasına izin vermeniz gerekmektedir. Tamam\'a basarak Uygulama bilgilerine yönlendirileceksiniz. Orada, 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 (Uygulama pil kullanımı) kısmına gidip pil kullanımını 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙 (Sınırsız) olarak ayarlayın. Bu iznin CS3\'ün pilinizi hızlıca tüketeceği anlamına gelmediğini lütfen unutmayın. Sadece gerektiğinde, resmi eklentilerden bildirim almak veya videoları indirmek gibi durumlarda arka planda çalışacaktır. İptal etmeyi seçerseniz, bu ayarı daha sonra 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨 (Genel Ayarlar) bölümünden ayarlayabilirsiniz. Uygulama pil kullanımı zaten sınırsız olarak ayarlanmış - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 981ac19b..403640b9 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -617,4 +617,4 @@ Аудіо книга Музика Медіа - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 202af75c..a12570ad 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -629,4 +629,4 @@ Chế độ tiêu thụ pin của ứng dụng đã được đặt ở mức không giới hạn …  \n——— - \ No newline at end of file + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 4423f20f..828703d1 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -659,4 +659,4 @@ 音乐 无法打开 CloudStream 的应用程序信息。 使用指纹、面部 ID、PIN 码、图案和密码解锁应用程序。 - \ No newline at end of file + From e6c111532dd0db555393c78b94bde5c047a168d0 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 13 Apr 2024 19:51:39 +0200 Subject: [PATCH 453/570] Defaults Play button to first unwatched Episode (#1035) --- .../com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 69f8e8aa..6a83f396 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 @@ -33,6 +33,7 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.GeneratorPlayer +import com.lagradost.cloudstream3.ui.player.NEXT_WATCH_EPISODE_PERCENTAGE import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent @@ -782,7 +783,7 @@ class ResultFragmentTv : Fragment() { // resultEpisodeLoading.isVisible = episodes is Resource.Loading if (episodes is Resource.Success) { - val lastWatchedIndex = episodes.value.indexOfLast { ep -> ep.videoWatchState == VideoWatchState.Watched } + val lastWatchedIndex = episodes.value.indexOfLast { ep -> ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f } val firstUnwatched = episodes.value.getOrElse(lastWatchedIndex + 1) { episodes.value.firstOrNull() } if (firstUnwatched != null) { From afdc4988ac5fa54060c6fae4dc56abdf7679b08f Mon Sep 17 00:00:00 2001 From: Rushikesh Chavan <66415100+rushi-chavan@users.noreply.github.com> Date: Sat, 13 Apr 2024 10:52:08 -0700 Subject: [PATCH 454/570] Extractor: Update Vidplay Extractor (#1036) --- .../main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt index b9a07a6d..d5d0fb32 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -66,7 +66,7 @@ open class Vidplay : ExtractorApi() { } private suspend fun callFutoken(id: String, url: String): String? { - val script = app.get("$mainUrl/futoken").text + val script = app.get("$mainUrl/futoken", referer = url).text val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null val a = mutableListOf(k) for (i in id.indices) { From aa8972870ccbaf1362be32ba134115463259fe5a Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Sat, 13 Apr 2024 22:45:58 +0000 Subject: [PATCH 455/570] Show download size on videos (#1038) --- .../ui/result/ResultViewModel2.kt | 9 +++++-- .../cloudstream3/utils/ExtractorApi.kt | 26 ++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) 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 84b8cf48..37a905a7 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 @@ -5,6 +5,7 @@ import android.content.* import android.net.Uri import android.os.Build import android.os.Bundle +import android.text.format.Formatter.formatFileSize import android.util.Log import android.widget.Toast import androidx.annotation.MainThread @@ -20,7 +21,6 @@ import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS -import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.getCastSession @@ -1280,9 +1280,14 @@ class ResultViewModel2 : ViewModel() { callback: (Pair) -> Unit, ) { loadLinks(result, isVisible = true, type) { links -> + // Could not find a better way to do this + val context = AcraApplication.context postPopup( text, - links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { + links.links.apmap { + val size = it.getVideoSize()?.let { size -> " " + formatFileSize(context, size) } ?: "" + txt("${it.name} ${Qualities.getStringByInt(it.quality)}$size") + }) { callback.invoke(links to (it ?: return@postPopup)) } } 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 e5d82d39..5a845326 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -404,9 +404,29 @@ open class ExtractorLink constructor( open val extractorData: String? = null, open val type: ExtractorLinkType, ) : VideoDownloadManager.IDownloadableMinimum { - val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8 - val isDash : Boolean get() = type == ExtractorLinkType.DASH - + val isM3u8: Boolean get() = type == ExtractorLinkType.M3U8 + val isDash: Boolean get() = type == ExtractorLinkType.DASH + + // Cached video size + private var videoSize: Long? = null + + /** + * Get video size in bytes with one head request. Only available for ExtractorLinkType.Video + * @param timeoutSeconds timeout of the head request. + */ + suspend fun getVideoSize(timeoutSeconds: Long = 3L): Long? { + // Content-Length is not applicable to other types of formats + if (this.type != ExtractorLinkType.VIDEO) return null + + videoSize = videoSize ?: runCatching { + val response = + app.head(this.url, headers = headers, referer = referer, timeout = timeoutSeconds) + response.headers["Content-Length"]?.toLong() + }.getOrNull() + + return videoSize + } + @JsonIgnore fun getAllHeaders() : Map { if (referer.isBlank()) { From 5db541d7ccdc6e305002e2169fa84c56aa0018ab Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Sun, 14 Apr 2024 02:13:12 +0200 Subject: [PATCH 456/570] feat(ui): added reset button to subtitle delay (#1040) --- .../cloudstream3/ui/player/FullScreenPlayer.kt | 14 ++++++-------- app/src/main/res/layout/subtitle_offset.xml | 7 +++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 14 insertions(+), 8 deletions(-) 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 56983190..c357ce9c 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 @@ -14,13 +14,7 @@ import android.os.Bundle import android.provider.Settings import android.text.Editable import android.text.format.DateUtils -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.Surface -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager +import android.view.* import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES import android.view.animation.AlphaAnimation import android.view.animation.Animation @@ -50,7 +44,6 @@ import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog @@ -498,6 +491,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() { dialog.dismissSafe(activity) player.seekTime(1L) } + resetBtt.setOnClickListener { + subtitleDelay = 0 + dialog.dismissSafe(activity) + player.seekTime(1L) + } cancelBtt.setOnClickListener { subtitleDelay = beforeOffset dialog.dismissSafe(activity) diff --git a/app/src/main/res/layout/subtitle_offset.xml b/app/src/main/res/layout/subtitle_offset.xml index c17c5eff..d5e303b6 100644 --- a/app/src/main/res/layout/subtitle_offset.xml +++ b/app/src/main/res/layout/subtitle_offset.xml @@ -113,6 +113,13 @@ + + Music Audio Book Media + Reset \ No newline at end of file From 6df3ef14f66dd3cfc038ee922c563684eb84ce4e Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Tue, 16 Apr 2024 21:07:28 +0000 Subject: [PATCH 457/570] First steps for multiplatform API (#1003) * First steps for multiplatform api * Buildconfig testing * Fix publishing and classes.jar * Update build.gradle.kts --- .idea/gradle.xml | 7 +- app/build.gradle.kts | 34 ++++++++-- .../com/lagradost/cloudstream3/MainAPI.kt | 2 - .../lagradost/cloudstream3/mvvm/Lifecycle.kt | 16 +++++ build.gradle.kts | 8 ++- library/build.gradle.kts | 68 +++++++++++++++++++ library/src/androidMain/AndroidManifest.xml | 2 + .../kotlin/com/lagradost/api/Log.kt | 21 ++++++ .../kotlin/com/lagradost/api/Log.kt | 8 +++ .../com/lagradost/cloudstream3/MainApi.kt | 3 + .../cloudstream3/mvvm/ArchComponentExt.kt | 35 +++------- .../jvmMain/kotlin/com/lagradost/api/Log.kt | 19 ++++++ settings.gradle.kts | 3 +- 13 files changed, 185 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt create mode 100644 library/build.gradle.kts create mode 100644 library/src/androidMain/AndroidManifest.xml create mode 100644 library/src/androidMain/kotlin/com/lagradost/api/Log.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/api/Log.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt (86%) create mode 100644 library/src/jvmMain/kotlin/com/lagradost/api/Log.kt diff --git a/.idea/gradle.xml b/.idea/gradle.xml index c5c0ff3b..d7c08c9c 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,17 +4,16 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 02946e85..e07162d7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import org.jetbrains.dokka.gradle.DokkaTask +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.ByteArrayOutputStream import java.net.URL @@ -13,6 +14,7 @@ plugins { val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/" val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first() +var isLibraryDebug = false fun String.execute() = ByteArrayOutputStream().use { baot -> if (project.exec { @@ -103,6 +105,7 @@ android { ) } debug { + isLibraryDebug = true isDebuggable = true applicationIdSuffix = ".debug" proguardFiles( @@ -232,18 +235,37 @@ dependencies { implementation("androidx.work:work-runtime:2.9.0") implementation("androidx.work:work-runtime-ktx:2.9.0") implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib + + implementation(project(":library") { + this.extra.set("isDebug", isLibraryDebug) + }) } -tasks.register("androidSourcesJar", Jar::class) { +tasks.register("androidSourcesJar") { archiveClassifier.set("sources") from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources } -// For GradLew Plugin -tasks.register("makeJar", Copy::class) { - from("build/intermediates/compile_app_classes_jar/prereleaseDebug") - into("build") - include("classes.jar") +tasks.register("copyJar") { + from( + "build/intermediates/compile_app_classes_jar/prereleaseDebug", + "../library/build/libs" + ) + into("build/app-classes") + include("classes.jar", "library-jvm*.jar") + // Remove the version + rename("library-jvm.*.jar", "library-jvm.jar") +} + +// Merge the app classes and the library classes into classes.jar +tasks.register("makeJar") { + dependsOn(tasks.getByName("copyJar")) + from( + zipTree("build/app-classes/classes.jar"), + zipTree("build/app-classes/library-jvm.jar") + ) + destinationDirectory.set(layout.buildDirectory) + archivesName = "classes" } tasks.withType { diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index ecbdcbbc..7b1b5775 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -743,8 +743,6 @@ fun base64Encode(array: ByteArray): String { } } -class ErrorLoadingException(message: String? = null) : Exception(message) - fun MainAPI.fixUrlNull(url: String?): String? { if (url.isNullOrEmpty()) { return null diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt new file mode 100644 index 00000000..3df5197c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt @@ -0,0 +1,16 @@ +package com.lagradost.cloudstream3.mvvm + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData + +/** NOTE: Only one observer at a time per value */ +fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) + liveData.observe(this) { it?.let { t -> action(t) } } +} + +/** NOTE: Only one observer at a time per value */ +fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) + liveData.observe(this) { action(it) } +} diff --git a/build.gradle.kts b/build.gradle.kts index 801a3c0f..ab1918fe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,8 @@ buildscript { classpath("com.android.tools.build:gradle:8.2.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") + // Universal build config + classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:0.15.1") } } @@ -22,6 +24,6 @@ plugins { id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false } -tasks.register("clean") { - delete(rootProject.layout.buildDirectory) -} +//tasks.register("clean") { +// delete(rootProject.layout.buildDirectory) +//} diff --git a/library/build.gradle.kts b/library/build.gradle.kts new file mode 100644 index 00000000..42a8c943 --- /dev/null +++ b/library/build.gradle.kts @@ -0,0 +1,68 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec + +plugins { + kotlin("multiplatform") + id("maven-publish") + id("com.android.library") + id("com.codingfeline.buildkonfig") +} + +kotlin { + version = "1.0.0" + androidTarget() + jvm() + + sourceSets { + commonMain.dependencies { + implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser + ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API + Level 25 or Less. */ + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + } + } +} + +repositories { + mavenLocal() + maven("https://jitpack.io") +} + +tasks.withType { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +buildkonfig { + packageName = "com.lagradost.api" + exposeObjectWithName = "BuildConfig" + + defaultConfigs { + val isDebug = kotlin.runCatching { extra.get("isDebug") }.getOrNull() == true + buildConfigField(FieldSpec.Type.BOOLEAN, "DEBUG", isDebug.toString()) + } +} + +android { + compileSdk = 34 + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + + defaultConfig { + minSdk = 21 + targetSdk = 33 + } + + // If this is the same com.lagradost.cloudstream3.R stops working + namespace = "com.lagradost.api" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} +publishing { + publications { + withType { + groupId = "com.lagradost.api" + } + } +} \ No newline at end of file diff --git a/library/src/androidMain/AndroidManifest.xml b/library/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..568741e5 --- /dev/null +++ b/library/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/library/src/androidMain/kotlin/com/lagradost/api/Log.kt b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..12524411 --- /dev/null +++ b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,21 @@ +package com.lagradost.api + +import android.util.Log + +actual object Log { + actual fun d(tag: String, message: String) { + Log.d(tag, message) + } + + actual fun i(tag: String, message: String) { + Log.i(tag, message) + } + + actual fun w(tag: String, message: String) { + Log.w(tag, message) + } + + actual fun e(tag: String, message: String) { + Log.e(tag, message) + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/api/Log.kt b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..4b8e6329 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,8 @@ +package com.lagradost.api + +expect object Log { + fun d(tag: String, message: String) + fun i(tag: String, message: String) + fun w(tag: String, message: String) + fun e(tag: String, message: String) +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt new file mode 100644 index 00000000..87ee4815 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt @@ -0,0 +1,3 @@ +package com.lagradost.cloudstream3 + +class ErrorLoadingException(message: String? = null) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt similarity index 86% rename from app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index 817d7db3..d3b4999a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -1,10 +1,7 @@ package com.lagradost.cloudstream3.mvvm -import android.util.Log -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import com.bumptech.glide.load.HttpException -import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.api.BuildConfig +import com.lagradost.api.Log import com.lagradost.cloudstream3.ErrorLoadingException import kotlinx.coroutines.* import java.io.InterruptedIOException @@ -49,18 +46,6 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) { } } -/** NOTE: Only one observer at a time per value */ -fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { - liveData.removeObservers(this) - liveData.observe(this) { it?.let { t -> action(t) } } -} - -/** NOTE: Only one observer at a time per value */ -fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { - liveData.removeObservers(this) - liveData.observe(this) { action(it) } -} - sealed class Resource { data class Success(val value: T) : Resource() data class Failure( @@ -158,14 +143,14 @@ fun throwAbleToResource( "Connection Timeout\nPlease try again later." ) } - is HttpException -> { - Resource.Failure( - false, - throwable.statusCode, - null, - throwable.message ?: "HttpException" - ) - } +// is HttpException -> { +// Resource.Failure( +// false, +// throwable.statusCode, +// null, +// throwable.message ?: "HttpException" +// ) +// } is UnknownHostException -> { Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}") } diff --git a/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..e9a0e6b4 --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,19 @@ +package com.lagradost.api + +actual object Log { + actual fun d(tag: String, message: String) { + println("DEBUG $tag: $message") + } + + actual fun i(tag: String, message: String) { + println("INFO $tag: $message") + } + + actual fun w(tag: String, message: String) { + println("WARNING $tag: $message") + } + + actual fun e(tag: String, message: String) { + println("ERROR $tag: $message") + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 17070047..eabd9f0e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ rootProject.name = "CloudStream" -include(":app") \ No newline at end of file +include(":app") +include(":library") \ No newline at end of file From 9a18ef641136cf9335c830145cf5b1bc4a62f8e3 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Wed, 17 Apr 2024 23:48:33 +0200 Subject: [PATCH 458/570] bugfix: fixing regex special chars break it (#1047) --- .../main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt index 153dbd3e..d9f0b382 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt @@ -50,7 +50,7 @@ class JsUnpacker(packedJS: String?) { throw Exception("Unknown p.a.c.k.e.r. encoding") } val unbase = Unbase(radix) - p = Pattern.compile("\\b\\w+\\b") + p = Pattern.compile("""\b[a-zA-Z0-9_]+\b""") m = p.matcher(payload) val decoded = StringBuilder(payload) var replaceOffset = 0 From 6cef9f7ea257f4af8ed3f739f79c1d01b1b3b36e Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 20 Apr 2024 22:18:49 +0200 Subject: [PATCH 459/570] Filtering first unwatched episode respects watched state (#1049) --- .../com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 6a83f396..13621cda 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 @@ -783,7 +783,10 @@ class ResultFragmentTv : Fragment() { // resultEpisodeLoading.isVisible = episodes is Resource.Loading if (episodes is Resource.Success) { - val lastWatchedIndex = episodes.value.indexOfLast { ep -> ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f } + val lastWatchedIndex = episodes.value.indexOfLast { ep -> + ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f || ep.videoWatchState == VideoWatchState.Watched + } + val firstUnwatched = episodes.value.getOrElse(lastWatchedIndex + 1) { episodes.value.firstOrNull() } if (firstUnwatched != null) { From e01ff4d843810467660add2a8464973a673daa08 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Mon, 22 Apr 2024 01:13:55 +0200 Subject: [PATCH 460/570] Fix NewPipeExtractor lib path (#1050) --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e07162d7..f854865d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -202,7 +202,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:6dc25f7") /* For Trailers + implementation("com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:6dc25f7b97") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding From 4399a612dfa0672acefc7de17c37884ee64331c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Mon, 22 Apr 2024 02:14:36 +0300 Subject: [PATCH 461/570] Update Vidmoly.kt (#1051) --- .../java/com/lagradost/cloudstream3/extractors/Vidmoly.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt index 615cfd74..979fd8c5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt @@ -25,9 +25,13 @@ open class Vidmoly : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - + val headers = mapOf( + "User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + "Sec-Fetch-Dest" to "iframe" + ) val script = app.get( url, + headers = headers, referer = referer, ).document.select("script") .find { it.data().contains("sources:") }?.data() @@ -66,4 +70,4 @@ open class Vidmoly : ExtractorApi() { @JsonProperty("kind") val kind: String? = null, ) -} \ No newline at end of file +} From 0744189020fb3132ebf0debed899e522ab4df246 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:18:54 +0530 Subject: [PATCH 462/570] feat(ui): show account name and image on main settings page (#1001) --- .../ui/settings/SettingsFragment.kt | 52 ++++++++++++++----- app/src/main/res/drawable/rounded_outline.xml | 13 +++++ app/src/main/res/layout/main_settings.xml | 9 ++-- 3 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable/rounded_outline.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index dfa84998..443eeda7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -1,13 +1,13 @@ package com.lagradost.cloudstream3.ui.settings import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.annotation.StringRes import androidx.core.view.children -import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.preference.Preference @@ -18,12 +18,14 @@ import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate @@ -133,7 +135,6 @@ class SettingsFragment : Fragment() { val localBinding = MainSettingsBinding.inflate(inflater, container, false) binding = localBinding return localBinding.root - //return inflater.inflate(R.layout.main_settings, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -141,21 +142,44 @@ class SettingsFragment : Fragment() { activity?.navigate(id, Bundle()) } - // used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}") + /** used to debug leaks + showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : + ${VideoDownloadManager.downloadProgressEvent.size}") **/ - for (syncApi in accountManagers) { - val login = syncApi.loginInfo() - val pic = login?.profilePicture ?: continue - if (binding?.settingsProfilePic?.setImage( - pic, - errorImageDrawable = HomeFragment.errorProfilePic - ) == true - ) { - binding?.settingsProfileText?.text = login.name - binding?.settingsProfile?.isVisible = true - break + fun hasProfilePictureFromAccountManagers(accountManagers: List): Boolean { + for (syncApi in accountManagers) { + val login = syncApi.loginInfo() + val pic = login?.profilePicture ?: continue + + if (binding?.settingsProfilePic?.setImage( + pic, + errorImageDrawable = HomeFragment.errorProfilePic + ) == true + ) { + binding?.settingsProfileText?.text = login.name + return true // sync profile exists + } } + return false // not syncing } + + // display local account information if not syncing + if (!hasProfilePictureFromAccountManagers(accountManagers)) { + val activity = activity ?: return + val currentAccount = try { + DataStoreHelper.accounts.firstOrNull { + it.keyIndex == DataStoreHelper.selectedKeyIndex + } ?: activity.let { DataStoreHelper.getDefaultAccount(activity) } + + } catch (t: IllegalStateException) { + Log.e("AccountManager", "Activity not found", t) + null + } + + binding?.settingsProfilePic?.setImage(currentAccount?.image) + binding?.settingsProfileText?.text = currentAccount?.name + } + binding?.apply { listOf( settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general, diff --git a/app/src/main/res/drawable/rounded_outline.xml b/app/src/main/res/drawable/rounded_outline.xml new file mode 100644 index 00000000..b85ace8e --- /dev/null +++ b/app/src/main/res/drawable/rounded_outline.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/main_settings.xml b/app/src/main/res/layout/main_settings.xml index 2c90d958..0b931843 100644 --- a/app/src/main/res/layout/main_settings.xml +++ b/app/src/main/res/layout/main_settings.xml @@ -24,7 +24,6 @@ android:layout_height="wrap_content" android:orientation="horizontal" android:padding="20dp" - android:visibility="gone" tools:visibility="visible"> + android:scaleType="centerCrop" + android:foreground="@drawable/rounded_outline" + tools:src="@drawable/profile_bg_orange" + android:contentDescription="@string/account"/> + + tools:text="Quick Brown Fox" /> Date: Mon, 22 Apr 2024 16:59:14 +0200 Subject: [PATCH 463/570] Trakt meta provider for extensions (#1026) --- .../metaproviders/TraktProvider.kt | 430 ++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt new file mode 100644 index 00000000..98e12bcd --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -0,0 +1,430 @@ +package com.lagradost.cloudstream3.metaproviders + +import android.net.Uri +import com.lagradost.cloudstream3.* +import com.fasterxml.jackson.annotation.JsonAlias +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import java.util.Locale +import java.text.SimpleDateFormat +import kotlin.math.roundToInt + +open class TraktProvider : MainAPI() { + override var name = "Trakt" + override val hasMainPage = true + override val providerType = ProviderType.MetaProvider + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + TvType.Anime, + ) + + private val traktClientId = base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==") + private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2") + + override val mainPage = mainPageOf( + "$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now + "$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time + "$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now + "$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + + val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page") + + val results = parseJson>(apiResponse).map { element -> + element.toSearchResponse() + } + return newHomePageResponse(request.name, results) + } + + private fun MediaDetails.toSearchResponse(): SearchResponse { + + val media = this.media ?: this + val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries + val poster = media.images?.poster?.firstOrNull() + + if (mediaType == TvType.Movie) { + return newMovieSearchResponse( + name = media.title!!, + url = Data( + type = mediaType, + mediaDetails = media, + ).toJson(), + type = TvType.Movie, + ) { + posterUrl = fixPath(poster) + } + } else { + return newTvSeriesSearchResponse( + name = media.title!!, + url = Data( + type = mediaType, + mediaDetails = media, + ).toJson(), + type = TvType.TvSeries, + ) { + this.posterUrl = fixPath(poster) + } + } + } + + override suspend fun search(query: String): List? { + val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query") + + val results = parseJson>(apiResponse).map { element -> + element.toSearchResponse() + } + + return results + } + override suspend fun load(url: String): LoadResponse { + + val data = parseJson(url) + val mediaDetails = data.mediaDetails + val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows" + + val posterUrl = mediaDetails?.images?.poster?.firstOrNull() + val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull() + + val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full") + + val actors = parseJson(resActor).cast?.map { + ActorData( + Actor( + name = it.person?.name!!, + image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500") + ), + roleString = it.character + ) + } + + val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20") + + val relatedMedia = parseJson>(resRelated).map { it.toSearchResponse() } + + val isCartoon = mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true + val isAnime = isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja") + val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko") + val isBollywood = mediaDetails?.country == "in" + + if (data.type == TvType.Movie) { + + val linkData = LinkData( + id = mediaDetails?.ids?.tmdb, + imdbId = mediaDetails?.ids?.imdb.toString(), + tvdbId = mediaDetails?.ids?.tvdb, + type = data.type.toString(), + title = mediaDetails?.title, + year = mediaDetails?.year, + orgTitle = mediaDetails?.title, + isAnime = isAnime, + //jpTitle = later if needed as it requires another network request, + airedDate = mediaDetails?.released + ?: mediaDetails?.firstAired, + isAsian = isAsian, + isBollywood = isBollywood, + ).toJson() + + return newMovieLoadResponse( + name = mediaDetails?.title!!, + url = data.toJson(), + dataUrl = linkData.toJson(), + type = if (isAnime) TvType.AnimeMovie else TvType.Movie, + ) { + this.name = mediaDetails.title + this.apiName = "Trakt" + this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie + this.posterUrl = getOriginalWidthImageUrl(posterUrl) + this.year = mediaDetails.year + this.plot = mediaDetails.overview + this.rating = mediaDetails.rating?.times(1000)?.roundToInt() + this.tags = mediaDetails.genres + this.duration = mediaDetails.runtime + this.recommendations = relatedMedia + this.actors = actors + this.comingSoon = isUpcoming(mediaDetails.released) + //posterHeaders + this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) + this.contentRating = mediaDetails.certification + addTrailer(mediaDetails.trailer) + addImdbId(mediaDetails.ids?.imdb) + addTMDbId(mediaDetails.ids?.tmdb.toString()) + } + } else { + + val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") + val episodes = mutableListOf() + val seasons = parseJson>(resSeasons) + val seasonsNames = mutableListOf() + + seasons.forEach { season -> + + seasonsNames.add( + SeasonData( + season.number!!, + season.title + ) + ) + + season.episodes?.map { episode -> + + val linkData = LinkData( + id = mediaDetails?.ids?.tmdb, + imdbId = mediaDetails?.ids?.imdb.toString(), + tvdbId = mediaDetails?.ids?.tvdb, + type = data.type.toString(), + season = episode.season, + episode = episode.number, + title = mediaDetails?.title, + year = mediaDetails?.year, + orgTitle = mediaDetails?.title, + isAnime = isAnime, + airedYear = mediaDetails?.year, + lastSeason = seasons.size, + epsTitle = episode.title, + //jpTitle = later if needed as it requires another network request, + date = episode.firstAired, + airedDate = episode.firstAired, + isAsian = isAsian, + isBollywood = isBollywood, + isCartoon = isCartoon + ).toJson() + + episodes.add( + Episode( + data = linkData.toJson(), + name = episode.title, + season = episode.season, + episode = episode.number, + posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()), + rating = episode.rating?.times(10)?.roundToInt(), + description = episode.overview, + ).apply { + this.addDate(episode.firstAired) + } + ) + } + } + + return newTvSeriesLoadResponse( + name = mediaDetails?.title!!, + url = data.toJson(), + type = if (isAnime) TvType.Anime else TvType.TvSeries, + episodes = episodes + ) { + this.name = mediaDetails.title + this.apiName = "Trakt" + this.type = if (isAnime) TvType.Anime else TvType.TvSeries + this.episodes = episodes + this.posterUrl = getOriginalWidthImageUrl(posterUrl) + this.year = mediaDetails.year + this.plot = mediaDetails.overview + this.showStatus = getStatus(mediaDetails.status) + this.rating = mediaDetails.rating?.times(1000)?.roundToInt() + this.tags = mediaDetails.genres + this.duration = mediaDetails.runtime + this.recommendations = relatedMedia + this.actors = actors + this.comingSoon = isUpcoming(mediaDetails.released) + //posterHeaders + this.seasonNames = seasonsNames + this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) + this.contentRating = mediaDetails.certification + addTrailer(mediaDetails.trailer) + addImdbId(mediaDetails.ids?.imdb) + addTMDbId(mediaDetails.ids?.tmdb.toString()) + } + } + } + + private suspend fun getApi(url: String) : String { + return app.get( + url = url, + headers = mapOf( + "Content-Type" to "application/json", + "trakt-api-version" to "2", + "trakt-api-key" to traktClientId, + ) + ).toString() + } + + private fun isUpcoming(dateString: String?): Boolean { + return try { + val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val dateTime = dateString?.let { format.parse(it)?.time } ?: return false + APIHolder.unixTimeMS < dateTime + } catch (t: Throwable) { + logError(t) + false + } + } + + private fun getStatus(t: String?): ShowStatus { + return when (t) { + "returning series" -> ShowStatus.Ongoing + "continuing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + + private fun fixPath(url: String?): String? { + url ?: return null + return "https://$url" + } + + private fun getWidthImageUrl(path: String?, width: String) : String? { + if (path == null) return null + if (!path.contains("image.tmdb.org")) return fixPath(path) + val fileName = Uri.parse(path).lastPathSegment ?: return null + return "https://image.tmdb.org/t/p/${width}/${fileName}" + } + + private fun getOriginalWidthImageUrl(path: String?) : String? { + if (path == null) return null + if (!path.contains("image.tmdb.org")) return fixPath(path) + return getWidthImageUrl(path, "original") + } + + data class Data( + val type: TvType? = null, + val mediaDetails: MediaDetails? = null, + ) + + data class MediaDetails( + @JsonProperty("title") val title: String? = null, + @JsonProperty("year") val year: Int? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("tagline") val tagline: String? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("released") val released: String? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("country") val country: String? = null, + @JsonProperty("updatedAt") val updatedAt: String? = null, + @JsonProperty("trailer") val trailer: String? = null, + @JsonProperty("homepage") val homepage: String? = null, + @JsonProperty("status") val status: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("votes") val votes: Long? = null, + @JsonProperty("comment_count") val commentCount: Long? = null, + @JsonProperty("language") val language: String? = null, + @JsonProperty("languages") val languages: List? = null, + @JsonProperty("available_translations") val availableTranslations: List? = null, + @JsonProperty("genres") val genres: List? = null, + @JsonProperty("certification") val certification: String? = null, + @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("airs") val airs: Airs? = null, + @JsonProperty("network") val network: String? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null + ) + + data class Airs( + @JsonProperty("day") val day: String? = null, + @JsonProperty("time") val time: String? = null, + @JsonProperty("timezone") val timezone: String? = null, + ) + + data class Ids( + @JsonProperty("trakt") val trakt: Int? = null, + @JsonProperty("slug") val slug: String? = null, + @JsonProperty("tvdb") val tvdb: Int? = null, + @JsonProperty("imdb") val imdb: String? = null, + @JsonProperty("tmdb") val tmdb: Int? = null, + @JsonProperty("tvrage") val tvrage: String? = null, + ) + + data class Images( + @JsonProperty("fanart") val fanart: List? = null, + @JsonProperty("poster") val poster: List? = null, + @JsonProperty("logo") val logo: List? = null, + @JsonProperty("clearart") val clearart: List? = null, + @JsonProperty("banner") val banner: List? = null, + @JsonProperty("thumb") val thumb: List? = null, + @JsonProperty("screenshot") val screenshot: List? = null, + @JsonProperty("headshot") val headshot: List? = null, + ) + + data class People( + @JsonProperty("cast") val cast: List? = null, + ) + + data class Cast( + @JsonProperty("character") val character: String? = null, + @JsonProperty("characters") val characters: List? = null, + @JsonProperty("episode_count") val episodeCount: Long? = null, + @JsonProperty("person") val person: Person? = null, + @JsonProperty("images") val images: Images? = null, + ) + + data class Person( + @JsonProperty("name") val name: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + ) + + data class Seasons( + @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, + @JsonProperty("episode_count") val episodeCount: Int? = null, + @JsonProperty("episodes") val episodes: List? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("network") val network: String? = null, + @JsonProperty("number") val number: Int? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("updated_at") val updatedAt: String? = null, + @JsonProperty("votes") val votes: Int? = null, + ) + + data class TraktEpisode( + @JsonProperty("available_translations") val availableTranslations: List? = null, + @JsonProperty("comment_count") val commentCount: Int? = null, + @JsonProperty("episode_type") val episodeType: String? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("number") val number: Int? = null, + @JsonProperty("number_abs") val numberAbs: Int? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("updated_at") val updatedAt: String? = null, + @JsonProperty("votes") val votes: Int? = null, + ) + + data class LinkData( + val id: Int? = null, + val imdbId: String? = null, + val tvdbId: Int? = null, + val type: String? = null, + val season: Int? = null, + val episode: Int? = null, + val aniId: String? = null, + val animeId: String? = null, + val title: String? = null, + val year: Int? = null, + val orgTitle: String? = null, + val isAnime: Boolean = false, + val airedYear: Int? = null, + val lastSeason: Int? = null, + val epsTitle: String? = null, + val jpTitle: String? = null, + val date: String? = null, + val airedDate: String? = null, + val isAsian: Boolean = false, + val isBollywood: Boolean = false, + val isCartoon: Boolean = false, + ) +} \ No newline at end of file From e6b9d621f96beba6e427aa092d09bb448caf8d93 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:00:27 +0200 Subject: [PATCH 464/570] feat(ui): added option to reset sub delay (#1041) --- .../ui/player/AbstractPlayerFragment.kt | 14 ++++++++------ .../lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 3 +++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index cfa6682d..0865b220 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -1,7 +1,10 @@ package com.lagradost.cloudstream3.ui.player import android.annotation.SuppressLint -import android.content.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.graphics.drawable.AnimatedImageDrawable import android.graphics.drawable.AnimatedVectorDrawable import android.media.metrics.PlaybackErrorEvent @@ -24,11 +27,7 @@ import androidx.fragment.app.Fragment import androidx.media3.common.PlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession -import androidx.media3.ui.AspectRatioFrameLayout -import androidx.media3.ui.DefaultTimeBar -import androidx.media3.ui.PlayerView -import androidx.media3.ui.SubtitleView -import androidx.media3.ui.TimeBar +import androidx.media3.ui.* import androidx.preference.PreferenceManager import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import com.github.rubensousa.previewseekbar.PreviewBar @@ -442,6 +441,9 @@ abstract class AbstractPlayerFragment( is VideoEndedEvent -> { context?.let { ctx -> + // Resets subtitle delay on ended video + player.setSubtitleOffset(0) + // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(ctx) ?.getBoolean( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 210bfdca..31adbc87 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -1118,6 +1118,9 @@ class CS3IPlayer : IPlayer { } Player.STATE_ENDED -> { + // Resets subtitle delay on ended video + setSubtitleOffset(0) + // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(context) ?.getBoolean( From e2946cad6b0eb2ef602174f8da38ab1a289ac8e2 Mon Sep 17 00:00:00 2001 From: b4byhuey <60543438+b4byhuey@users.noreply.github.com> Date: Sun, 28 Apr 2024 00:00:40 +0800 Subject: [PATCH 465/570] Added Vidguard Extractor (#1053) --- .../cloudstream3/extractors/Vidguard.kt | 101 ++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 4 +- 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt new file mode 100644 index 00000000..230a9e1a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt @@ -0,0 +1,101 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE +import com.lagradost.cloudstream3.utils.Qualities +import org.mozilla.javascript.Context +import org.mozilla.javascript.NativeJSON +import org.mozilla.javascript.NativeObject +import org.mozilla.javascript.Scriptable +import java.util.Base64 + +open class Vidguardto : ExtractorApi() { + override val name = "Vidguard" + override val mainUrl = "https://vidguard.to" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get(url) + val resc = res.document.select("script:containsData(eval)").firstOrNull()?.data() + resc?.let { + val jsonStr2 = AppUtils.parseJson(runJS2(it)) + val watchlink = sigDecode(jsonStr2.stream) + + callback.invoke( + ExtractorLink( + this.name, + name, + watchlink, + this.mainUrl, + Qualities.Unknown.value, + INFER_TYPE + ) + ) + } + } + + private fun sigDecode(url: String): String { + val sig = url.split("sig=")[1].split("&")[0] + var t = "" + for (v in sig.chunked(2)) { + val byteValue = Integer.parseInt(v, 16) xor 2 + t += byteValue.toChar() + } + val padding = when (t.length % 4) { + 2 -> "==" + 3 -> "=" + else -> "" + } + val decoded = Base64.getDecoder().decode((t + padding).toByteArray(Charsets.UTF_8)) + t = String(decoded).dropLast(5).reversed() + val charArray = t.toCharArray() + for (i in 0 until charArray.size - 1 step 2) { + val temp = charArray[i] + charArray[i] = charArray[i + 1] + charArray[i + 1] = temp + } + val modifiedSig = String(charArray).dropLast(5) + return url.replace(sig, modifiedSig) + } + + private fun runJS2(hideMyHtmlContent: String): String { + Log.d("runJS", "start") + val rhino = Context.enter() + rhino.initSafeStandardObjects() + rhino.optimizationLevel = -1 + val scope: Scriptable = rhino.initSafeStandardObjects() + scope.put("window", scope, scope) + var result = "" + try { + Log.d("runJS", "Executing JavaScript: $hideMyHtmlContent") + rhino.evaluateString(scope, hideMyHtmlContent, "JavaScript", 1, null) + val svgObject = scope.get("svg", scope) + result = if (svgObject is NativeObject) { + NativeJSON.stringify(Context.getCurrentContext(), scope, svgObject, null, null).toString() + } else { + Context.toString(svgObject) + } + Log.d("runJS", "Result: $result") + } catch (e: Exception) { + Log.e("runJS", "Error executing JavaScript", e) + } finally { + Context.exit() + } + return result + } + + data class SvgObject( + val stream: String, + val hash: String + ) +} 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 5a845326..592dc6f9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -186,6 +186,7 @@ import com.lagradost.cloudstream3.extractors.VideoVard import com.lagradost.cloudstream3.extractors.VideovardSX import com.lagradost.cloudstream3.extractors.Vidgomunime import com.lagradost.cloudstream3.extractors.Vidgomunimesb +import com.lagradost.cloudstream3.extractors.Vidguardto import com.lagradost.cloudstream3.extractors.VidhideExtractor import com.lagradost.cloudstream3.extractors.Vidmoly import com.lagradost.cloudstream3.extractors.Vidmolyme @@ -888,7 +889,8 @@ val extractorApis: MutableList = arrayListOf( StreamWishExtractor(), EmturbovidExtractor(), Vtbe(), - EPlayExtractor() + EPlayExtractor(), + Vidguardto() ) From 004c481a5eb8ac8bb0c5a486f2e1f5b35e414f52 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 27 Apr 2024 19:11:22 +0300 Subject: [PATCH 466/570] feat(ui): Episode Air date & Upcoming countdown (#1058) --- .../cloudstream3/ui/result/EpisodeAdapter.kt | 34 ++++++++++++++++++- .../cloudstream3/ui/result/ResultFragment.kt | 5 ++- .../ui/result/ResultViewModel2.kt | 6 ++-- app/src/main/res/drawable/hourglass_24.xml | 9 +++++ .../main/res/layout/result_episode_large.xml | 23 +++++++++++-- app/src/main/res/values/strings.xml | 1 + 6 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/drawable/hourglass_24.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index fad349c8..2019aa50 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -9,9 +9,11 @@ import androidx.core.view.isVisible import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK import com.lagradost.cloudstream3.ui.download.DownloadClickEvent @@ -23,6 +25,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import java.text.DateFormat +import java.text.SimpleDateFormat import java.util.* const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 @@ -104,7 +108,7 @@ class EpisodeAdapter( override fun getItemViewType(position: Int): Int { val item = getItem(position) - return if (item.poster.isNullOrBlank()) 0 else 1 + return if (item.poster.isNullOrBlank() && item.description.isNullOrBlank()) 0 else 1 } @@ -260,6 +264,33 @@ class EpisodeAdapter( } } + if (card.airDate != null) { + val isUpcoming = unixTimeMS < card.airDate + + if (isUpcoming) { + episodePlayIcon.isVisible = false + episodeUpcomingIcon.isVisible = !episodePoster.isVisible + episodeDate.setText( + txt( + R.string.episode_upcoming_format, + secondsToReadable(card.airDate.minus(unixTimeMS).div(1000).toInt(), "") + ) + ) + } else { + episodeUpcomingIcon.isVisible = false + + val formattedAirDate = SimpleDateFormat.getDateInstance( + DateFormat.LONG, + Locale.getDefault() + ).apply { + }.format(Date(card.airDate)) + + episodeDate.setText(txt(formattedAirDate)) + } + } else { + episodeDate.isVisible = false + } + if (isLayout(EMULATOR or PHONE)) { episodePoster.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) @@ -271,6 +302,7 @@ class EpisodeAdapter( } } } + itemView.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } 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 a1574eec..1d3f5a08 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 @@ -50,6 +50,7 @@ data class ResultEpisode( val videoWatchState: VideoWatchState, /** Sum of all previous season episode counts + episode */ val totalEpisodeIndex: Int? = null, + val airDate: Long? = null, ) fun ResultEpisode.getRealPosition(): Long { @@ -85,6 +86,7 @@ fun buildResultEpisode( tvType: TvType, parentId: Int, totalEpisodeIndex: Int? = null, + airDate: Long? = null, ): ResultEpisode { val posDur = getViewPos(id) val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None @@ -107,7 +109,8 @@ fun buildResultEpisode( tvType, parentId, videoWatchState, - totalEpisodeIndex + totalEpisodeIndex, + airDate, ) } 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 37a905a7..499fced2 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 @@ -2277,7 +2277,8 @@ class ResultViewModel2 : ViewModel() { fillers.getOrDefault(episode, false), loadResponse.type, mainId, - totalIndex + totalIndex, + airDate = i.date ) val season = eps.seasonIndex ?: 0 @@ -2326,7 +2327,8 @@ class ResultViewModel2 : ViewModel() { null, loadResponse.type, mainId, - totalIndex + totalIndex, + airDate = episode.date ) val season = ep.seasonIndex ?: 0 diff --git a/app/src/main/res/drawable/hourglass_24.xml b/app/src/main/res/drawable/hourglass_24.xml new file mode 100644 index 00000000..7bd1ebbd --- /dev/null +++ b/app/src/main/res/drawable/hourglass_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index 76e8c434..e5a6881a 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -43,14 +43,26 @@ android:foreground="?android:attr/selectableItemBackgroundBorderless" android:nextFocusRight="@id/download_button" android:scaleType="centerCrop" - tools:src="@drawable/example_poster" /> + tools:src="@drawable/example_poster" + tools:visibility="invisible"/> + android:src="@drawable/play_button" + tools:visibility="invisible"/> + + + + Episodes %1$d-%2$d %1$d %2$s + Upcoming in %s S E No Episodes found From 138e1a1f0ea4515c33274ac4fa3805e9595dd85e Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 28 Apr 2024 04:40:15 +0800 Subject: [PATCH 467/570] Don't check year when checking duplicates if year is empty (#1060) Some sources don't use year which makes this not match when it really should match --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 499fced2..de339aee 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 @@ -1099,13 +1099,14 @@ class ResultViewModel2 : ViewModel() { val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse -> val librarySyncData = it.syncData + val yearCheck = year == it.year || year == null || it.year == null val checks = listOf( { imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId }, { tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId }, { malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId }, { aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId }, - { normalizedName == normalizeString(it.name) && year == it.year } + { normalizedName == normalizeString(it.name) && yearCheck } ) checks.any { it() } From ff1ffbeb836a1bc94d002044ba1863e93fd654dc Mon Sep 17 00:00:00 2001 From: b4byhuey <60543438+b4byhuey@users.noreply.github.com> Date: Mon, 29 Apr 2024 03:42:38 +0800 Subject: [PATCH 468/570] Update Voe.kt (#1062) --- .../lagradost/cloudstream3/extractors/Voe.kt | 66 ++++++++++++++++--- .../cloudstream3/utils/ExtractorApi.kt | 10 ++- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt index 2c6998de..67fd7eea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt @@ -1,19 +1,46 @@ package com.lagradost.cloudstream3.extractors +import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper class Tubeless : Voe() { - override var mainUrl = "https://tubelessceliolymph.com" + override val name = "Tubeless" + override val mainUrl = "https://tubelessceliolymph.com" +} + +class Simpulumlamerop : Voe() { + override val name = "Simplum" + override var mainUrl = "https://simpulumlamerop.com" +} + +class Urochsunloath : Voe() { + override val name = "Uroch" + override var mainUrl = "https://urochsunloath.com" +} + +class Yipsu : Voe() { + override val name = "Yipsu" + override var mainUrl = "https://yip.su" +} + +class MetaGnathTuggers : Voe() { + override val name = "Metagnath" + override val mainUrl = "https://metagnathtuggers.com" } open class Voe : ExtractorApi() { override val name = "Voe" override val mainUrl = "https://voe.sx" override val requiresReferer = true + + private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex() + private val base64Regex = Regex("'.*'") override suspend fun getUrl( url: String, @@ -25,12 +52,33 @@ open class Voe : ExtractorApi() { val script = res.select("script").find { it.data().contains("sources =") }?.data() val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1) - M3u8Helper.generateM3u8( - name, - link ?: return, - "$mainUrl/", - headers = mapOf("Origin" to "$mainUrl/") - ).forEach(callback) - + val videoLinks = mutableListOf() + + if (!link.isNullOrBlank()) { + videoLinks.add( + when { + linkRegex.matches(link) -> link + else -> String(Base64.decode(link, Base64.DEFAULT)) + } + ) + } else { + val link2 = base64Regex.find(script)?.value ?: return + val decoded = Base64.decode(link2, Base64.DEFAULT).toString() + val videoLinkDTO = AppUtils.parseJson(decoded) + videoLinkDTO.let { videoLinks.add(it.toString()) } + } + + videoLinks.forEach { videoLink -> + M3u8Helper.generateM3u8( + name, + videoLink, + "$mainUrl/", + headers = mapOf("Origin" to "$mainUrl/") + ).forEach(callback) + } } -} \ No newline at end of file + + data class WcoSources( + @JsonProperty("VideoLinkDTO") val VideoLinkDTO: String, + ) +} 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 592dc6f9..75dceb54 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -83,6 +83,7 @@ import com.lagradost.cloudstream3.extractors.Maxstream import com.lagradost.cloudstream3.extractors.Mcloud import com.lagradost.cloudstream3.extractors.Megacloud import com.lagradost.cloudstream3.extractors.Meownime +import com.lagradost.cloudstream3.extractors.MetaGnathTuggers import com.lagradost.cloudstream3.extractors.Minoplres import com.lagradost.cloudstream3.extractors.MixDrop import com.lagradost.cloudstream3.extractors.MixDropBz @@ -139,6 +140,7 @@ import com.lagradost.cloudstream3.extractors.Sbspeed import com.lagradost.cloudstream3.extractors.Sbthe import com.lagradost.cloudstream3.extractors.Sendvid import com.lagradost.cloudstream3.extractors.ShaveTape +import com.lagradost.cloudstream3.extractors.Simpulumlamerop import com.lagradost.cloudstream3.extractors.Solidfiles import com.lagradost.cloudstream3.extractors.Ssbstream import com.lagradost.cloudstream3.extractors.StreamM4u @@ -175,6 +177,7 @@ import com.lagradost.cloudstream3.extractors.UpstreamExtractor import com.lagradost.cloudstream3.extractors.Uqload import com.lagradost.cloudstream3.extractors.Uqload1 import com.lagradost.cloudstream3.extractors.Uqload2 +import com.lagradost.cloudstream3.extractors.Urochsunloath import com.lagradost.cloudstream3.extractors.Userload import com.lagradost.cloudstream3.extractors.Userscloud import com.lagradost.cloudstream3.extractors.Uservideo @@ -208,6 +211,7 @@ import com.lagradost.cloudstream3.extractors.Watchx import com.lagradost.cloudstream3.extractors.WcoStream import com.lagradost.cloudstream3.extractors.Wibufile import com.lagradost.cloudstream3.extractors.XStreamCdn +import com.lagradost.cloudstream3.extractors.Yipsu import com.lagradost.cloudstream3.extractors.YourUpload import com.lagradost.cloudstream3.extractors.YoutubeExtractor import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor @@ -890,7 +894,11 @@ val extractorApis: MutableList = arrayListOf( EmturbovidExtractor(), Vtbe(), EPlayExtractor(), - Vidguardto() + Vidguardto(), + Simpulumlamerop(), + Urochsunloath(), + Yipsu(), + MetaGnathTuggers() ) From 949b5830b644d3ac23216dd533d40943ab5f6347 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 1 May 2024 20:29:49 +0300 Subject: [PATCH 469/570] feat(ui): Fix downloads focus on TV (#1066) --- .../cloudstream3/ui/download/DownloadChildFragment.kt | 3 ++- .../cloudstream3/ui/download/DownloadFragment.kt | 3 +++ .../java/com/lagradost/cloudstream3/utils/UIHelper.kt | 10 ++++++++++ app/src/main/res/layout/download_child_episode.xml | 5 ++++- app/src/main/res/layout/download_header_episode.xml | 6 +++++- 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index c3ec2bbd..d138a1e6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar +import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import kotlinx.coroutines.Dispatchers @@ -89,9 +90,9 @@ class DownloadChildFragment : Fragment() { setNavigationOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() } + setAppBarNoScrollFlagsOnTV() } - val adapter: RecyclerView.Adapter = DownloadChildAdapter( ArrayList(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index e08eb772..31790b0f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -41,6 +41,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import java.net.URI @@ -97,6 +98,8 @@ class DownloadFragment : Fragment() { super.onViewCreated(view, savedInstanceState) hideKeyboard() + binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() + observe(downloadsViewModel.noDownloadsText) { binding?.textNoDownloads?.text = it } 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 eedb626a..cb527020 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -45,6 +45,7 @@ import androidx.core.view.marginBottom import androidx.core.view.marginLeft import androidx.core.view.marginRight import androidx.core.view.marginTop +import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.NavHostFragment @@ -58,6 +59,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions.bitmapTransform import com.bumptech.glide.request.target.Target +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup @@ -208,6 +210,14 @@ object UIHelper { } } + fun View?.setAppBarNoScrollFlagsOnTV() { + if (isLayout(Globals.TV or EMULATOR)) { + this?.updateLayoutParams { + scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL + } + } + } + fun Activity.hideKeyboard() { window?.decorView?.clearFocus() this.findViewById(android.R.id.content)?.rootView?.let { diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml index fd845ee8..4974a027 100644 --- a/app/src/main/res/layout/download_child_episode.xml +++ b/app/src/main/res/layout/download_child_episode.xml @@ -9,6 +9,7 @@ android:layout_height="50dp" android:layout_marginBottom="5dp" android:foreground="@drawable/outline_drawable" + android:focusable="true" android:nextFocusLeft="@id/nav_rail_view" android:nextFocusRight="@id/download_button" app:cardBackgroundColor="@color/transparent" @@ -84,7 +85,9 @@ android:layout_height="@dimen/download_size" android:layout_gravity="center_vertical|end" android:layout_marginStart="-50dp" - android:background="?selectableItemBackgroundBorderless" + android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusLeft="@id/download_child_episode_holder" android:padding="10dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index 226c1632..21f79ca6 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -9,6 +9,8 @@ android:layout_marginTop="10dp" android:layout_marginEnd="10dp" android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusRight="@id/download_button" app:cardBackgroundColor="?attr/boxItemBackground" app:cardCornerRadius="@dimen/rounded_image_radius"> @@ -71,7 +73,9 @@ android:layout_height="@dimen/download_size" android:layout_gravity="center_vertical|end" android:layout_marginStart="-50dp" - android:background="?selectableItemBackgroundBorderless" + android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusLeft="@id/episode_holder" android:padding="10dp" /> \ No newline at end of file From c07e6d3222123ce9b711cafa8827f682f9ad9516 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Thu, 2 May 2024 23:58:32 +0200 Subject: [PATCH 470/570] hotfix: Remove resume information (#1063) --- .../cloudstream3/ui/download/button/PieFetchButton.kt | 4 ++++ .../com/lagradost/cloudstream3/utils/VideoDownloadManager.kt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index a729f33a..f1031c24 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -13,6 +13,8 @@ import androidx.annotation.MainThread import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE @@ -25,6 +27,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager +import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES open class PieFetchButton(context: Context, attributeSet: AttributeSet) : @@ -167,6 +170,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : this.setPersistentId(card.id) view.setOnClickListener { if (isZeroBytes) { + removeKey(KEY_RESUME_PACKAGES, card.id.toString()) callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card)) //callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 50a8df02..7d4d5d98 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -187,7 +187,7 @@ object VideoDownloadManager { private val DOWNLOAD_BAD_CONFIG = DownloadStatus(retrySame = false, tryNext = false, success = false) - private const val KEY_RESUME_PACKAGES = "download_resume" + const val KEY_RESUME_PACKAGES = "download_resume" const val KEY_DOWNLOAD_INFO = "download_info" private const val KEY_RESUME_QUEUE_PACKAGES = "download_q_resume" From d3828eeafed0fd4fbeb32c4d37dee2126296b564 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Thu, 2 May 2024 23:59:05 +0200 Subject: [PATCH 471/570] refact: rename logcat file (#1061) Rename logcat file to prevent override --- .../cloudstream3/ui/settings/SettingsUpdates.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index fb24c185..4aaa5e12 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -35,6 +35,9 @@ import okhttp3.internal.closeQuietly import java.io.BufferedReader import java.io.InputStreamReader import java.io.OutputStream +import java.lang.System.currentTimeMillis +import java.text.SimpleDateFormat +import java.util.* class SettingsUpdates : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -125,12 +128,12 @@ class SettingsUpdates : PreferenceFragmentCompat() { } binding.saveBtt.setOnClickListener { + val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) var fileStream: OutputStream? = null try { - fileStream = - VideoDownloadManager.setupStream( + fileStream = VideoDownloadManager.setupStream( it.context, - "logcat", + "logcat_${date}", null, "txt", false From c28a3cb9873d64634b1e7bb131ef648ab40fd22e Mon Sep 17 00:00:00 2001 From: RowdyRushya <66415100+rushi-chavan@users.noreply.github.com> Date: Sat, 4 May 2024 04:15:34 -0700 Subject: [PATCH 472/570] Extractor: new VidSrcTo extractor (#1044) --- .../cloudstream3/extractors/VidSrcTo.kt | 65 +++++++++++++++++++ .../cloudstream3/extractors/Vidplay.kt | 4 ++ .../metaproviders/TmdbProvider.kt | 2 + .../cloudstream3/utils/ExtractorApi.kt | 2 + 4 files changed, 73 insertions(+) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt new file mode 100644 index 00000000..b9065688 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -0,0 +1,65 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import java.net.URLDecoder +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec + +class VidSrcTo : ExtractorApi() { + override val name = "VidSrcTo" + override val mainUrl = "https://vidsrc.to" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return + val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return + if (res.status != 200) return + res.result?.amap { source -> + val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap + val finalUrl = DecryptUrl(embedRes.result.encUrl) + if(finalUrl.equals(embedRes.result.encUrl)) return@amap + when (source.title) { + "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + } + } + } + + private fun DecryptUrl(encUrl: String): String { + var data = encUrl.toByteArray() + data = Base64.decode(data, Base64.URL_SAFE) + val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters) + data = cipher.doFinal(data) + return URLDecoder.decode(data.toString(Charsets.UTF_8), "utf-8") + } + + data class VidsrctoEpisodeSources( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: List? + ) + + data class VidsrctoResult( + @JsonProperty("id") val id: String, + @JsonProperty("title") val title: String + ) + + data class VidsrctoEmbedSource( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: VidsrctoUrl + ) + + data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt index d5d0fb32..c5e01552 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -13,6 +13,10 @@ import javax.crypto.spec.SecretKeySpec // Code found in https://github.com/KillerDogeEmpire/vidplay-keys // special credits to @KillerDogeEmpire for providing key +class AnyVidplay(hostUrl: String) : Vidplay() { + override val mainUrl = hostUrl +} + class MyCloud : Vidplay() { override val name = "MyCloud" override val mainUrl = "https://mcloud.bz" diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt index 50301e22..c5b4d453 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt @@ -105,6 +105,7 @@ open class TmdbProvider : MainAPI() { this.id, episode.episode_number, episode.season_number, + this.name ?: this.original_name, ).toJson(), episode.name, episode.season_number, @@ -122,6 +123,7 @@ open class TmdbProvider : MainAPI() { this.id, episodeNum, season.season_number, + this.name ?: this.original_name, ).toJson(), season = season.season_number ) 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 75dceb54..6106845e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -185,6 +185,7 @@ import com.lagradost.cloudstream3.extractors.Vanfem import com.lagradost.cloudstream3.extractors.Vicloud import com.lagradost.cloudstream3.extractors.VidSrcExtractor import com.lagradost.cloudstream3.extractors.VidSrcExtractor2 +import com.lagradost.cloudstream3.extractors.VidSrcTo import com.lagradost.cloudstream3.extractors.VideoVard import com.lagradost.cloudstream3.extractors.VideovardSX import com.lagradost.cloudstream3.extractors.Vidgomunime @@ -876,6 +877,7 @@ val extractorApis: MutableList = arrayListOf( Streamlare(), VidSrcExtractor(), VidSrcExtractor2(), + VidSrcTo(), PlayLtXyz(), AStreamHub(), Vidplay(), From 83c473d9f801cc43c0716453bea79afc539a1fea Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 4 May 2024 14:16:09 +0300 Subject: [PATCH 473/570] More external Ids in Trakt meta provider (#1075) --- .../cloudstream3/metaproviders/TraktProvider.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 98e12bcd..37c6be1b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -118,8 +118,12 @@ open class TraktProvider : MainAPI() { val linkData = LinkData( id = mediaDetails?.ids?.tmdb, + traktId = mediaDetails?.ids?.trakt, + traktSlug = mediaDetails?.ids?.slug, + tmdbId = mediaDetails?.ids?.tmdb, imdbId = mediaDetails?.ids?.imdb.toString(), tvdbId = mediaDetails?.ids?.tvdb, + tvrageId = mediaDetails?.ids?.tvrage, type = data.type.toString(), title = mediaDetails?.title, year = mediaDetails?.year, @@ -139,7 +143,6 @@ open class TraktProvider : MainAPI() { type = if (isAnime) TvType.AnimeMovie else TvType.Movie, ) { this.name = mediaDetails.title - this.apiName = "Trakt" this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie this.posterUrl = getOriginalWidthImageUrl(posterUrl) this.year = mediaDetails.year @@ -177,8 +180,12 @@ open class TraktProvider : MainAPI() { val linkData = LinkData( id = mediaDetails?.ids?.tmdb, + traktId = mediaDetails?.ids?.trakt, + traktSlug = mediaDetails?.ids?.slug, + tmdbId = mediaDetails?.ids?.tmdb, imdbId = mediaDetails?.ids?.imdb.toString(), tvdbId = mediaDetails?.ids?.tvdb, + tvrageId = mediaDetails?.ids?.tvrage, type = data.type.toString(), season = episode.season, episode = episode.number, @@ -220,7 +227,6 @@ open class TraktProvider : MainAPI() { episodes = episodes ) { this.name = mediaDetails.title - this.apiName = "Trakt" this.type = if (isAnime) TvType.Anime else TvType.TvSeries this.episodes = episodes this.posterUrl = getOriginalWidthImageUrl(posterUrl) @@ -406,8 +412,12 @@ open class TraktProvider : MainAPI() { data class LinkData( val id: Int? = null, + val traktId: Int? = null, + val traktSlug: String? = null, + val tmdbId: Int? = null, val imdbId: String? = null, val tvdbId: Int? = null, + val tvrageId: String? = null, val type: String? = null, val season: Int? = null, val episode: Int? = null, From 71bd48f4930d255beabe6f86b7e4057b732dc70e Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 4 May 2024 14:17:52 +0300 Subject: [PATCH 474/570] feat(ui): Hide Downloads & Settings Back button on TV (#1074) --- .../ui/download/DownloadChildFragment.kt | 11 ++++++++--- .../ui/quicksearch/QuickSearchFragment.kt | 12 ++++++++++-- .../ui/settings/SettingsFragment.kt | 19 ++++++++++++------- app/src/main/res/layout/quick_search.xml | 7 +++---- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index d138a1e6..f54c8698 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -11,6 +11,9 @@ import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys @@ -86,9 +89,11 @@ class DownloadChildFragment : Fragment() { binding?.downloadChildToolbar?.apply { title = name - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } setAppBarNoScrollFlagsOnTV() } 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 e9e00736..85e20d1c 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 @@ -34,6 +34,9 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchViewModel +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.ownShow @@ -274,8 +277,13 @@ class QuickSearchFragment : Fragment() { // UIHelper.showInputMethod(view.findFocus()) // } //} - binding?.quickSearchBack?.setOnClickListener { - activity?.popCurrentPage() + if (isLayout(PHONE or EMULATOR)) { + binding?.quickSearchBack?.apply { + isVisible = true + setOnClickListener { + activity?.popCurrentPage() + } + } } if (isLayout(TV)) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 443eeda7..8ac17928 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.account import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStoreHelper @@ -84,9 +85,11 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } } UIHelper.fixPaddingStatusbar(settingsToolbar) @@ -98,10 +101,12 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } } UIHelper.fixPaddingStatusbar(settingsToolbar) diff --git a/app/src/main/res/layout/quick_search.xml b/app/src/main/res/layout/quick_search.xml index 12d94aaa..84f2c548 100644 --- a/app/src/main/res/layout/quick_search.xml +++ b/app/src/main/res/layout/quick_search.xml @@ -23,11 +23,10 @@ android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_baseline_arrow_back_24" app:tint="@android:color/white" - android:focusable="true" + android:visibility="gone" android:layout_width="25dp" - android:layout_height="wrap_content"> - - + android:layout_height="wrap_content" + tools:visibility="visible"> Date: Sun, 5 May 2024 04:30:42 +0530 Subject: [PATCH 475/570] Updates and Chillx Extractor Updated (#1065) --- .../cloudstream3/extractors/Chillx.kt | 48 ++++++++++--------- .../cloudstream3/extractors/EPlay.kt | 1 - .../lagradost/cloudstream3/extractors/Vtbe.kt | 1 - 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index f03a5525..26567c7a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -2,9 +2,7 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.extractors.helper.* import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler -import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper @@ -28,30 +26,39 @@ open class Chillx : ExtractorApi() { override val name = "Chillx" override val mainUrl = "https://chillx.top" override val requiresReferer = true - private var key: String? = null + companion object { + private var key: String? = null + + suspend fun fetchKey(): String { + return if (key != null) { + key!! + } else { + val fetch = app.get("https://raw.githubusercontent.com/rushi-chavan/multi-keys/keys/keys.json").parsedSafe()?.key?.get(0) ?: throw ErrorLoadingException("Unable to get key") + key = fetch + key!! + } + } + } + + @Suppress("NAME_SHADOWING") override suspend fun getUrl( url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val master = Regex("\\s*=\\s*'([^']+)").find( + val master = Regex("""JScript[\w+]?\s*=\s*'([^']+)""").find( app.get( url, - referer = referer ?: "", - headers = mapOf( - "Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", - "Accept-Language" to "en-US,en;q=0.5", - ) + referer = url, ).text )?.groupValues?.get(1) - val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") - + val key = fetchKey() + val decrypt = cryptoAESHandler(master ?: "", key.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val subtitlePattern = """\[(.*?)\](https?://[^\s,]+)""".toRegex() + val subtitlePattern = """\[(.*?)](https?://[^\s,]+)""".toRegex() val matches = subtitlePattern.findAll(subtitles ?: "") val languageUrlPairs = matches.map { matchResult -> val (language, url) = matchResult.destructured @@ -83,23 +90,18 @@ open class Chillx : ExtractorApi() { headers = headers ).forEach(callback) } - + private fun decodeUnicodeEscape(input: String): String { val regex = Regex("u([0-9a-fA-F]{4})") return regex.replace(input) { it.groupValues[1].toInt(16).toChar().toString() } } - - suspend fun getKey() = key ?: fetchKey().also { key = it } - private suspend fun fetchKey(): String { - return app.get("https://raw.githubusercontent.com/Sofie99/Resources/main/chillix_key.json").parsed() - } - data class Tracks( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, - @JsonProperty("kind") val kind: String? = null, + + data class Keys( + @JsonProperty("chillx") val key: List ) + } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt index 565a2680..2cb12e16 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt index 65af01ec..919a9cbd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* From 3874cb9f9d3a2b0894c59346b92fb6eec4fa2b2e Mon Sep 17 00:00:00 2001 From: b4byhuey <60543438+b4byhuey@users.noreply.github.com> Date: Thu, 9 May 2024 23:06:33 +0800 Subject: [PATCH 476/570] Update Dailymotion Extractor (#1081) --- .../cloudstream3/extractors/Dailymotion.kt | 23 +++++++++++++------ .../cloudstream3/utils/ExtractorApi.kt | 5 +++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt index 0df93dc5..2343a92e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt @@ -9,10 +9,16 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import java.net.URL +class Geodailymotion : Dailymotion() { + override val name = "GeoDailymotion" + override val mainUrl = "https://geo.dailymotion.com" +} + open class Dailymotion : ExtractorApi() { override val mainUrl = "https://www.dailymotion.com" override val name = "Dailymotion" override val requiresReferer = false + private val baseUrl = "https://www.dailymotion.com" @Suppress("RegExpSimplifiable") private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex() @@ -34,7 +40,7 @@ open class Dailymotion : ExtractorApi() { val dmV1st = config.dmInternalData.v1st val dmTs = config.dmInternalData.ts val embedder = config.context.embedder - val metaDataUrl = "$mainUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" + val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies) .parsedSafe() ?: return metaData.qualities.forEach { (_, video) -> @@ -45,16 +51,19 @@ open class Dailymotion : ExtractorApi() { } private fun getEmbedUrl(url: String): String? { - if (url.contains("/embed/")) { - return url - } - val vid = getVideoId(url) ?: return null - return "$mainUrl/embed/video/$vid" + if (url.contains("/embed/") || url.contains("/video/")) { + return url } + if (url.contains("geo.dailymotion.com")) { + val videoId = url.substringAfter("video=") + return "$baseUrl/embed/video/$videoId" + } + return null + } private fun getVideoId(url: String): String? { val path = URL(url).path - val id = path.substringAfter("video/") + val id = path.substringAfter("/video/") if (id.matches(videoIdRegex)) { return id } 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 6106845e..0e4dc870 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -53,6 +53,7 @@ import com.lagradost.cloudstream3.extractors.FileMoonIn import com.lagradost.cloudstream3.extractors.FileMoonSx import com.lagradost.cloudstream3.extractors.Filesim import com.lagradost.cloudstream3.extractors.Fplayer +import com.lagradost.cloudstream3.extractors.Geodailymotion import com.lagradost.cloudstream3.extractors.GMPlayer import com.lagradost.cloudstream3.extractors.Gdriveplayer import com.lagradost.cloudstream3.extractors.Gdriveplayerapi @@ -900,7 +901,9 @@ val extractorApis: MutableList = arrayListOf( Simpulumlamerop(), Urochsunloath(), Yipsu(), - MetaGnathTuggers() + MetaGnathTuggers(), + Geodailymotion(), + ) From f1cc4db89cc6c1a2cd6316e81340206b56a72ad6 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 9 May 2024 18:08:18 +0300 Subject: [PATCH 477/570] Show Season number for next airing episode (#1071) --- .../com/lagradost/cloudstream3/MainAPI.kt | 17 +++++++-- .../ui/result/ResultViewModel2.kt | 6 +++- .../main/res/layout/fragment_result_tv.xml | 36 +++++++++---------- app/src/main/res/values/strings.xml | 1 + 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 7b1b5775..699159b5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -1448,11 +1448,24 @@ fun TvType?.isEpisodeBased(): Boolean { return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama) } - data class NextAiring( val episode: Int, val unixTime: Long, -) + val season: Int? = null, +) { + /** + * Secondary constructor for backwards compatibility without season. + * TODO Remove this constructor after there is a new stable release and extensions are updated to support season. + */ + constructor( + episode: Int, + unixTime: Long, + ) : this ( + episode, + unixTime, + null + ) +} /** * @param season To be mapped with episode season, not shown in UI if displaySeason is defined 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 de339aee..61b65bc2 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 @@ -197,7 +197,11 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { else -> null }?.also { - nextAiringEpisode = txt(R.string.next_episode_format, airing.episode) + nextAiringEpisode = when (airing.season) { + + null -> txt(R.string.next_episode_format, airing.episode) + else -> txt(R.string.next_season_episode_format, airing.season, airing.episode) + } } } } diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 2ec2ae0a..893c19ff 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -178,42 +178,40 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:textStyle="bold" tools:text="The Perfect Run The Perfect Run" /> + + - - + android:orientation="horizontal"> + android:layout_marginEnd="5dp" + tools:text="Season 2 Episode 1022 will be released in" /> %1$s Ep %2$d Cast: %s Episode %d will be released in + Season %1$d Episode %2$d will be released in %1$dd %2$dh %3$dm %1$dh %2$dm %dm From ee4d1dedc5adb1be656a05d1ee0f41b11f9d0a84 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 9 May 2024 19:46:54 +0000 Subject: [PATCH 478/570] Add basic fcast support (#1084) --- .../lagradost/cloudstream3/MainActivity.kt | 3 + .../cloudstream3/ui/player/IGenerator.kt | 10 +- .../cloudstream3/ui/result/EpisodeAdapter.kt | 2 + .../ui/result/ResultViewModel2.kt | 44 ++++++ .../cloudstream3/utils/ExtractorApi.kt | 13 +- .../cloudstream3/utils/fcast/FcastManager.kt | 135 ++++++++++++++++++ .../cloudstream3/utils/fcast/FcastSession.kt | 60 ++++++++ .../cloudstream3/utils/fcast/Packets.kt | 62 ++++++++ app/src/main/res/values/strings.xml | 3 + 9 files changed, 329 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 7baac71c..56322b73 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -161,6 +161,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API +import com.lagradost.cloudstream3.utils.fcast.FcastManager import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser import com.lagradost.safefile.SafeFile @@ -1756,6 +1757,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, runAutoUpdate() } + FcastManager().init(this, false) + APIRepository.dubStatusActive = getApiDubstatusSettings() try { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index af74cb57..c5de1a1c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -10,7 +10,8 @@ enum class LoadType { InAppDownload, ExternalApp, Browser, - Chromecast + Chromecast, + Fcast } fun LoadType.toSet() : Set { @@ -29,12 +30,17 @@ fun LoadType.toSet() : Set { ExtractorLinkType.VIDEO, ExtractorLinkType.M3U8 ) - LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet() + LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.entries.toSet() LoadType.Chromecast -> setOf( ExtractorLinkType.VIDEO, ExtractorLinkType.DASH, ExtractorLinkType.M3U8 ) + LoadType.Fcast -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 2019aa50..e4fd0559 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -55,6 +55,8 @@ const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16 const val ACTION_PLAY_EPISODE_IN_MPV = 17 const val ACTION_MARK_AS_WATCHED = 18 +const val ACTION_FCAST = 19 + const val TV_EP_SIZE_LARGE = 400 const val TV_EP_SIZE_SMALL = 300 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) 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 61b65bc2..a32942f6 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 @@ -83,6 +83,10 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.fcast.FcastManager +import com.lagradost.cloudstream3.utils.fcast.FcastSession +import com.lagradost.cloudstream3.utils.fcast.Opcode +import com.lagradost.cloudstream3.utils.fcast.PlayMessage import kotlinx.coroutines.* import java.io.File import java.util.concurrent.TimeUnit @@ -1519,6 +1523,13 @@ class ResultViewModel2 : ViewModel() { ) ) } + + if (FcastManager.currentDevices.isNotEmpty()) { + options.add( + txt(R.string.player_settings_play_in_fcast) to ACTION_FCAST + ) + } + options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER) for (app in apps) { @@ -1694,6 +1705,39 @@ class ResultViewModel2 : ViewModel() { } } + ACTION_FCAST -> { + val devices = FcastManager.currentDevices.toList() + postPopup( + txt(R.string.player_settings_select_cast_device), + devices.map { txt(it.name) }) { index -> + if (index == null) return@postPopup + val device = devices.getOrNull(index) + + acquireSingleLink( + click.data, + LoadType.Fcast, + txt(R.string.episode_action_cast_mirror) + ) { (result, index) -> + val host = device?.host ?: return@acquireSingleLink + val link = result.links.firstOrNull() ?: return@acquireSingleLink + + FcastSession(host).use { session -> + session.sendMessage( + Opcode.Play, + PlayMessage( + link.type.getMimeType(), + link.url, + headers = mapOf( + "referer" to link.referer, + "user-agent" to USER_AGENT + ) + link.headers + ) + ) + } + } + } + } + ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( click.data, LoadType.Browser, 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 0e4dc870..61cdd26a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -308,7 +308,18 @@ enum class ExtractorLinkType { /** No support at the moment */ TORRENT, /** No support at the moment */ - MAGNET, + MAGNET; + + // See https://www.iana.org/assignments/media-types/media-types.xhtml + fun getMimeType(): String { + return when (this) { + VIDEO -> "video/mp4" + M3U8 -> "application/x-mpegURL" + DASH -> "application/dash+xml" + TORRENT -> "application/x-bittorrent" + MAGNET -> "application/x-bittorrent" + } + } } private fun inferTypeFromUrl(url: String): ExtractorLinkType { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt new file mode 100644 index 00000000..9ff5cc08 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt @@ -0,0 +1,135 @@ +package com.lagradost.cloudstream3.utils.fcast + +import android.content.Context +import android.net.nsd.NsdManager +import android.net.nsd.NsdManager.ResolveListener +import android.net.nsd.NsdServiceInfo +import android.os.Build +import android.util.Log +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe + +class FcastManager { + private var nsdManager: NsdManager? = null + + // Used for receiver + private val registrationListenerTcp = DefaultRegistrationListener() + private fun getDeviceName(): String { + return "${Build.MANUFACTURER}-${Build.MODEL}" + } + + /** + * Start the fcast service + * @param registerReceiver If true will register the app as a compatible fcast receiver for discovery in other app + */ + fun init(context: Context, registerReceiver: Boolean) = ioSafe { + nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager + val serviceType = "_fcast._tcp" + + if (registerReceiver) { + val serviceName = "$APP_PREFIX-${getDeviceName()}" + + val serviceInfo = NsdServiceInfo().apply { + this.serviceName = serviceName + this.serviceType = serviceType + this.port = TCP_PORT + } + + nsdManager?.registerService( + serviceInfo, + NsdManager.PROTOCOL_DNS_SD, + registrationListenerTcp + ) + } + + nsdManager?.discoverServices( + serviceType, + NsdManager.PROTOCOL_DNS_SD, + DefaultDiscoveryListener() + ) + } + + fun stop() { + nsdManager?.unregisterService(registrationListenerTcp) + } + + inner class DefaultDiscoveryListener : NsdManager.DiscoveryListener { + val tag = "DiscoveryListener" + override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) { + Log.d(tag, "Discovery failed: $serviceType, error code: $errorCode") + } + + override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) { + Log.d(tag, "Stop discovery failed: $serviceType, error code: $errorCode") + } + + override fun onDiscoveryStarted(serviceType: String?) { + Log.d(tag, "Discovery started: $serviceType") + } + + override fun onDiscoveryStopped(serviceType: String?) { + Log.d(tag, "Discovery stopped: $serviceType") + } + + override fun onServiceFound(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + nsdManager?.resolveService(serviceInfo, object : ResolveListener { + override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) { + } + + override fun onServiceResolved(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + + currentDevices.add(PublicDeviceInfo(serviceInfo)) + + Log.d( + tag, + "Service found: ${serviceInfo.serviceName}, Net: ${serviceInfo.host.hostAddress}" + ) + } + }) + } + + override fun onServiceLost(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + + // May remove duplicates, but net and port is null here, preventing device specific identification + currentDevices.removeAll { + it.rawName == serviceInfo.serviceName + } + + Log.d(tag, "Service lost: ${serviceInfo.serviceName}") + } + } + + companion object { + const val APP_PREFIX = "CloudStream" + val currentDevices: MutableList = mutableListOf() + + class DefaultRegistrationListener : NsdManager.RegistrationListener { + val tag = "DiscoveryService" + override fun onServiceRegistered(serviceInfo: NsdServiceInfo) { + Log.d(tag, "Service registered: ${serviceInfo.serviceName}") + } + + override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.e(tag, "Service registration failed: errorCode=$errorCode") + } + + override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) { + Log.d(tag, "Service unregistered: ${serviceInfo.serviceName}") + } + + override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.e(tag, "Service unregistration failed: errorCode=$errorCode") + } + } + + const val TCP_PORT = 46899 + } +} + +class PublicDeviceInfo(serviceInfo: NsdServiceInfo) { + val rawName: String = serviceInfo.serviceName + val host: String? = serviceInfo.host.hostAddress + val name = rawName.replace("-", " ") + host?.let { " $it" } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt new file mode 100644 index 00000000..1f33bca4 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt @@ -0,0 +1,60 @@ +package com.lagradost.cloudstream3.utils.fcast + +import android.util.Log +import androidx.annotation.WorkerThread +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.safefile.closeQuietly +import java.io.DataOutputStream +import java.net.Socket +import kotlin.jvm.Throws + +class FcastSession(private val hostAddress: String): AutoCloseable { + val tag = "FcastSession" + + private var socket: Socket? = null + @Throws + @WorkerThread + fun open(): Socket { + val socket = Socket(hostAddress, FcastManager.TCP_PORT) + this.socket = socket + return socket + } + + override fun close() { + socket?.closeQuietly() + socket = null + } + + @Throws + private fun acquireSocket(): Socket { + return socket ?: open() + } + + fun ping() { + sendMessage(Opcode.Ping, null) + } + + fun sendMessage(opcode: Opcode, message: T) { + ioSafe { + val socket = acquireSocket() + val outputStream = DataOutputStream(socket.getOutputStream()) + + val json = message?.toJson() + val content = json?.toByteArray() ?: ByteArray(0) + + // Little endian starting from 1 + // https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 + val size = content.size + 1 + + val sizeArray = ByteArray(4) { num -> + (size shr 8 * num and 0xff).toByte() + } + + Log.d(tag, "Sending message with size: $size, opcode: $opcode") + outputStream.write(sizeArray) + outputStream.write(ByteArray(1) { opcode.value }) + outputStream.write(content) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt new file mode 100644 index 00000000..61c00d6e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt @@ -0,0 +1,62 @@ +package com.lagradost.cloudstream3.utils.fcast + +// See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 +enum class Opcode(val value: Byte) { + None(0), + Play(1), + Pause(2), + Resume(3), + Stop(4), + Seek(5), + PlaybackUpdate(6), + VolumeUpdate(7), + SetVolume(8), + PlaybackError(9), + SetSpeed(10), + Version(11), + Ping(12), + Pong(13); +} + + +data class PlayMessage( + val container: String, + val url: String? = null, + val content: String? = null, + val time: Double? = null, + val speed: Double? = null, + val headers: Map? = null +) + +data class SeekMessage( + val time: Double +) + +data class PlaybackUpdateMessage( + val generationTime: Long, + val time: Double, + val duration: Double, + val state: Int, + val speed: Double +) + +data class VolumeUpdateMessage( + val generationTime: Long, + val volume: Double +) + +data class PlaybackErrorMessage( + val message: String +) + +data class SetSpeedMessage( + val speed: Double +) + +data class SetVolumeMessage( + val volume: Double +) + +data class VersionMessage( + val version: Long +) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9bd2426c..a8108623 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -357,6 +357,7 @@ Download error, check storage permissions Chromecast episode Chromecast mirror + Cast mirror Play in app Play in %s Play in browser @@ -634,7 +635,9 @@ VLC MPV Web Video Cast + Fcast Web browser + Select cast device App not found All Languages Skip %s From af828de8d5264e7d2c3a6d6954b0a2a228ca2264 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 18 May 2024 14:41:37 +0300 Subject: [PATCH 479/570] feat(TV UI: Fix online subtitles dialog focus (#1085) --- app/src/main/res/layout/dialog_online_subtitles.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/dialog_online_subtitles.xml b/app/src/main/res/layout/dialog_online_subtitles.xml index 7803e261..d480bd34 100644 --- a/app/src/main/res/layout/dialog_online_subtitles.xml +++ b/app/src/main/res/layout/dialog_online_subtitles.xml @@ -40,7 +40,7 @@ android:layout_width="match_parent" android:layout_height="30dp" android:layout_gravity="center_vertical" - android:layout_marginEnd="30dp"> + android:layout_marginEnd="40dp"> @@ -106,7 +107,7 @@ android:layout_margin="10dp" android:background="?selectableItemBackgroundBorderless" android:contentDescription="@string/change_providers_img_des" - android:nextFocusLeft="@id/main_search" + android:nextFocusLeft="@id/year_btt" android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" android:nextFocusDown="@id/search_autofit_results" From 4d5cd288abd07c74fc88900c58e119c27f1b7867 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Sat, 18 May 2024 11:47:12 +0000 Subject: [PATCH 480/570] Ported more files for multiplatform (#1056) --- .../lagradost/cloudstream3/CommonActivity.kt | 1 + .../com/lagradost/cloudstream3/MainAPI.kt | 114 ++++++++-- .../lagradost/cloudstream3/MainActivity.kt | 197 ++++++++---------- .../ui/result/ResultViewModel2.kt | 10 +- .../cloudstream3/utils/Coroutines.android.kt | 11 + .../lagradost/cloudstream3/MainActivity.kt | 35 ++++ .../com/lagradost/cloudstream3/MainApi.kt | 3 + .../lagradost/cloudstream3/ParCollections.kt | 0 .../cloudstream3/utils/Coroutines.kt | 8 +- .../lagradost/cloudstream3/utils/JsHunter.kt | 0 .../cloudstream3/utils/JsUnpacker.kt | 0 .../cloudstream3/utils/Coroutines.jvm.kt | 5 + 12 files changed, 243 insertions(+), 141 deletions(-) create mode 100644 library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/ParCollections.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/Coroutines.kt (90%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/JsHunter.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/JsUnpacker.kt (100%) create mode 100644 library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 4dc78dc7..82e985db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -29,6 +29,7 @@ import com.google.android.material.chip.ChipGroup import com.google.android.material.navigationrail.NavigationRailView import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps import com.lagradost.cloudstream3.databinding.ToastBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.PlayerEventType diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 699159b5..07a82583 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -31,19 +31,16 @@ import java.text.SimpleDateFormat import java.util.* import kotlin.math.absoluteValue -const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" - -//val baseHeader = mapOf("User-Agent" to USER_AGENT) -val mapper = JsonMapper.builder().addModule(kotlinModule()) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! - /** * Defines the constant for the all languages preference, if this is set then it is * the equivalent of all languages being set **/ const val AllLanguagesName = "universal" +//val baseHeader = mapOf("User-Agent" to USER_AGENT) +val mapper = JsonMapper.builder().addModule(kotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! + object APIHolder { val unixTime: Long get() = System.currentTimeMillis() / 1000L @@ -121,7 +118,8 @@ object APIHolder { fun LoadResponse.getId(): Int { // this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked - return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) ?: getLoadResponseIdFromUrl(url, apiName) + return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) + ?: getLoadResponseIdFromUrl(url, apiName) } /** @@ -222,10 +220,15 @@ object APIHolder { } ?: false val matchingTypes = types?.any { it.name.equals(media.format, true) } == true - if(lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears + if (lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears } ?: return null - Tracker(res.idMal, res.id.toString(), res.coverImage?.extraLarge ?: res.coverImage?.large, res.bannerImage) + Tracker( + res.idMal, + res.id.toString(), + res.coverImage?.extraLarge ?: res.coverImage?.large, + res.bannerImage + ) } catch (t: Throwable) { logError(t) null @@ -866,6 +869,7 @@ enum class TvType(value: Int?) { Others(12), Music(13), AudioBook(14), + /** Wont load the built in player, make your own interaction */ CustomMedia(15), } @@ -1253,13 +1257,15 @@ interface LoadResponse { fun LoadResponse.getImdbId(): String? { return normalSafeApiCall { - SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Imdb) + SimklApi.readIdFromString(this.syncData[simklIdPrefix]) + ?.get(SimklApi.Companion.SyncServices.Imdb) } } fun LoadResponse.getTMDbId(): String? { return normalSafeApiCall { - SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Tmdb) + SimklApi.readIdFromString(this.syncData[simklIdPrefix]) + ?.get(SimklApi.Companion.SyncServices.Tmdb) } } @@ -1556,8 +1562,26 @@ data class TorrentLoadResponse( posterHeaders: Map? = null, backgroundPosterUrl: String? = null, ) : this( - name, url, apiName, magnet, torrent, plot, type, posterUrl, year, rating, tags, duration, trailers, - recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null + name, + url, + apiName, + magnet, + torrent, + plot, + type, + posterUrl, + year, + rating, + tags, + duration, + trailers, + recommendations, + actors, + comingSoon, + syncData, + posterHeaders, + backgroundPosterUrl, + null ) } @@ -1609,7 +1633,8 @@ data class AnimeLoadResponse( return this.episodes.maxOf { (_, episodes) -> episodes.count { episodeData -> // Prioritize display season as actual season may be something random to fit multiple seasons into one. - val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE + val episodeSeason = + displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE // Count all episodes from season 1 to below the current season. episodeSeason in 1..? = null, backgroundPosterUrl: String? = null, ) : this( - engName, japName, name, url, apiName, type, posterUrl, year, episodes, showStatus, plot, tags, - synonyms, rating, duration, trailers, recommendations, actors, comingSoon, syncData, posterHeaders, - nextAiring, seasonNames, backgroundPosterUrl, null + engName, + japName, + name, + url, + apiName, + type, + posterUrl, + year, + episodes, + showStatus, + plot, + tags, + synonyms, + rating, + duration, + trailers, + recommendations, + actors, + comingSoon, + syncData, + posterHeaders, + nextAiring, + seasonNames, + backgroundPosterUrl, + null ) } @@ -1780,7 +1827,7 @@ data class MovieLoadResponse( backgroundPosterUrl: String? = null, ) : this( name, url, apiName, type, dataUrl, posterUrl, year, plot, rating, tags, duration, trailers, - recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl,null + recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null ) } @@ -1923,7 +1970,8 @@ data class TvSeriesLoadResponse( return episodes.count { episodeData -> // Prioritize display season as actual season may be something random to fit multiple seasons into one. - val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE + val episodeSeason = + displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE // Count all episodes from season 1 to below the current season. episodeSeason in 1..? = null, backgroundPosterUrl: String? = null, ) : this( - name, url, apiName, type, episodes, posterUrl, year, plot, showStatus, rating, tags, duration, - trailers, recommendations, actors, comingSoon, syncData, posterHeaders, nextAiring, seasonNames, - backgroundPosterUrl, null + name, + url, + apiName, + type, + episodes, + posterUrl, + year, + plot, + showStatus, + rating, + tags, + duration, + trailers, + recommendations, + actors, + comingSoon, + syncData, + posterHeaders, + nextAiring, + seasonNames, + backgroundPosterUrl, + null ) } @@ -2022,6 +2089,7 @@ data class AniSearch( @JsonProperty("extraLarge") var extraLarge: String? = null, @JsonProperty("large") var large: String? = null, ) + data class Title( @JsonProperty("romaji") var romaji: String? = null, @JsonProperty("english") var english: String? = null, diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 56322b73..1ff0575b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -174,7 +174,6 @@ import java.net.URLDecoder import java.nio.charset.Charset import kotlin.math.abs import kotlin.math.absoluteValue -import kotlin.reflect.KClass import kotlin.system.exitProcess //https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898 @@ -187,117 +186,93 @@ import kotlin.system.exitProcess //https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225 -const val VLC_PACKAGE = "org.videolan.vlc" -const val MPV_PACKAGE = "is.xyz.mpv" -const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo" - -val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity") -val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") - -//TODO REFACTOR AF -open class ResultResume( - val packageString: String, - val action: String = Intent.ACTION_VIEW, - val position: String? = null, - val duration: String? = null, - var launcher: ActivityResultLauncher? = null, -) { - val defaultTime = -1L - - val lastId get() = "${packageString}_last_open_id" - suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) { - val intent = Intent(action) - - if (id != null) - setKey(lastId, id) - else - removeKey(lastId) - - intent.setPackage(packageString) - callback.invoke(intent) - launcher?.launch(intent) - } - - open fun getPosition(intent: Intent?): Long { - return defaultTime - } - - open fun getDuration(intent: Intent?): Long { - return defaultTime - } -} - -val VLC = object : ResultResume( - VLC_PACKAGE, - // Android 13 intent restrictions fucks up specifically launching the VLC player - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - "org.videolan.vlc.player.result" - } else { - Intent.ACTION_VIEW - }, - "extra_position", - "extra_duration", -) { - override fun getPosition(intent: Intent?): Long { - return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime - } - - override fun getDuration(intent: Intent?): Long { - return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime - } -} - -val MPV = object : ResultResume( - MPV_PACKAGE, - //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive: - position = "position", - duration = "duration", -) { - override fun getPosition(intent: Intent?): Long { - return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime - } - - override fun getDuration(intent: Intent?): Long { - return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime - } -} - -val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE) - -val resumeApps = arrayOf( - VLC, MPV, WEB_VIDEO -) - -// Short name for requests client to make it nicer to use - -var app = Requests(responseParser = object : ResponseParser { - val mapper: ObjectMapper = jacksonObjectMapper().configure( - DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, - false - ) - - override fun parse(text: String, kClass: KClass): T { - return mapper.readValue(text, kClass.java) - } - - override fun parseSafe(text: String, kClass: KClass): T? { - return try { - mapper.readValue(text, kClass.java) - } catch (e: Exception) { - null - } - } - - override fun writeValueAsString(obj: Any): String { - return mapper.writeValueAsString(obj) - } -}).apply { - defaultHeaders = mapOf("user-agent" to USER_AGENT) -} - class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAuthenticator.BiometricAuthCallback { companion object { + const val VLC_PACKAGE = "org.videolan.vlc" + const val MPV_PACKAGE = "is.xyz.mpv" + const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo" + + val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity") + val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") + + //TODO REFACTOR AF + open class ResultResume( + val packageString: String, + val action: String = Intent.ACTION_VIEW, + val position: String? = null, + val duration: String? = null, + var launcher: ActivityResultLauncher? = null, + ) { + val defaultTime = -1L + + val lastId get() = "${packageString}_last_open_id" + suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) { + val intent = Intent(action) + + if (id != null) + setKey(lastId, id) + else + removeKey(lastId) + + intent.setPackage(packageString) + callback.invoke(intent) + launcher?.launch(intent) + } + + open fun getPosition(intent: Intent?): Long { + return defaultTime + } + + open fun getDuration(intent: Intent?): Long { + return defaultTime + } + } + + val VLC = object : ResultResume( + VLC_PACKAGE, + // Android 13 intent restrictions fucks up specifically launching the VLC player + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + "org.videolan.vlc.player.result" + } else { + Intent.ACTION_VIEW + }, + "extra_position", + "extra_duration", + ) { + override fun getPosition(intent: Intent?): Long { + return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime + } + + override fun getDuration(intent: Intent?): Long { + return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime + } + } + + val MPV = object : ResultResume( + MPV_PACKAGE, + //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive: + position = "position", + duration = "duration", + ) { + override fun getPosition(intent: Intent?): Long { + return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() + ?: defaultTime + } + + override fun getDuration(intent: Intent?): Long { + return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() + ?: defaultTime + } + } + + val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE) + + val resumeApps = arrayOf( + VLC, MPV, WEB_VIDEO + ) + + const val TAG = "MAINACT" const val ANIMATED_OUTLINE: Boolean = false var lastError: String? = null @@ -1403,7 +1378,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, } } - observe(viewModel.watchStatus,::setWatchStatus) + observe(viewModel.watchStatus, ::setWatchStatus) observe(syncViewModel.userData, ::setUserData) observeNullable(viewModel.subscribeStatus, ::setSubscribeStatus) @@ -1831,7 +1806,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, } override fun onAuthenticationError() { - finish() + finish() } private var backPressedCallback: OnBackPressedCallback? = null 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 a32942f6..0af01ca8 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 @@ -29,6 +29,14 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie +import com.lagradost.cloudstream3.MainActivity.Companion.MPV +import com.lagradost.cloudstream3.MainActivity.Companion.MPV_COMPONENT +import com.lagradost.cloudstream3.MainActivity.Companion.MPV_PACKAGE +import com.lagradost.cloudstream3.MainActivity.Companion.VLC +import com.lagradost.cloudstream3.MainActivity.Companion.VLC_COMPONENT +import com.lagradost.cloudstream3.MainActivity.Companion.VLC_PACKAGE +import com.lagradost.cloudstream3.MainActivity.Companion.WEB_VIDEO +import com.lagradost.cloudstream3.MainActivity.Companion.WEB_VIDEO_CAST_PACKAGE import com.lagradost.cloudstream3.metaproviders.SyncRedirector import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.syncproviders.AccountManager @@ -1354,7 +1362,7 @@ class ResultViewModel2 : ViewModel() { private fun launchActivity( activity: Activity?, - resumeApp: ResultResume, + resumeApp: MainActivity.Companion.ResultResume, id: Int? = null, work: suspend (Intent.(Activity) -> Unit) ): Job? { diff --git a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt new file mode 100644 index 00000000..48a709eb --- /dev/null +++ b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt @@ -0,0 +1,11 @@ +package com.lagradost.cloudstream3.utils + +import android.os.Handler +import android.os.Looper + +actual fun runOnMainThreadNative(work: () -> Unit) { + val mainHandler = Handler(Looper.getMainLooper()) + mainHandler.post { + work() + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt new file mode 100644 index 00000000..6502cc83 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt @@ -0,0 +1,35 @@ +package com.lagradost.cloudstream3 + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.lagradost.nicehttp.Requests +import com.lagradost.nicehttp.ResponseParser +import kotlin.reflect.KClass + +// Short name for requests client to make it nicer to use + +var app = Requests(responseParser = object : ResponseParser { + val mapper: ObjectMapper = jacksonObjectMapper().configure( + DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, + false + ) + + override fun parse(text: String, kClass: KClass): T { + return mapper.readValue(text, kClass.java) + } + + override fun parseSafe(text: String, kClass: KClass): T? { + return try { + mapper.readValue(text, kClass.java) + } catch (e: Exception) { + null + } + } + + override fun writeValueAsString(obj: Any): String { + return mapper.writeValueAsString(obj) + } +}).apply { + defaultHeaders = mapOf("user-agent" to USER_AGENT) +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt index 87ee4815..160ff098 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt @@ -1,3 +1,6 @@ package com.lagradost.cloudstream3 +const val USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" + class ErrorLoadingException(message: String? = null) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt similarity index 90% rename from app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt index c3b244c2..f87ddc6a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt @@ -1,12 +1,11 @@ package com.lagradost.cloudstream3.utils -import android.os.Handler -import android.os.Looper import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* import java.util.Collections.synchronizedList +expect fun runOnMainThreadNative(work: (() -> Unit)) object Coroutines { fun T.main(work: suspend ((T) -> Unit)): Job { val value = this @@ -50,10 +49,7 @@ object Coroutines { } fun runOnMainThread(work: (() -> Unit)) { - val mainHandler = Handler(Looper.getMainLooper()) - mainHandler.post { - work() - } + runOnMainThreadNative(work) } /** diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsHunter.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/utils/JsHunter.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt diff --git a/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt new file mode 100644 index 00000000..0a9667cb --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt @@ -0,0 +1,5 @@ +package com.lagradost.cloudstream3.utils + +actual fun runOnMainThreadNative(work: () -> Unit) { + work.invoke() +} \ No newline at end of file From 469a71236b6e78018f3f72c83f0b051bdab189c3 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 18 May 2024 19:15:23 +0300 Subject: [PATCH 481/570] SubDL subtitles provider (#1082) --- .../subtitles/AbstractSubtitleEntities.kt | 6 +- .../syncproviders/AccountManager.kt | 5 +- .../providers/IndexSubtitleApi.kt | 2 +- .../providers/OpenSubtitlesApi.kt | 2 +- .../syncproviders/providers/Subdl.kt | 102 ++++++++++++++++++ .../ui/player/FullScreenPlayer.kt | 3 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 18 +++- .../ui/player/PlayerGeneratorViewModel.kt | 4 + .../ui/result/ResultTrailerPlayer.kt | 3 +- .../ui/result/ResultViewModel2.kt | 3 +- 10 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt index f6424c4c..ed4ccb74 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.subtitles +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.TvType class AbstractSubtitleEntities { @@ -19,8 +20,11 @@ class AbstractSubtitleEntities { data class SubtitleSearch( var query: String = "", - var imdb: Long? = null, var lang: String? = null, + var imdbId: String? = null, + var tmdbId: Int? = null, + var malId: Int? = null, + var aniListId: Int? = null, var epNumber: Int? = null, var seasonNumber: Int? = null, var year: Int? = null 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 bae8a5df..55418890 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey -import com.lagradost.cloudstream3.syncproviders.providers.SubScene import com.lagradost.cloudstream3.syncproviders.providers.* import java.util.concurrent.TimeUnit @@ -16,6 +15,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() val subScene = SubScene() + val subDl = SubDL() val localListApi = LocalList() // used to login via app intent @@ -44,7 +44,8 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { openSubtitlesApi, indexSubtitlesApi, // they got anti scraping measures in place :( addic7ed, - subScene + subScene, + subDl ) const val appString = "cloudstreamapp" diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt index 1adecce9..5ca3f3d5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt @@ -98,7 +98,7 @@ class IndexSubtitleApi : AbstractSubApi { } override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List { - val imdbId = query.imdb ?: 0 + val imdbId = query.imdbId?.replace("tt", "")?.toLong() ?: 0 val lang = query.lang val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString()) val queryText = query.query 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 4030649d..7d0514d1 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 @@ -185,7 +185,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi throwIfCantDoRequest() val fixedLang = fixLanguage(query.lang) - val imdbId = query.imdb ?: 0 + val imdbId = query.imdbId?.replace("tt", "")?.toInt() ?: 0 val queryText = query.query val epNum = query.epNumber ?: 0 val seasonNum = query.seasonNumber ?: 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt new file mode 100644 index 00000000..d25d3f22 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt @@ -0,0 +1,102 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.subtitles.AbstractSubProvider +import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities +import com.lagradost.cloudstream3.subtitles.SubtitleResource + +class SubDL : AbstractSubProvider { + //API Documentation: https://subdl.com/api-doc + val mainUrl = "https://subdl.com/" + val name = "SubDL" + override val idPrefix = "subdl" + companion object { + const val APIKEY = "zRJl5QA-8jNA2i0pE8cxANbEukANp7IM" + const val APIENDPOINT = "https://api.subdl.com/api/v1/subtitles" + const val DOWNLOADENDPOINT = "https://dl.subdl.com" + } + + override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { + + val queryText = query.query + val epNum = query.epNumber ?: 0 + val seasonNum = query.seasonNumber ?: 0 + val yearNum = query.year ?: 0 + + val idQuery = when { + query.imdbId != null -> "&imdb_id=${query.imdbId}" + query.tmdbId != null -> "&tmdb_id=${query.tmdbId}" + else -> null + } + + val epQuery = if (epNum > 0) "&episode_number=$epNum" else "" + val seasonQuery = if (seasonNum > 0) "&season_number=$seasonNum" else "" + val yearQuery = if (yearNum > 0) "&year=$yearNum" else "" + + val searchQueryUrl = when (idQuery) { + //Use imdb/tmdb id to search if its valid + null -> "$APIENDPOINT?api_key=$APIKEY&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery" + else -> "$APIENDPOINT?api_key=$APIKEY$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery" + } + + val req = app.get( + url = searchQueryUrl, + headers = mapOf( + "Accept" to "application/json" + ) + ) + + return req.parsedSafe()?.subtitles?.map { subtitle -> + val name = subtitle.releaseName + val lang = subtitle.lang.replaceFirstChar { it.uppercase() } + val resEpNum = subtitle.episode ?: query.epNumber + val resSeasonNum = subtitle.season ?: query.seasonNumber + val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie + + AbstractSubtitleEntities.SubtitleEntity( + idPrefix = this.idPrefix, + name = name, + lang = lang, + data = "${DOWNLOADENDPOINT}${subtitle.url}", + type = type, + source = this.name, + epNumber = resEpNum, + seasonNumber = resSeasonNum, + ) + } + } + + override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { + this.addZipUrl(data.data) { name, _ -> + name + } + } + + data class ApiResponse( + @JsonProperty("status") val status: Boolean? = null, + @JsonProperty("results") val results: List? = null, + @JsonProperty("subtitles") val subtitles: List? = null, + ) + + data class Result( + @JsonProperty("sd_id") val sdId: Int? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("name") val name: String? = null, + @JsonProperty("imdb_id") val imdbId: String? = null, + @JsonProperty("tmdb_id") val tmdbId: Long? = null, + @JsonProperty("first_air_date") val firstAirDate: String? = null, + @JsonProperty("year") val year: Int? = null, + ) + + data class Subtitle( + @JsonProperty("release_name") val releaseName: String, + @JsonProperty("name") val name: String, + @JsonProperty("lang") val lang: String, + @JsonProperty("author") val author: String? = null, + @JsonProperty("url") val url: String? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("episode") val episode: Int? = null, + ) +} \ 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 c357ce9c..aa25157b 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 @@ -32,6 +32,7 @@ import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.screenWidth +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding @@ -177,7 +178,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { open fun openOnlineSubPicker( context: Context, - imdbId: Long?, + loadResponse: LoadResponse?, dismissCallback: (() -> Unit) ) { throw NotImplementedError() 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 7ff56886..c77f9404 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 @@ -25,6 +25,10 @@ import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.getTMDbId import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding @@ -39,7 +43,6 @@ import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSub import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog import com.lagradost.cloudstream3.ui.result.* -import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout @@ -258,6 +261,7 @@ class GeneratorPlayer : FullScreenPlayer() { var episode: Int? = null, var season: Int? = null, var name: String? = null, + var imdbId: String? = null, ) private fun getMetaData(): TempMetaData { @@ -284,7 +288,7 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun openOnlineSubPicker( - context: Context, imdbId: Long?, dismissCallback: (() -> Unit) + context: Context, loadResponse: LoadResponse?, dismissCallback: (() -> Unit) ) { val providers = subsProviders val isSingleProvider = subsProviders.size == 1 @@ -377,6 +381,7 @@ class GeneratorPlayer : FullScreenPlayer() { } val currentTempMeta = getMetaData() + // bruh idk why it is not correct val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent)) binding.searchLoadingBar.progressTintList = color @@ -424,7 +429,10 @@ class GeneratorPlayer : FullScreenPlayer() { val search = AbstractSubtitleEntities.SubtitleSearch( query = query ?: return@ioSafe, - imdb = imdbId, + imdbId = loadResponse?.getImdbId(), + tmdbId = loadResponse?.getTMDbId()?.toInt(), + malId = loadResponse?.getMalId()?.toInt(), + aniListId = loadResponse?.getAniListId()?.toInt(), epNumber = currentTempMeta.episode, seasonNumber = currentTempMeta.season, lang = currentLanguageTwoLetters.ifBlank { null }, @@ -633,6 +641,8 @@ class GeneratorPlayer : FullScreenPlayer() { } if (subsProvidersIsActive) { + val currentLoadResponse = viewModel.getLoadResponse() + val loadFromOpenSubsFooter: TextView = layoutInflater.inflate( R.layout.sort_bottom_footer_add_choice, null ) as TextView @@ -643,7 +653,7 @@ class GeneratorPlayer : FullScreenPlayer() { loadFromOpenSubsFooter.setOnClickListener { shouldDismiss = false sourceDialog.dismissSafe(activity) - openOnlineSubPicker(it.context, null) { + openOnlineSubPicker(it.context, currentLoadResponse) { dismiss() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 0d98f205..ee44567f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError @@ -111,6 +112,9 @@ class PlayerGeneratorViewModel : ViewModel() { } } } + fun getLoadResponse(): LoadResponse? { + return normalSafeApiCall { (generator as? RepoLinkGenerator?)?.page } + } fun getMeta(): Any? { return normalSafeApiCall { generator?.getCurrent() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index ef3db0b4..135dc530 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -12,6 +12,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.screenWidth +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.PlayerEventSource @@ -110,7 +111,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { override fun openOnlineSubPicker( context: Context, - imdbId: Long?, + loadResponse: LoadResponse?, dismissCallback: () -> Unit ) { } 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 0af01ca8..e1a52074 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 @@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie import com.lagradost.cloudstream3.MainActivity.Companion.MPV @@ -2417,7 +2418,7 @@ class ResultViewModel2 : ViewModel() { null, loadResponse.type, mainId, - null + null, ) ) } From db2bf5e7be3f952e440e02989a0c0878c4bc4b15 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 19 May 2024 18:43:46 +0800 Subject: [PATCH 482/570] Remove subscene (#1096) subscene.com just shows a "Subscene is closed" message now. --- .../syncproviders/AccountManager.kt | 2 - .../syncproviders/providers/SubScene.kt | 118 ------------------ 2 files changed, 120 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt 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 55418890..e96499f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -14,7 +14,6 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val simklApi = SimklApi(0) val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() - val subScene = SubScene() val subDl = SubDL() val localListApi = LocalList() @@ -44,7 +43,6 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { openSubtitlesApi, indexSubtitlesApi, // they got anti scraping measures in place :( addic7ed, - subScene, subDl ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt deleted file mode 100644 index fbe05026..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.lagradost.cloudstream3.syncproviders.providers - -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.mvvm.debugPrint -import com.lagradost.cloudstream3.subtitles.AbstractSubProvider -import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities -import com.lagradost.cloudstream3.subtitles.SubtitleResource -import com.lagradost.cloudstream3.syncproviders.providers.IndexSubtitleApi.Companion.getOrdinal -import com.lagradost.cloudstream3.utils.SubtitleHelper - -class SubScene : AbstractSubProvider { - val mainUrl = "https://subscene.com" - val name = "Subscene" - override val idPrefix = "subscene" - - override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { - val seasonName = - query.seasonNumber?.let { number -> - // Need to translate "7" to "Seventh Season" - getOrdinal(number)?.let { words -> " - $words Season" } - } ?: "" - - val fullQuery = query.query + seasonName - - val doc = app.post( - "$mainUrl/subtitles/searchbytitle", - data = mapOf("query" to fullQuery, "l" to "") - ).document - - return doc.select("div.title a").map { element -> - val href = "$mainUrl${element.attr("href")}" - val title = element.text() - - AbstractSubtitleEntities.SubtitleEntity( - idPrefix = idPrefix, - name = title, - source = name, - data = href, - lang = query.lang ?: "en", - epNumber = query.epNumber - ) - }.distinctBy { it.data } - } - - override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { - val resultDoc = app.get(data.data).document - val queryLanguage = SubtitleHelper.fromTwoLettersToLanguage(data.lang) ?: "English" - - val results = resultDoc.select("table tbody tr").mapNotNull { element -> - val anchor = element.select("a") - val href = anchor.attr("href") ?: return@mapNotNull null - val fixedHref = "$mainUrl${href}" - val spans = anchor.select("span") - val language = spans.firstOrNull()?.text() - val title = spans.getOrNull(1)?.text() - val isPositive = anchor.select("span.positive-icon").isNotEmpty() - - TableElement(title, language, fixedHref, isPositive) - }.sortedBy { - it.getScore(queryLanguage, data.epNumber) - } - - debugPrint { "$name found subtitles: ${results.takeLast(3)}" } - // Last = highest score - val selectedResult = results.lastOrNull() ?: return - - val subtitleDocument = app.get(selectedResult.href).document - val subtitleDownloadUrl = - "$mainUrl${subtitleDocument.select("div.download a").attr("href")}" - - this.addZipUrl(subtitleDownloadUrl) { name, _ -> - name - } - } - - /** - * Class to manage the various different subtitle results and rank them. - */ - data class TableElement( - val title: String?, - val language: String?, - val href: String, - val isPositive: Boolean - ) { - private fun matchesLanguage(other: String): Boolean { - return language != null && (language.contains(other, ignoreCase = true) || - other.contains(language, ignoreCase = true)) - } - - /** - * Scores in this order: - * Preferred Language > Episode number > Positive rating > English Language - */ - fun getScore(queryLanguage: String, episodeNum: Int?): Int { - var score = 0 - if (this.matchesLanguage(queryLanguage)) { - score += 8 - } - // Matches Episode 7 using "E07" with any number of leading zeroes - if (episodeNum != null && title != null && title.contains( - Regex( - """E0*${episodeNum}""", - RegexOption.IGNORE_CASE - ) - ) - ) { - score += 4 - } - if (isPositive) { - score += 2 - } - if (this.matchesLanguage("English")) { - score += 1 - } - return score - } - } -} \ No newline at end of file From e697bf75544d0777e21f67c5af3bef8392bbd35b Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 21 May 2024 23:06:28 +0300 Subject: [PATCH 483/570] Next Airing episode support in Trakt meta provider (#1072) --- .../cloudstream3/metaproviders/TraktProvider.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 37c6be1b..07c9f316 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -4,6 +4,7 @@ import android.net.Uri import com.lagradost.cloudstream3.* import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer @@ -166,6 +167,7 @@ open class TraktProvider : MainAPI() { val episodes = mutableListOf() val seasons = parseJson>(resSeasons) val seasonsNames = mutableListOf() + var nextAir: NextAiring? = null seasons.forEach { season -> @@ -215,6 +217,13 @@ open class TraktProvider : MainAPI() { description = episode.overview, ).apply { this.addDate(episode.firstAired) + if (nextAir == null && this.date != null && this.date!! > unixTimeMS) { + nextAir = NextAiring( + episode = this.episode!!, + unixTime = this.date!!.div(1000L), + season = if (this.season == 1) null else this.season, + ) + } } ) } @@ -240,6 +249,7 @@ open class TraktProvider : MainAPI() { this.actors = actors this.comingSoon = isUpcoming(mediaDetails.released) //posterHeaders + this.nextAiring = nextAir this.seasonNames = seasonsNames this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) this.contentRating = mediaDetails.certification From d0852449a50f548f88cd72785c338f2a5ad45184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Mon, 27 May 2024 16:54:25 +0300 Subject: [PATCH 484/570] Extractor: Added FourPichive (#1103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🕊 --- .../cloudstream3/extractors/HotlingerExtractor.kt | 7 ++++++- .../java/com/lagradost/cloudstream3/utils/ExtractorApi.kt | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt index b557a53e..db721108 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt @@ -20,4 +20,9 @@ class PlayRu : ContentX() { class FourPlayRu : ContentX() { override var name = "FourPlayRu" override var mainUrl = "https://four.playru.net" -} \ No newline at end of file +} + +class FourPichive : ContentX() { + override var name = "FourPichive" + override var mainUrl = "https://four.pichive.online" +} 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 61cdd26a..c6cad804 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -110,6 +110,7 @@ import com.lagradost.cloudstream3.extractors.Hotlinger import com.lagradost.cloudstream3.extractors.FourCX import com.lagradost.cloudstream3.extractors.PlayRu import com.lagradost.cloudstream3.extractors.FourPlayRu +import com.lagradost.cloudstream3.extractors.FourPichive import com.lagradost.cloudstream3.extractors.HDMomPlayer import com.lagradost.cloudstream3.extractors.HDPlayerSystem import com.lagradost.cloudstream3.extractors.VideoSeyred @@ -748,6 +749,7 @@ val extractorApis: MutableList = arrayListOf( FourCX(), PlayRu(), FourPlayRu(), + FourPichive(), HDMomPlayer(), HDPlayerSystem(), VideoSeyred(), From 960f8449b7eda687f70b9cebbbfd76502cffa398 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Mon, 27 May 2024 13:54:51 +0000 Subject: [PATCH 485/570] Update ResultViewModel2.kt (#1102) --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e1a52074..4285feb1 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 @@ -1728,7 +1728,7 @@ class ResultViewModel2 : ViewModel() { txt(R.string.episode_action_cast_mirror) ) { (result, index) -> val host = device?.host ?: return@acquireSingleLink - val link = result.links.firstOrNull() ?: return@acquireSingleLink + val link = result.links.getOrNull(index) ?: return@acquireSingleLink FcastSession(host).use { session -> session.sendMessage( From 5502e478c4f9227e6da2f785b436e9c599d06fdf Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 27 May 2024 19:35:56 +0530 Subject: [PATCH 486/570] chore: update material,kotlin compiler,newpipe extractor,rhino-js,guava,corektx (#1091) --- app/build.gradle.kts | 12 +++++------- .../java/com/lagradost/cloudstream3/MainActivity.kt | 2 +- .../ui/player/DownloadedPlayerActivity.kt | 2 +- build.gradle.kts | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f854865d..61a0634f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -164,7 +164,7 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") // Android Core & Lifecycle - implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.navigation:navigation-ui-ktx:2.7.7") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") @@ -174,7 +174,7 @@ dependencies { // Design & UI implementation("jp.wasabeef:glide-transformations:4.3.0") implementation("androidx.preference:preference-ktx:1.2.1") - implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.12.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") @@ -185,7 +185,7 @@ dependencies { // For KSP -> Official Annotation Processors are Not Yet Supported for KSP ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") - implementation("com.google.guava:guava:32.1.3-android") + implementation("com.google.guava:guava:33.2.0-android") implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") // Media 3 (ExoPlayer) @@ -202,7 +202,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:6dc25f7b97") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding @@ -219,9 +219,7 @@ dependencies { implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview // Extensions & Other Libs - implementation("org.mozilla:rhino:1.7.13") /* run JavaScript - ^ Don't Bump RhinoJS to 1.7.14,`NoClassDefFoundError` Occurs and Trailers won't play (even with Desugaring) - NewPipeExtractor Issue */ + implementation("org.mozilla:rhino:1.7.15") // run JavaScript implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9 diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 1ff0575b..cc2c99de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -652,7 +652,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, } } - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + override fun dispatchKeyEvent(event: KeyEvent): Boolean { val response = CommonActivity.dispatchKeyEvent(this, event) if (response != null) return response diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 1e2ea540..4d8860f8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -17,7 +17,7 @@ import com.lagradost.safefile.SafeFile const val DTAG = "PlayerActivity" class DownloadedPlayerActivity : AppCompatActivity() { - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + override fun dispatchKeyEvent(event: KeyEvent): Boolean { CommonActivity.dispatchKeyEvent(this, event)?.let { return it } diff --git a/build.gradle.kts b/build.gradle.kts index ab1918fe..34f141b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle:8.2.2") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") // Universal build config classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:0.15.1") From dff56026de873d0f35cdd134decd1fa1008c0f5f Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 29 May 2024 23:39:55 +0300 Subject: [PATCH 487/570] SubDL Account login support (#1101) --- .../syncproviders/AccountManager.kt | 11 +- .../syncproviders/providers/Subdl.kt | 167 ++++++++++++++++-- .../ui/settings/SettingsAccount.kt | 2 + .../cloudstream3/utils/BackupUtils.kt | 2 + app/src/main/res/drawable/subdl_logo_big.xml | 10 ++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/settings_account.xml | 4 + 7 files changed, 182 insertions(+), 15 deletions(-) create mode 100644 app/src/main/res/drawable/subdl_logo_big.xml 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 e96499f0..1fd7900f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -14,7 +14,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val simklApi = SimklApi(0) val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() - val subDl = SubDL() + val subDlApi = SubDlApi(0) val localListApi = LocalList() // used to login via app intent @@ -26,7 +26,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { // this needs init with context and can be accessed in settings val accountManagers get() = listOf( - malApi, aniListApi, openSubtitlesApi, simklApi //nginxApi + malApi, aniListApi, openSubtitlesApi, subDlApi, simklApi //nginxApi ) // used for active syncing @@ -36,14 +36,17 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { ) val inAppAuths - get() = listOf(openSubtitlesApi)//, nginxApi) + get() = listOf( + openSubtitlesApi, + subDlApi + )//, nginxApi) val subtitleProviders get() = listOf( openSubtitlesApi, indexSubtitlesApi, // they got anti scraping measures in place :( addic7ed, - subDl + subDlApi ) const val appString = "cloudstreamapp" diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt index d25d3f22..29544e65 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt @@ -1,21 +1,80 @@ package com.lagradost.cloudstream3.syncproviders.providers import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.subtitles.AbstractSubProvider +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.subtitles.AbstractSubApi import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.SubtitleResource +import com.lagradost.cloudstream3.syncproviders.AuthAPI.LoginInfo +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager -class SubDL : AbstractSubProvider { - //API Documentation: https://subdl.com/api-doc - val mainUrl = "https://subdl.com/" - val name = "SubDL" +class SubDlApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi { override val idPrefix = "subdl" + override val name = "SubDL" + override val icon = R.drawable.subdl_logo_big + override val requiresPassword = true + override val requiresEmail = true + override val createAccountUrl = "https://subdl.com/login" + companion object { - const val APIKEY = "zRJl5QA-8jNA2i0pE8cxANbEukANp7IM" - const val APIENDPOINT = "https://api.subdl.com/api/v1/subtitles" + const val APIURL = "https://api.subdl.com" + const val APIENDPOINT = "$APIURL/api/v1/subtitles" const val DOWNLOADENDPOINT = "https://dl.subdl.com" + const val SUBDL_SUBTITLES_USER_KEY: String = "subdl_user" + var currentSession: SubtitleOAuthEntity? = null + } + + override suspend fun initialize() { + currentSession = getAuthKey() + } + + override fun logOut() { + setAuthKey(null) + removeAccountKeys() + currentSession = getAuthKey() + } + override suspend fun login(data: InAppAuthAPI.LoginData): Boolean { + val email = data.email ?: throw ErrorLoadingException("Requires Email") + val password = data.password ?: throw ErrorLoadingException("Requires Password") + switchToNewAccount() + try { + if (initLogin(email, password)) { + registerAccount() + return true + } + } catch (e: Exception) { + logError(e) + switchToOldAccount() + } + switchToOldAccount() + return false + } + + override fun getLatestLoginData(): InAppAuthAPI.LoginData? { + val current = getAuthKey() ?: return null + return InAppAuthAPI.LoginData( + email = current.userEmail, + password = current.pass + ) + } + + override fun loginInfo(): LoginInfo? { + getAuthKey()?.let { user -> + return LoginInfo( + profilePicture = null, + name = user.name ?: user.userEmail, + accountIndex = accountIndex + ) + } + return null } override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { @@ -37,8 +96,8 @@ class SubDL : AbstractSubProvider { val searchQueryUrl = when (idQuery) { //Use imdb/tmdb id to search if its valid - null -> "$APIENDPOINT?api_key=$APIKEY&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery" - else -> "$APIENDPOINT?api_key=$APIKEY$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery" + null -> "$APIENDPOINT?api_key=${currentSession?.apiKey}&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery" + else -> "$APIENDPOINT?api_key=${currentSession?.apiKey}$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery" } val req = app.get( @@ -49,7 +108,7 @@ class SubDL : AbstractSubProvider { ) return req.parsedSafe()?.subtitles?.map { subtitle -> - val name = subtitle.releaseName + val lang = subtitle.lang.replaceFirstChar { it.uppercase() } val resEpNum = subtitle.episode ?: query.epNumber val resSeasonNum = subtitle.season ?: query.seasonNumber @@ -57,13 +116,14 @@ class SubDL : AbstractSubProvider { AbstractSubtitleEntities.SubtitleEntity( idPrefix = this.idPrefix, - name = name, + name = subtitle.releaseName, lang = lang, data = "${DOWNLOADENDPOINT}${subtitle.url}", type = type, source = this.name, epNumber = resEpNum, seasonNumber = resSeasonNum, + isHearingImpaired = subtitle.hearingImpaired ?: false, ) } } @@ -74,6 +134,88 @@ class SubDL : AbstractSubProvider { } } + private suspend fun initLogin(useremail: String, password: String): Boolean { + + val tokenResponse = app.post( + url = "$APIURL/login", + data = mapOf( + "email" to useremail, + "password" to password + ) + ).parsedSafe() + + if (tokenResponse?.token == null) return false + + val apiResponse = app.get( + url = "$APIURL/user/userApi", + headers = mapOf( + "Authorization" to "Bearer ${tokenResponse.token}" + ) + ).parsedSafe() + + if (apiResponse?.ok == false) return false + + setAuthKey( + SubtitleOAuthEntity( + userEmail = useremail, + pass = password, + name = tokenResponse.userData?.username ?: tokenResponse.userData?.name, + accessToken = tokenResponse.token, + apiKey = apiResponse?.apiKey + ) + ) + return true + } + + private fun getAuthKey(): SubtitleOAuthEntity? { + return getKey(accountId, SUBDL_SUBTITLES_USER_KEY) + } + + private fun setAuthKey(data: SubtitleOAuthEntity?) { + if (data == null) removeKey( + accountId, + SUBDL_SUBTITLES_USER_KEY + ) + currentSession = data + setKey(accountId, SUBDL_SUBTITLES_USER_KEY, data) + } + + data class SubtitleOAuthEntity( + @JsonProperty("userEmail") var userEmail: String, + @JsonProperty("pass") var pass: String, + @JsonProperty("name") var name: String? = null, + @JsonProperty("accessToken") var accessToken: String? = null, + @JsonProperty("apiKey") var apiKey: String? = null, + ) + + data class OAuthTokenResponse( + @JsonProperty("token") val token: String? = null, + @JsonProperty("userData") val userData: UserData? = null, + @JsonProperty("status") val status: Boolean? = null, + @JsonProperty("message") val message: String? = null, + ) + + data class UserData( + @JsonProperty("email") val email: String, + @JsonProperty("name") val name: String, + @JsonProperty("country") val country: String, + @JsonProperty("scStepCode") val scStepCode: String, + @JsonProperty("scVerified") val scVerified: Boolean, + @JsonProperty("username") val username: String? = null, + @JsonProperty("scUsername") val scUsername: String, + ) + + data class ApiKeyResponse( + @JsonProperty("ok") val ok: Boolean? = false, + @JsonProperty("api_key") val apiKey: String? = null, + @JsonProperty("usage") val usage: Usage? = null, + ) + + data class Usage( + @JsonProperty("total") val total: Long? = 0, + @JsonProperty("today") val today: Long? = 0, + ) + data class ApiResponse( @JsonProperty("status") val status: Boolean? = null, @JsonProperty("results") val results: List? = null, @@ -96,7 +238,10 @@ class SubDL : AbstractSubProvider { @JsonProperty("lang") val lang: String, @JsonProperty("author") val author: String? = null, @JsonProperty("url") val url: String? = null, + @JsonProperty("subtitlePage") val subtitlePage: String? = null, @JsonProperty("season") val season: Int? = null, @JsonProperty("episode") val episode: Int? = null, + @JsonProperty("language") val language: String? = null, + @JsonProperty("hi") val hearingImpaired: Boolean? = null, ) } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index f0d402da..27233525 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniList import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subDlApi import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API @@ -324,6 +325,7 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome R.string.anilist_key to aniListApi, R.string.simkl_key to simklApi, R.string.opensubtitles_key to openSubtitlesApi, + R.string.subdl_key to subDlApi, ) for ((key, api) in syncApis) { 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 279a0cb5..1d23e503 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -26,6 +26,7 @@ import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_T 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.syncproviders.providers.SubDlApi.Companion.SUBDL_SUBTITLES_USER_KEY import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -64,6 +65,7 @@ object BackupUtils { PLUGINS_KEY_LOCAL, OPEN_SUBTITLES_USER_KEY, + SUBDL_SUBTITLES_USER_KEY, DOWNLOAD_EPISODE_CACHE, diff --git a/app/src/main/res/drawable/subdl_logo_big.xml b/app/src/main/res/drawable/subdl_logo_big.xml new file mode 100644 index 00000000..a6cbb311 --- /dev/null +++ b/app/src/main/res/drawable/subdl_logo_big.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8108623..44171dc5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -471,6 +471,7 @@ simkl_key mal_key opensubtitles_key + subdl_key nginx_key password123 Username diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml index 5cde06c4..d1d18a0f 100644 --- a/app/src/main/res/xml/settings_account.xml +++ b/app/src/main/res/xml/settings_account.xml @@ -17,6 +17,10 @@ android:icon="@drawable/open_subtitles_icon" android:key="@string/opensubtitles_key" /> + + Date: Sat, 1 Jun 2024 19:16:42 +0300 Subject: [PATCH 488/570] feat(TV UI): Account switch focus fix (#1112) --- app/src/main/res/layout/account_managment.xml | 14 +++++---- app/src/main/res/layout/account_single.xml | 29 ++++++++++--------- app/src/main/res/layout/account_switch.xml | 22 +++++++------- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/app/src/main/res/layout/account_managment.xml b/app/src/main/res/layout/account_managment.xml index 389a3406..e7afb382 100644 --- a/app/src/main/res/layout/account_managment.xml +++ b/app/src/main/res/layout/account_managment.xml @@ -62,14 +62,16 @@ + android:id="@+id/account_switch_account" + android:text="@string/switch_account" + style="@style/SettingsItem" + android:focusable="true"/> + android:id="@+id/account_logout" + android:text="@string/logout" + style="@style/SettingsItem" + android:focusable="true"> diff --git a/app/src/main/res/layout/account_single.xml b/app/src/main/res/layout/account_single.xml index cbfb9f18..c4f7fa39 100644 --- a/app/src/main/res/layout/account_single.xml +++ b/app/src/main/res/layout/account_single.xml @@ -1,10 +1,11 @@ + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:foreground="?android:attr/selectableItemBackgroundBorderless" + android:orientation="horizontal" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:focusable="true"> + android:id="@+id/account_profile_picture" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:ignore="ContentDescription" /> + android:foreground="@null" + android:id="@+id/account_name" + tools:text="Account 1" + style="@style/SettingsItem" /> diff --git a/app/src/main/res/layout/account_switch.xml b/app/src/main/res/layout/account_switch.xml index 659ad840..5153f0e3 100644 --- a/app/src/main/res/layout/account_switch.xml +++ b/app/src/main/res/layout/account_switch.xml @@ -7,18 +7,20 @@ android:layout_height="match_parent"> + android:id="@+id/account_list" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + android:background="?attr/primaryBlackBackground" + tools:listitem="@layout/account_single" + android:layout_width="match_parent" + android:layout_rowWeight="1" + android:layout_height="wrap_content" + android:focusable="true"/> + android:id="@+id/account_add" + android:text="@string/add_account" + style="@style/SettingsItem" + android:focusable="true"> From b3e3dadc72e61cd9925f721749578b074026f745 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 1 Jun 2024 19:17:41 +0300 Subject: [PATCH 489/570] Remove IndexSubtitles provider (#1111) --- .../syncproviders/AccountManager.kt | 2 - .../providers/IndexSubtitleApi.kt | 265 ------------------ 2 files changed, 267 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt 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 1fd7900f..a14f8438 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -12,7 +12,6 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val aniListApi = AniListApi(0) val openSubtitlesApi = OpenSubtitlesApi(0) val simklApi = SimklApi(0) - val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() val subDlApi = SubDlApi(0) val localListApi = LocalList() @@ -44,7 +43,6 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val subtitleProviders get() = listOf( openSubtitlesApi, - indexSubtitlesApi, // they got anti scraping measures in place :( addic7ed, subDlApi ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt deleted file mode 100644 index 5ca3f3d5..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt +++ /dev/null @@ -1,265 +0,0 @@ -package com.lagradost.cloudstream3.syncproviders.providers - -import android.util.Log -import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.imdbUrlToIdNullable -import com.lagradost.cloudstream3.subtitles.AbstractSubApi -import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities -import com.lagradost.cloudstream3.utils.SubtitleHelper - -class IndexSubtitleApi : AbstractSubApi { - override val name = "IndexSubtitle" - override val idPrefix = "indexsubtitle" - override val requiresLogin = false - override val icon: Nothing? = null - override val createAccountUrl: Nothing? = null - - override fun loginInfo(): Nothing? = null - - override fun logOut() {} - - - companion object { - const val host = "https://indexsubtitle.com" - const val TAG = "INDEXSUBS" - - fun getOrdinal(num: Int?): String? { - return when (num) { - 1 -> "First" - 2 -> "Second" - 3 -> "Third" - 4 -> "Fourth" - 5 -> "Fifth" - 6 -> "Sixth" - 7 -> "Seventh" - 8 -> "Eighth" - 9 -> "Ninth" - 10 -> "Tenth" - 11 -> "Eleventh" - 12 -> "Twelfth" - 13 -> "Thirteenth" - 14 -> "Fourteenth" - 15 -> "Fifteenth" - 16 -> "Sixteenth" - 17 -> "Seventeenth" - 18 -> "Eighteenth" - 19 -> "Nineteenth" - 20 -> "Twentieth" - 21 -> "Twenty-First" - 22 -> "Twenty-Second" - 23 -> "Twenty-Third" - 24 -> "Twenty-Fourth" - 25 -> "Twenty-Fifth" - 26 -> "Twenty-Sixth" - 27 -> "Twenty-Seventh" - 28 -> "Twenty-Eighth" - 29 -> "Twenty-Ninth" - 30 -> "Thirtieth" - 31 -> "Thirty-First" - 32 -> "Thirty-Second" - 33 -> "Thirty-Third" - 34 -> "Thirty-Fourth" - 35 -> "Thirty-Fifth" - else -> null - } - } - } - - private fun fixUrl(url: String): String { - if (url.startsWith("http")) { - return url - } - if (url.isEmpty()) { - return "" - } - - val startsWithNoHttp = url.startsWith("//") - if (startsWithNoHttp) { - return "https:$url" - } else { - if (url.startsWith('/')) { - return host + url - } - return "$host/$url" - } - } - - private fun isRightEps(text: String, seasonNum: Int?, epNum: Int?): Boolean { - val FILTER_EPS_REGEX = - Regex("(?i)((Chapter\\s?0?${epNum})|((Season)?\\s?0?${seasonNum}?\\s?(Episode)\\s?0?${epNum}[^0-9]))|(?i)((S?0?${seasonNum}?E0?${epNum}[^0-9])|(0?${seasonNum}[a-z]0?${epNum}[^0-9]))") - return text.contains(FILTER_EPS_REGEX) - } - - private fun haveEps(text: String): Boolean { - val HAVE_EPS_REGEX = - Regex("(?i)((Chapter\\s?0?\\d)|((Season)?\\s?0?\\d?\\s?(Episode)\\s?0?\\d))|(?i)((S?0?\\d?E0?\\d)|(0?\\d[a-z]0?\\d))") - return text.contains(HAVE_EPS_REGEX) - } - - override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List { - val imdbId = query.imdbId?.replace("tt", "")?.toLong() ?: 0 - val lang = query.lang - val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString()) - val queryText = query.query - val epNum = query.epNumber ?: 0 - val seasonNum = query.seasonNumber ?: 0 - val yearNum = query.year ?: 0 - - val urlItems = ArrayList() - - fun cleanResources( - results: MutableList, - name: String, - link: String - ) { - results.add( - AbstractSubtitleEntities.SubtitleEntity( - idPrefix = idPrefix, - name = name, - lang = queryLang.toString(), - data = link, - source = this.name, - type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie, - epNumber = epNum, - seasonNumber = seasonNum, - year = yearNum, - ) - ) - } - - val document = app.get("$host/?search=$queryText").document - - document.select("div.my-3.p-3 div.media").map { block -> - if (seasonNum > 0) { - val name = block.select("strong.text-primary, strong.text-info").text().trim() - val season = getOrdinal(seasonNum) - if ((block.selectFirst("a")?.attr("href") - ?.contains( - "$season", - ignoreCase = true - )!! || name.contains( - "$season", - ignoreCase = true - )) && name.contains(queryText, ignoreCase = true) - ) { - block.select("div.media").mapNotNull { - urlItems.add( - fixUrl( - it.selectFirst("a")!!.attr("href") - ) - ) - } - } - } else { - if (block.selectFirst("strong")!!.text().trim() - .matches(Regex("(?i)^$queryText\$")) - ) { - if (block.select("span[title=Release]").isNullOrEmpty()) { - block.select("div.media").mapNotNull { - val urlItem = fixUrl( - it.selectFirst("a")!!.attr("href") - ) - val itemDoc = app.get(urlItem).document - val id = imdbUrlToIdNullable( - itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent() - ?.attr("href") - )?.toLongOrNull() - val year = itemDoc.selectFirst("div.d-flex span.badge.badge-success") - ?.ownText() - ?.trim().toString() - Log.i(TAG, "id => $id \nyear => $year||$yearNum") - if (imdbId > 0) { - if (id == imdbId) { - urlItems.add(urlItem) - } - } else { - if (year.contains("$yearNum")) { - urlItems.add(urlItem) - } - } - } - } else { - if (block.select("span[title=Release]").text().trim() - .contains("$yearNum") - ) { - block.select("div.media").mapNotNull { - urlItems.add( - fixUrl( - it.selectFirst("a")!!.attr("href") - ) - ) - } - } - } - } - } - } - Log.i(TAG, "urlItems => $urlItems") - val results = mutableListOf() - - urlItems.forEach { url -> - val request = app.get(url) - if (request.isSuccessful) { - request.document.select("div.my-3.p-3 div.media").map { block -> - if (block.select("span.d-block span[data-original-title=Language]").text() - .trim() - .contains("$queryLang") - ) { - var name = block.select("strong.text-primary, strong.text-info").text().trim() - val link = fixUrl(block.selectFirst("a")!!.attr("href")) - if (seasonNum > 0) { - when { - isRightEps(name, seasonNum, epNum) -> { - cleanResources(results, name, link) - } - !(haveEps(name)) -> { - name = "$name (S${seasonNum}:E${epNum})" - cleanResources(results, name, link) - } - } - } else { - cleanResources(results, name, link) - } - } - } - } - } - return results - } - - override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String? { - val seasonNum = data.seasonNumber - val epNum = data.epNumber - - val req = app.get(data.data) - - if (req.isSuccessful) { - val document = req.document - val link = if (document.select("div.my-3.p-3 div.media").size == 1) { - fixUrl( - document.selectFirst("div.my-3.p-3 div.media a")!!.attr("href") - ) - } else { - document.select("div.my-3.p-3 div.media").firstNotNullOf { block -> - val name = - block.selectFirst("strong.d-block")?.text()?.trim().toString() - if (seasonNum!! > 0) { - if (isRightEps(name, seasonNum, epNum)) { - fixUrl(block.selectFirst("a")!!.attr("href")) - } else { - null - } - } else { - fixUrl(block.selectFirst("a")!!.attr("href")) - } - } - } - return link - } - - return null - - } - -} \ No newline at end of file From 9bebfe459005021970e25bd5ca38816fa5a66ba4 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:07:54 +0200 Subject: [PATCH 490/570] feature(ui): hide NSFW plugins (#1117) Hide NSFW plugins if Settings / Providers NSFW is disabled --- .../cloudstream3/ui/settings/extensions/PluginsViewModel.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt index 151c8d57..2b026e0d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.amap @@ -181,8 +182,11 @@ class PluginsViewModel : ViewModel() { } private suspend fun updatePluginListPrivate(context: Context, repositoryUrl: String) { + val isAdult = settingsForProvider.enableAdult val plugins = getPlugins(repositoryUrl) - val list = plugins.map { plugin -> + val list = plugins.filter { + return@filter !(it.second.tvTypes?.contains("NSFW") == true && !isAdult) + }.map { plugin -> PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first)) } From 0391a3b89cb3d4266afc1e1a710366b9266f1241 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:09:05 +0200 Subject: [PATCH 491/570] feature(ui): added wikipedia to links (#1119) --- app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/settings_general.xml | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 44171dc5..deee5ad2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -774,4 +774,5 @@ Audio Book Media Reset + CloudStream Wiki \ No newline at end of file diff --git a/app/src/main/res/xml/settings_general.xml b/app/src/main/res/xml/settings_general.xml index cdda6d85..853bbda1 100644 --- a/app/src/main/res/xml/settings_general.xml +++ b/app/src/main/res/xml/settings_general.xml @@ -86,6 +86,14 @@ android:action="android.intent.action.VIEW" android:data="https://discord.gg/5Hus6fM" /> + + + \ No newline at end of file From 358a20eb7786daa661ef60d20caca67d8eec105f Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Thu, 6 Jun 2024 02:48:33 +0530 Subject: [PATCH 492/570] chore: refactor gradlelocalproperties and update gradle plugin (#957) --- app/build.gradle.kts | 2 +- build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 61a0634f..21f22dd1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -69,7 +69,7 @@ android { resValue("bool", "is_prerelease", "false") // Reads local.properties - val localProperties = gradleLocalProperties(rootDir) + val localProperties = gradleLocalProperties(rootDir, providers) buildConfigField( "long", diff --git a/build.gradle.kts b/build.gradle.kts index 34f141b4..ba87b6f4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath("com.android.tools.build:gradle:8.2.2") + classpath("com.android.tools.build:gradle:8.4.0") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") // Universal build config diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fc2d0f86..2968a1b2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Apr 30 17:11:15 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 7eec0eff02b1e7f8bd18a948515adae5fdc13f9e Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:41:06 +0200 Subject: [PATCH 493/570] Revert "chore: refactor gradlelocalproperties and update gradle plugin (#957)" (#1120) This reverts commit 358a20eb7786daa661ef60d20caca67d8eec105f. --- app/build.gradle.kts | 2 +- build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 21f22dd1..61a0634f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -69,7 +69,7 @@ android { resValue("bool", "is_prerelease", "false") // Reads local.properties - val localProperties = gradleLocalProperties(rootDir, providers) + val localProperties = gradleLocalProperties(rootDir) buildConfigField( "long", diff --git a/build.gradle.kts b/build.gradle.kts index ba87b6f4..34f141b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath("com.android.tools.build:gradle:8.4.0") + classpath("com.android.tools.build:gradle:8.2.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") // Universal build config diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2968a1b2..fc2d0f86 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Apr 30 17:11:15 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From f775c1725d30d6f105dc114c8e6ecbfc6d2d56d9 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 8 Jun 2024 22:07:33 +0300 Subject: [PATCH 494/570] feat(TV UI): Subtitles Filter button focus fix (#1125) --- app/src/main/res/layout/dialog_online_subtitles.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/dialog_online_subtitles.xml b/app/src/main/res/layout/dialog_online_subtitles.xml index d480bd34..e0eac5e0 100644 --- a/app/src/main/res/layout/dialog_online_subtitles.xml +++ b/app/src/main/res/layout/dialog_online_subtitles.xml @@ -107,6 +107,7 @@ android:layout_margin="10dp" android:background="?selectableItemBackgroundBorderless" android:contentDescription="@string/change_providers_img_des" + android:focusable="true" android:nextFocusLeft="@id/year_btt" android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" From 607a4510b6d941293fd29115818d79deb9b03681 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 8 Jun 2024 22:08:35 +0300 Subject: [PATCH 495/570] feat(Extensions): Trakt season names remove (#1124) --- .../cloudstream3/metaproviders/TraktProvider.kt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 07c9f316..8d149888 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -166,18 +166,10 @@ open class TraktProvider : MainAPI() { val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") val episodes = mutableListOf() val seasons = parseJson>(resSeasons) - val seasonsNames = mutableListOf() var nextAir: NextAiring? = null seasons.forEach { season -> - seasonsNames.add( - SeasonData( - season.number!!, - season.title - ) - ) - season.episodes?.map { episode -> val linkData = LinkData( @@ -250,7 +242,6 @@ open class TraktProvider : MainAPI() { this.comingSoon = isUpcoming(mediaDetails.released) //posterHeaders this.nextAiring = nextAir - this.seasonNames = seasonsNames this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) this.contentRating = mediaDetails.certification addTrailer(mediaDetails.trailer) From 3345326cb2987b98b87db0d93e291aa0d0b27b7e Mon Sep 17 00:00:00 2001 From: RowdyRushya <66415100+rushi-chavan@users.noreply.github.com> Date: Sat, 8 Jun 2024 12:19:29 -0700 Subject: [PATCH 496/570] Extractor: VidSrcTo: better handling of runtime errors (#1121) --- .../cloudstream3/extractors/VidSrcTo.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt index b9065688..2655670d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import java.net.URLDecoder @@ -26,12 +27,16 @@ class VidSrcTo : ExtractorApi() { val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return if (res.status != 200) return res.result?.amap { source -> - val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap - val finalUrl = DecryptUrl(embedRes.result.encUrl) - if(finalUrl.equals(embedRes.result.encUrl)) return@amap - when (source.title) { - "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) - "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + try { + val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap + val finalUrl = DecryptUrl(embedRes.result.encUrl) + if(finalUrl.equals(embedRes.result.encUrl)) return@amap + when (source.title) { + "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + } + } catch (e: Exception) { + logError(e) } } } From 4c95610238d671cd8e11c3e85786a53e0db003c7 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sun, 9 Jun 2024 17:38:08 +0300 Subject: [PATCH 497/570] feat(UI): Hide Platform's not related settings (#1128) --- .../ui/settings/SettingsAccount.kt | 6 ++--- .../ui/settings/SettingsFragment.kt | 25 +++++++++++++++++++ .../ui/settings/SettingsGeneral.kt | 7 +++--- .../ui/settings/SettingsPlayer.kt | 19 +++++++++++++- app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/settings_player.xml | 6 +++-- 6 files changed, 55 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 27233525..3ec47648 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -36,6 +36,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -298,10 +299,7 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome hideKeyboard() setPreferencesFromResource(R.xml.settings_account, rootKey) - // hide preference on tvs and emulators - getPref(R.string.biometric_key)?.isEnabled = isLayout(PHONE) - - getPref(R.string.biometric_key)?.setOnPreferenceClickListener { + getPref(R.string.biometric_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener { val ctx = context ?: return@setOnPreferenceClickListener false if (deviceHasPasswordPinLock(ctx)) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 8ac17928..6ba93c0f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -26,6 +26,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper @@ -53,6 +54,30 @@ class SettingsFragment : Fragment() { } } + /** + * Hide many Preferences on selected layouts. + **/ + fun PreferenceFragmentCompat?.hidePrefs(ids: List, layoutFlags: Int) { + if (this == null) return + + try { + ids.forEach { + getPref(it)?.isVisible = !isLayout(layoutFlags) + } + } catch (e: Exception) { + logError(e) + } + } + + /** + * Hide the Preference on selected layouts. + **/ + fun Preference?.hideOn(layoutFlags: Int): Preference? { + if (this == null) return null + this.isVisible = !isLayout(layoutFlags) + return this + } + /** * On TV you cannot properly scroll to the bottom of settings, this fixes that. * */ 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 ff891c43..22a7e098 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 @@ -27,10 +27,13 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.ui.EasterEggMonke +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -208,9 +211,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - // disable preference on tvs and emulators - getPref(R.string.battery_optimisation_key)?.isEnabled = isLayout(PHONE) - getPref(R.string.battery_optimisation_key)?.setOnPreferenceClickListener { + getPref(R.string.battery_optimisation_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener { val ctx = context ?: return@setOnPreferenceClickListener false if (isAppRestricted(ctx)) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 3d0bcb1f..20279cd1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -7,8 +7,14 @@ import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hidePrefs import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -31,6 +37,18 @@ class SettingsPlayer : PreferenceFragmentCompat() { setPreferencesFromResource(R.xml.settings_player, rootKey) val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) + //Hide specific prefs on TV/EMULATOR + hidePrefs( + listOf( + R.string.pref_category_gestures_key, + R.string.rotate_video_key, + R.string.auto_rotate_video_key + ), + TV or EMULATOR + ) + + getPref(R.string.pref_category_android_tv_key)?.hideOn(PHONE) + getPref(R.string.video_buffer_length_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.video_buffer_length_names) val prefValues = resources.getIntArray(R.array.video_buffer_length_values) @@ -227,6 +245,5 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } } - } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index deee5ad2..fad44ad4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -438,7 +438,9 @@ Actions Cache Android TV + pref_category_android_tv_key Gestures + pref_category_gestures_key Player features Subtitles Layout diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index 82505511..5d5b11d0 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -101,7 +101,8 @@ + android:title="@string/pref_category_gestures" + app:key="@string/pref_category_gestures_key"> + android:title="@string/pref_category_android_tv" + android:key="@string/pref_category_android_tv_key" > Date: Sat, 15 Jun 2024 21:47:30 +0000 Subject: [PATCH 498/570] goodstream (#1133) --- .../extractors/GoodstreamExtractor.kt | 37 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 2 + 2 files changed, 39 insertions(+) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt new file mode 100644 index 00000000..9f6ba611 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt @@ -0,0 +1,37 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities + +class GoodstreamExtractor : ExtractorApi() { + override var name = "Goodstream" + override val mainUrl = "https://goodstream.uno" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + app.get(url).document.select("script").map { script -> + if (script.data().contains(Regex("file|player"))) { + val urlRegex = Regex("file: \"(https:\\/\\/[a-z0-9.\\/-_?=&]+)\",") + urlRegex.find(script.data())?.groupValues?.get(1).let { link -> + callback.invoke( + ExtractorLink( + name, + name, + link!!, + mainUrl, + Qualities.Unknown.value, + ) + ) + } + } + } + } +} \ No newline at end of file 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 c6cad804..5d696d33 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -67,6 +67,7 @@ import com.lagradost.cloudstream3.extractors.Gdriveplayerorg import com.lagradost.cloudstream3.extractors.Gdriveplayerus import com.lagradost.cloudstream3.extractors.Gofile import com.lagradost.cloudstream3.extractors.GuardareStream +import com.lagradost.cloudstream3.extractors.GoodstreamExtractor import com.lagradost.cloudstream3.extractors.Guccihide import com.lagradost.cloudstream3.extractors.Hxfile import com.lagradost.cloudstream3.extractors.JWPlayer @@ -879,6 +880,7 @@ val extractorApis: MutableList = arrayListOf( Gdriveplayerorg(), Gdriveplayerus(), Gdriveplayerco(), + GoodstreamExtractor(), Gdriveplayer(), DatabaseGdrive(), DatabaseGdrive2(), From 30d223cfe3c65b8f104245d58056087e7913adbd Mon Sep 17 00:00:00 2001 From: KingLucius Date: Mon, 17 Jun 2024 04:01:14 +0300 Subject: [PATCH 499/570] feat(UI): Reorganize Settings (#1137) - Accounts Section & Remove "account" from title. - Security Section for Biometric that is hidden on TV. - Move "send logs" to "Action" section. --- .../ui/settings/SettingsAccount.kt | 6 +- app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/settings_account.xml | 66 +++++++++++-------- app/src/main/res/xml/settings_updates.xml | 14 ++-- 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 3ec47648..a8358d0d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -299,6 +299,9 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome hideKeyboard() setPreferencesFromResource(R.xml.settings_account, rootKey) + //Hides the security category on TV as it's only Biometric for now + getPref(R.string.pref_category_security_key)?.hideOn(TV or EMULATOR) + getPref(R.string.biometric_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener { val ctx = context ?: return@setOnPreferenceClickListener false @@ -328,8 +331,7 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome for ((key, api) in syncApis) { getPref(key)?.apply { - title = - getString(R.string.login_format).format(api.name, getString(R.string.account)) + title = api.name setOnPreferenceClickListener { val info = api.loginInfo() if (info != null) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fad44ad4..d9317ccd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -441,6 +441,9 @@ pref_category_android_tv_key Gestures pref_category_gestures_key + Security + pref_category_security_key + Accounts Player features Subtitles Layout diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml index d1d18a0f..d165cd87 100644 --- a/app/src/main/res/xml/settings_account.xml +++ b/app/src/main/res/xml/settings_account.xml @@ -1,37 +1,49 @@ - + - + - + - + - + - + - + - + + + + + + + + + \ 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 e3b36648..102f8ee4 100644 --- a/app/src/main/res/xml/settings_updates.xml +++ b/app/src/main/res/xml/settings_updates.xml @@ -1,13 +1,6 @@ - @@ -80,5 +73,12 @@ android:icon="@drawable/ic_baseline_construction_24" android:title="@string/redo_setup_process" app:key="@string/redo_setup_key" /> + From 7a0cd07dc19f3d1523292ef0569338b326cb1784 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 18 Jun 2024 06:02:32 +0300 Subject: [PATCH 500/570] feat(TV UI): Press Right to focus save on Logcat (#1136) --- app/src/main/res/layout/logcat.xml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/layout/logcat.xml b/app/src/main/res/layout/logcat.xml index caa8c5cb..5cbb3f53 100644 --- a/app/src/main/res/layout/logcat.xml +++ b/app/src/main/res/layout/logcat.xml @@ -6,20 +6,20 @@ android:layout_height="match_parent"> + android:layout_marginBottom="60dp" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:nextFocusRight="@id/save_btt"> + android:id="@+id/text1" + android:padding="15dp" + android:textSize="15sp" + android:textColor="?attr/textColor" + android:layout_width="match_parent" + android:layout_rowWeight="1" + tools:text="Test" + android:layout_height="wrap_content"/> Date: Wed, 19 Jun 2024 00:24:35 +0300 Subject: [PATCH 501/570] feat(Extensions): Consider time zone in Trakt durations (#1140) --- .../metaproviders/TraktProvider.kt | 57 ++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 8d149888..736e05f2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -1,18 +1,39 @@ package com.lagradost.cloudstream3.metaproviders import android.net.Uri -import com.lagradost.cloudstream3.* import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.Actor +import com.lagradost.cloudstream3.ActorData +import com.lagradost.cloudstream3.Episode +import com.lagradost.cloudstream3.HomePageResponse +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MainPageRequest +import com.lagradost.cloudstream3.NextAiring +import com.lagradost.cloudstream3.ProviderType +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.ShowStatus +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.addDate +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64Decode +import com.lagradost.cloudstream3.mainPageOf import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.newHomePageResponse +import com.lagradost.cloudstream3.newMovieLoadResponse +import com.lagradost.cloudstream3.newMovieSearchResponse +import com.lagradost.cloudstream3.newTvSeriesLoadResponse +import com.lagradost.cloudstream3.newTvSeriesSearchResponse import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson -import java.util.Locale import java.text.SimpleDateFormat +import java.util.Locale import kotlin.math.roundToInt open class TraktProvider : MainAPI() { @@ -25,7 +46,8 @@ open class TraktProvider : MainAPI() { TvType.Anime, ) - private val traktClientId = base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==") + private val traktClientId = + base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==") private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2") override val mainPage = mainPageOf( @@ -77,7 +99,8 @@ open class TraktProvider : MainAPI() { } override suspend fun search(query: String): List? { - val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query") + val apiResponse = + getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query") val results = parseJson>(apiResponse).map { element -> element.toSearchResponse() @@ -85,6 +108,7 @@ open class TraktProvider : MainAPI() { return results } + override suspend fun load(url: String): LoadResponse { val data = parseJson(url) @@ -94,7 +118,8 @@ open class TraktProvider : MainAPI() { val posterUrl = mediaDetails?.images?.poster?.firstOrNull() val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull() - val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full") + val resActor = + getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full") val actors = parseJson(resActor).cast?.map { ActorData( @@ -106,12 +131,15 @@ open class TraktProvider : MainAPI() { ) } - val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20") + val resRelated = + getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20") val relatedMedia = parseJson>(resRelated).map { it.toSearchResponse() } - val isCartoon = mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true - val isAnime = isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja") + val isCartoon = + mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true + val isAnime = + isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja") val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko") val isBollywood = mediaDetails?.country == "in" @@ -163,10 +191,11 @@ open class TraktProvider : MainAPI() { } } else { - val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") + val resSeasons = + getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") val episodes = mutableListOf() val seasons = parseJson>(resSeasons) - var nextAir: NextAiring? = null + var nextAir: NextAiring? = null seasons.forEach { season -> @@ -208,7 +237,7 @@ open class TraktProvider : MainAPI() { rating = episode.rating?.times(10)?.roundToInt(), description = episode.overview, ).apply { - this.addDate(episode.firstAired) + this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") if (nextAir == null && this.date != null && this.date!! > unixTimeMS) { nextAir = NextAiring( episode = this.episode!!, @@ -251,7 +280,7 @@ open class TraktProvider : MainAPI() { } } - private suspend fun getApi(url: String) : String { + private suspend fun getApi(url: String): String { return app.get( url = url, headers = mapOf( @@ -286,14 +315,14 @@ open class TraktProvider : MainAPI() { return "https://$url" } - private fun getWidthImageUrl(path: String?, width: String) : String? { + private fun getWidthImageUrl(path: String?, width: String): String? { if (path == null) return null if (!path.contains("image.tmdb.org")) return fixPath(path) val fileName = Uri.parse(path).lastPathSegment ?: return null return "https://image.tmdb.org/t/p/${width}/${fileName}" } - private fun getOriginalWidthImageUrl(path: String?) : String? { + private fun getOriginalWidthImageUrl(path: String?): String? { if (path == null) return null if (!path.contains("image.tmdb.org")) return fixPath(path) return getWidthImageUrl(path, "original") From b702b7b1ecfc254dd9b3f8a408a8092452c0cf7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Misael=20Jim=C3=A9nez?= Date: Wed, 19 Jun 2024 07:40:23 -0600 Subject: [PATCH 502/570] Fix DoodExtractor. (#1134) Fix StreamWishExtractor --- .../cloudstream3/extractors/DoodExtractor.kt | 19 +++++- .../extractors/StreamWishExtractor.kt | 60 +++++++++++++------ .../cloudstream3/utils/ExtractorApi.kt | 15 +++++ 3 files changed, 72 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt index 8dcfb859..370dcaca 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt @@ -7,6 +7,18 @@ import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.getQualityFromName import kotlinx.coroutines.delay +class D0000d : DoodLaExtractor() { + override var mainUrl = "https://d0000d.com" +} + +class D000dCom : DoodLaExtractor() { + override var mainUrl = "https://d000d.com" +} + +class DoodstreamCom : DoodLaExtractor() { + override var mainUrl = "https://doodstream.com" +} + class Dooood : DoodLaExtractor() { override var mainUrl = "https://dooood.com" } @@ -56,9 +68,10 @@ open class DoodLaExtractor : ExtractorApi() { } override suspend fun getUrl(url: String, referer: String?): List? { - val response0 = app.get(url).text // html of DoodStream page to look for /pass_md5/... - val md5 =mainUrl+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/... - val trueUrl = app.get(md5, referer = url).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random) + val newUrl= url.replace(mainUrl, "https://d0000d.com") + val response0 = app.get(newUrl).text // html of DoodStream page to look for /pass_md5/... + val md5 ="https://d0000d.com"+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/... + val trueUrl = app.get(md5, referer = newUrl).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random) val quality = Regex("\\d{3,4}p").find(response0.substringAfter("").substringBefore(""))?.groupValues?.get(0) return listOf( ExtractorLink( diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt index 77d98e49..551d1ef6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt @@ -1,34 +1,56 @@ package com.lagradost.cloudstream3.extractors +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.getQualityFromName + +class WishembedPro : StreamWishExtractor() { + override val mainUrl = "https://wishembed.pro" +} +class CdnwishCom : StreamWishExtractor() { + override val mainUrl = "https://cdnwish.com" +} +class FlaswishCom : StreamWishExtractor() { + override val mainUrl = "https://flaswish.com" +} +class SfastwishCom : StreamWishExtractor() { + override val mainUrl = "https://sfastwish.com" +} open class StreamWishExtractor : ExtractorApi() { override var name = "StreamWish" - override var mainUrl = "https://streamwish.to" + override val mainUrl = "https://streamwish.to" override val requiresReferer = false - override suspend fun getUrl(url: String, referer: String?): List? { - val response = app.get( - url, referer = referer ?: "$mainUrl/", interceptor = WebViewResolver( - Regex("""master\.m3u8""") - ) - ) - val sources = mutableListOf() - if (response.url.contains("m3u8")) - sources.add( + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val doc = app.get( + url, + referer = referer, + allowRedirects = false + ).document + var script = doc.select("script").find { + it.html().contains("jwplayer(\"vplayer\").setup(") + } + var scriptContent = script?.html() + val extractedurl = Regex("""sources: \[\{file:"(.*?)"""").find(scriptContent ?: "")?.groupValues?.get(1) + if (!extractedurl.isNullOrBlank()) { + callback( ExtractorLink( - source = name, - name = name, - url = response.url, - referer = referer ?: "$mainUrl/", - quality = Qualities.Unknown.value, - isM3u8 = true + this.name, + this.name, + extractedurl, + referer ?: "$mainUrl/", + getQualityFromName(""), + extractedurl.contains("m3u8") ) ) - return sources + } } } \ No newline at end of file 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 5d696d33..1302453a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -17,6 +17,7 @@ import com.lagradost.cloudstream3.extractors.BullStream import com.lagradost.cloudstream3.extractors.ByteShare import com.lagradost.cloudstream3.extractors.Cda import com.lagradost.cloudstream3.extractors.Cdnplayer +import com.lagradost.cloudstream3.extractors.CdnwishCom import com.lagradost.cloudstream3.extractors.Chillx import com.lagradost.cloudstream3.extractors.CineGrabber import com.lagradost.cloudstream3.extractors.Cinestart @@ -106,6 +107,9 @@ import com.lagradost.cloudstream3.extractors.Odnoklassniki import com.lagradost.cloudstream3.extractors.TauVideo import com.lagradost.cloudstream3.extractors.SibNet import com.lagradost.cloudstream3.extractors.ContentX +import com.lagradost.cloudstream3.extractors.D0000d +import com.lagradost.cloudstream3.extractors.D000dCom +import com.lagradost.cloudstream3.extractors.DoodstreamCom import com.lagradost.cloudstream3.extractors.EmturbovidExtractor import com.lagradost.cloudstream3.extractors.Hotlinger import com.lagradost.cloudstream3.extractors.FourCX @@ -227,7 +231,10 @@ import com.lagradost.cloudstream3.extractors.Zplayer import com.lagradost.cloudstream3.extractors.ZplayerV2 import com.lagradost.cloudstream3.extractors.Ztreamhub import com.lagradost.cloudstream3.extractors.EPlayExtractor +import com.lagradost.cloudstream3.extractors.FlaswishCom +import com.lagradost.cloudstream3.extractors.SfastwishCom import com.lagradost.cloudstream3.extractors.Vtbe +import com.lagradost.cloudstream3.extractors.WishembedPro import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay @@ -777,6 +784,9 @@ val extractorApis: MutableList = arrayListOf( DoodSoExtractor(), DoodLaExtractor(), Dooood(), + D0000d(), + D000dCom(), + DoodstreamCom(), DoodWsExtractor(), DoodShExtractor(), DoodWatchExtractor(), @@ -854,6 +864,7 @@ val extractorApis: MutableList = arrayListOf( Guccihide(), FileMoon(), FileMoonSx(), + Vido(), Linkbox(), Acefile(), @@ -909,6 +920,10 @@ val extractorApis: MutableList = arrayListOf( Megacloud(), VidhideExtractor(), StreamWishExtractor(), + WishembedPro(), + CdnwishCom(), + FlaswishCom(), + SfastwishCom(), EmturbovidExtractor(), Vtbe(), EPlayExtractor(), From afa178a63a7173316cc04fbbd3fb989f77a06515 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 19 Jun 2024 17:06:08 +0300 Subject: [PATCH 503/570] feat(TV UI): Accounts PIN login support (#1123) --- app/build.gradle.kts | 1 + .../cloudstream3/syncproviders/OAuth2API.kt | 16 ++ .../syncproviders/providers/AniListApi.kt | 1 + .../syncproviders/providers/DropboxApi.kt | 1 + .../syncproviders/providers/LocalList.kt | 1 + .../syncproviders/providers/MALApi.kt | 1 + .../syncproviders/providers/SimklApi.kt | 55 +++++++ .../cloudstream3/ui/result/UiText.kt | 13 ++ .../ui/settings/SettingsAccount.kt | 141 +++++++++++++++--- app/src/main/res/drawable/cloud_2_solid.xml | 8 + app/src/main/res/drawable/example_qr.png | Bin 0 -> 46354 bytes app/src/main/res/layout/device_auth.xml | 59 ++++++++ app/src/main/res/values/strings.xml | 6 + 13 files changed, 286 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable/cloud_2_solid.xml create mode 100644 app/src/main/res/drawable/example_qr.png create mode 100644 app/src/main/res/layout/device_auth.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 61a0634f..fc2e9131 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -217,6 +217,7 @@ dependencies { implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview + implementation("io.github.g0dkar:qrcode-kotlin:4.1.1") // QR code for PIN Auth on TV // Extensions & Other Libs implementation("org.mozilla:rhino:1.7.15") // run JavaScript diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt index ef74edfc..3d0bb940 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt @@ -5,7 +5,23 @@ import androidx.fragment.app.FragmentActivity interface OAuth2API : AuthAPI { val key: String val redirectUrl: String + val supportDeviceAuth: Boolean suspend fun handleRedirect(url: String) : Boolean fun authenticate(activity: FragmentActivity?) + suspend fun getDevicePin() : PinAuthData? { + return null + } + + suspend fun handleDeviceAuth(pinAuthData: PinAuthData) : Boolean { + return false + } + + data class PinAuthData( + val deviceCode: String, + val userCode: String, + val verificationUrl: String, + val expiresIn: Int, + val interval: Int, + ) } \ 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 5c02e7f7..0551fe6c 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 @@ -32,6 +32,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override val redirectUrl = "anilistlogin" override val idPrefix = "anilist" override var requireLibraryRefresh = true + override val supportDeviceAuth = false override var mainUrl = "https://anilist.co" override val icon = R.drawable.ic_anilist_icon override val requiresLogin = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt index 7ec168da..94537ea3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt @@ -11,6 +11,7 @@ class Dropbox : OAuth2API { override val key = "zlqsamadlwydvb2" override val redirectUrl = "dropboxlogin" override val requiresLogin = true + override val supportDeviceAuth = false override val createAccountUrl: String? = null override val icon: Int 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 index 7552fe9d..00f8d00c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -21,6 +21,7 @@ class LocalList : SyncAPI { override val name = "Local" override val icon: Int = R.drawable.ic_baseline_storage_24 override val requiresLogin = false + override val supportDeviceAuth = false override val createAccountUrl: Nothing? = null override val idPrefix = "local" override var requireLibraryRefresh = true 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 fdbe763a..4249f949 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 @@ -40,6 +40,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private val apiUrl = "https://api.myanimelist.net" override val icon = R.drawable.mal_logo override val requiresLogin = false + override val supportDeviceAuth = false override val syncIdName = SyncIdName.MyAnimeList override var requireLibraryRefresh = true override val createAccountUrl = "$mainUrl/register.php" diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 08c8588b..4385fa5e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -22,6 +22,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AuthAPI +import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType @@ -45,6 +46,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override var name = "Simkl" override val key = "simkl-key" override val redirectUrl = "simkl" + override val supportDeviceAuth = true override val idPrefix = "simkl" override var requireLibraryRefresh = true override var mainUrl = "https://api.simkl.com" @@ -267,6 +269,21 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ) } + data class PinAuthResponse( + @JsonProperty("result") val result: String, + @JsonProperty("device_code") val deviceCode: String, + @JsonProperty("user_code") val userCode: String, + @JsonProperty("verification_url") val verificationUrl: String, + @JsonProperty("expires_in") val expiresIn: Int, + @JsonProperty("interval") val interval: Int, + ) + + data class PinExchangeResponse( + @JsonProperty("result") val result: String, + @JsonProperty("message") val message: String? = null, + @JsonProperty("access_token") val accessToken: String? = null, + ) + // ------------------- data class ActivitiesResponse( val all: String?, @@ -1045,6 +1062,44 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { return simklUrlRegex.find(url)?.groupValues?.get(1) ?: "" } + override suspend fun getDevicePin(): OAuth2API.PinAuthData? { + val pinAuthResp = app.get( + "$mainUrl/oauth/pin?client_id=$clientId&redirect_uri=$appString://${redirectUrl}" + ).parsedSafe() ?: return null + + return OAuth2API.PinAuthData( + deviceCode = pinAuthResp.deviceCode, + userCode = pinAuthResp.userCode, + verificationUrl = pinAuthResp.verificationUrl, + expiresIn = pinAuthResp.expiresIn, + interval = pinAuthResp.interval + ) + } + + override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean { + val pinAuthResp = app.get( + "$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$clientId" + ).parsedSafe() ?: return false + + if (pinAuthResp.accessToken != null) { + switchToNewAccount() + setKey(accountId, SIMKL_TOKEN_KEY, pinAuthResp.accessToken) + + val user = getUser() + if (user == null) { + removeKey(accountId, SIMKL_TOKEN_KEY) + switchToOldAccount() + return false + } + + setKey(accountId, SIMKL_USER_KEY, user) + registerAccount() + requireLibraryRefresh = true + return true + } + return false + } + override suspend fun handleRedirect(url: String): Boolean { val uri = url.toUri() val state = uri.getQueryParameter("state") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index 0e8160db..e0762cc5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.result import android.content.Context +import android.graphics.Bitmap import android.util.Log import android.widget.ImageView import android.widget.TextView @@ -84,12 +85,14 @@ sealed class UiImage { ) : UiImage() data class Drawable(@DrawableRes val resId: Int) : UiImage() + data class Bitmap(val bitmap: android.graphics.Bitmap) : UiImage() } fun ImageView?.setImage(value: UiImage?, fadeIn: Boolean = true) { when (value) { is UiImage.Image -> setImageImage(value, fadeIn) is UiImage.Drawable -> setImageDrawable(value) + is UiImage.Bitmap -> setImageBitmap(value) null -> { this?.isVisible = false } @@ -107,6 +110,12 @@ fun ImageView?.setImageDrawable(value: UiImage.Drawable) { this.setImage(UiImage.Drawable(value.resId)) } +fun ImageView?.setImageBitmap(value: UiImage.Bitmap) { + if (this == null) return + this.isVisible = true + this.setImageBitmap(value.bitmap) +} + @JvmName("imgNull") fun img( url: String?, @@ -129,6 +138,10 @@ fun img(@DrawableRes drawable: Int): UiImage { return UiImage.Drawable(drawable) } +fun img(bitmap: Bitmap): UiImage { + return UiImage.Bitmap(bitmap) +} + fun txt(value: String): UiText { return UiText.DynamicString(value) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index a8358d0d..d227f9f6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -1,12 +1,16 @@ package com.lagradost.cloudstream3.ui.settings +import android.graphics.Bitmap import android.os.Bundle +import android.os.CountDownTimer import android.view.View -import android.view.View.* +import android.view.View.FOCUS_DOWN import android.view.inputmethod.EditorInfo import android.widget.TextView import androidx.annotation.UiThread import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmapOrNull import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity @@ -21,6 +25,7 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.AccountManagmentBinding import com.lagradost.cloudstream3.databinding.AccountSwitchBinding import com.lagradost.cloudstream3.databinding.AddAccountInputBinding +import com.lagradost.cloudstream3.databinding.DeviceAuthBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi @@ -31,6 +36,10 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subDlAp import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API +import com.lagradost.cloudstream3.ui.result.img +import com.lagradost.cloudstream3.ui.result.setImage +import com.lagradost.cloudstream3.ui.result.setText +import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -51,9 +60,13 @@ import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage +import com.lagradost.cloudstream3.utils.UIHelper.toPx +import qrcode.QRCode +import java.io.ByteArrayOutputStream class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback { companion object { @@ -134,7 +147,109 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome try { when (api) { is OAuth2API -> { - api.authenticate(activity) + if (isLayout(PHONE) || !api.supportDeviceAuth) { + api.authenticate(activity) + } else if (api.supportDeviceAuth && activity != null) { + + val binding: DeviceAuthBinding = + DeviceAuthBinding.inflate(activity.layoutInflater, null, false) + + val builder = + AlertDialog.Builder(activity) + .setView(binding.root) + + builder.apply { + setNegativeButton(R.string.cancel) { _, _ -> } + setPositiveButton(R.string.auth_locally) { _, _ -> + api.authenticate(activity) + } + } + + val dialog = builder.create() + + ioSafe { + try { + val pinCodeData = api.getDevicePin() + if (pinCodeData == null) { + showToast(R.string.device_pin_error_message) + api.authenticate(activity) + return@ioSafe + } + + /*val logoBytes = ContextCompat.getDrawable( + activity, + R.drawable.cloud_2_solid + )?.toBitmapOrNull()?.let { bitmap -> + val csLogo = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, csLogo) + csLogo.toByteArray() + }*/ + + val qrCodeImage = QRCode.ofRoundedSquares() + .withColor(activity.colorFromAttribute(R.attr.textColor)) + .withBackgroundColor(activity.colorFromAttribute(R.attr.primaryBlackBackground)) + //.withLogo(logoBytes, 200.toPx, 200.toPx) //For later if logo needed anytime + .build(pinCodeData.verificationUrl) + .render().nativeImage() as Bitmap + + activity.runOnUiThread { + dialog.show() + binding.apply { + devicePinCode.setText(txt(pinCodeData.userCode)) + deviceAuthMessage.setText( + txt( + R.string.device_pin_url_message, + pinCodeData.verificationUrl + ) + ) + deviceAuthQrcode.setImage( + img(qrCodeImage) + ) + } + + val expirationMillis = + pinCodeData.expiresIn.times(1000).toLong() + + object : CountDownTimer(expirationMillis, 1000) { + + override fun onTick(millisUntilFinished: Long) { + val secondsUntilFinished = + millisUntilFinished.div(1000).toInt() + + binding.deviceAuthValidationCounter.setText( + txt( + R.string.device_pin_counter_text, + secondsUntilFinished.div(60), + secondsUntilFinished.rem(60) + ) + ) + + ioSafe { + if (secondsUntilFinished.rem(pinCodeData.interval) == 0 && api.handleDeviceAuth(pinCodeData)) { + showToast( + txt( + R.string.authenticated_user, + api.name + ) + ) + dialog.dismissSafe(activity) + cancel() + } + } + } + + override fun onFinish() { + showToast(R.string.device_pin_expired_message) + dialog.dismissSafe(activity) + } + + }.start() + } + } catch (e: Exception) { + logError(e) + } + } + } } is InAppAuthAPI -> { @@ -227,23 +342,15 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome server = if (api.requiresServer) binding.loginServerInput.text?.toString() else null, ) ioSafe { - val isSuccessful = try { - api.login(loginData) + try { + showToast( + txt( + if (api.login(loginData)) R.string.authenticated_user else R.string.authenticated_user_fail, + api.name + ) + ) } catch (e: Exception) { logError(e) - false - } - activity.runOnUiThread { - try { - showToast( - activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail) - .format( - api.name - ) - ) - } catch (e: Exception) { - logError(e) // format might fail - } } } dialog.dismissSafe(activity) diff --git a/app/src/main/res/drawable/cloud_2_solid.xml b/app/src/main/res/drawable/cloud_2_solid.xml new file mode 100644 index 00000000..3810b4bf --- /dev/null +++ b/app/src/main/res/drawable/cloud_2_solid.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/example_qr.png b/app/src/main/res/drawable/example_qr.png new file mode 100644 index 0000000000000000000000000000000000000000..18decbac49533dcd2890ac8112305b19d05cfa11 GIT binary patch literal 46354 zcmeFa3pmtk_cxv~gJ$H^FvwvhVn_xlr-X5?90nDoCgqfBqm !=O#FGnJfjN;>Qk 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 + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d9317ccd..f16ca7d0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -497,6 +497,7 @@ account Log out Log in + Auth Locally Switch account Add account Create account @@ -562,6 +563,7 @@ SDR Web Poster Image + QR Code Image Player Resolution and title Title @@ -780,4 +782,8 @@ Media Reset CloudStream Wiki + Visit %s on your smartphone or computer and enter the above code + Can\'t get the device PIN code, try local authentication + PIN code is now expired ! + Code expires in %1$dm %2$ds \ No newline at end of file From c71d5d8add602ba0be84ddf197da1918c6f65970 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Wed, 19 Jun 2024 20:06:40 +0530 Subject: [PATCH 504/570] feat(ui): new dialog on adding repository and auto redirection (#1025) --- .../lagradost/cloudstream3/MainActivity.kt | 5 +- .../ui/account/AccountSelectActivity.kt | 3 +- .../ui/settings/SettingsAccount.kt | 4 +- .../settings/extensions/ExtensionsFragment.kt | 6 +-- .../ui/settings/extensions/PluginsFragment.kt | 9 +++- .../lagradost/cloudstream3/utils/AppUtils.kt | 50 ++++++++++++------- .../utils/BiometricAuthenticator.kt | 10 ++-- app/src/main/res/menu/repository.xml | 3 +- .../main/res/navigation/mobile_navigation.xml | 21 ++++++++ app/src/main/res/values/strings.xml | 12 +++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 11 files changed, 84 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index cc2c99de..8d312ceb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -134,7 +134,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup -import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled @@ -186,8 +186,7 @@ import kotlin.system.exitProcess //https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225 -class MainActivity : AppCompatActivity(), ColorPickerDialogListener, - BiometricAuthenticator.BiometricAuthCallback { +class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCallback { companion object { const val VLC_PACKAGE = "org.videolan.vlc" const val MPV_PACKAGE = "is.xyz.mpv" diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt index 0b0d83db..0da69f9c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled @@ -33,7 +34,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute -class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.BiometricAuthCallback { +class AccountSelectActivity : AppCompatActivity(), BiometricCallback { lateinit var viewModel: AccountViewModel diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index d227f9f6..67a2a15b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -51,7 +51,7 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setTool import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.BackupUtils -import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock @@ -68,7 +68,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx import qrcode.QRCode import java.io.ByteArrayOutputStream -class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback { +class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback { companion object { /** Used by nginx plugin too */ fun showLoginInfo( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index ebd3260f..1364c376 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -33,7 +33,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.AppUtils.downloadAllPluginsDialog +import com.lagradost.cloudstream3.utils.AppUtils.addRepositoryDialog import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -273,9 +273,9 @@ class ExtensionsFragment : Fragment() { if (plugins.isNullOrEmpty()) { showToast(R.string.no_plugins_found_error, Toast.LENGTH_LONG) } else { - this@ExtensionsFragment.activity?.downloadAllPluginsDialog( + this@ExtensionsFragment.activity?.addRepositoryDialog( + fixedName, url, - fixedName ) } } 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 acfbc584..3bdcb251 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 @@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName +import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentPluginsBinding @@ -70,6 +71,8 @@ class PluginsFragment : Fragment() { val name = arguments?.getString(PLUGINS_BUNDLE_NAME) val url = arguments?.getString(PLUGINS_BUNDLE_URL) val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true + // download all extensions button + val downloadAllButton = binding?.settingsToolbar?.menu?.findItem(R.id.download_all) if (url == null || name == null) { activity?.onBackPressedDispatcher?.onBackPressed() @@ -171,7 +174,7 @@ class PluginsFragment : Fragment() { if (isLocal) { // No download button and no categories on local - binding?.settingsToolbar?.menu?.findItem(R.id.download_all)?.isVisible = false + downloadAllButton?.isVisible = false binding?.settingsToolbar?.menu?.findItem(R.id.lang_filter)?.isVisible = false pluginViewModel.updatePluginListLocal() @@ -179,6 +182,10 @@ class PluginsFragment : Fragment() { } else { pluginViewModel.updatePluginList(context, url) binding?.tvtypesChipsScroll?.root?.isVisible = true + // not needed for users but may be useful for devs + downloadAllButton?.isVisible = BuildConfig.DEBUG + + bindChips( binding?.tvtypesChipsScroll?.tvtypesChips, 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 ff27b192..626eca12 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -62,7 +62,8 @@ import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WebviewFragment import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.settings.Globals -import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel.Companion.downloadAll +import com.lagradost.cloudstream3.ui.settings.extensions.ExtensionsFragment +import com.lagradost.cloudstream3.ui.settings.extensions.PluginsFragment import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -386,7 +387,7 @@ object AppUtils { ) } afterRepositoryLoadedEvent.invoke(true) - downloadAllPluginsDialog(url, repo.name) + addRepositoryDialog(repo.name, url) } } @@ -429,25 +430,36 @@ object AppUtils { } } + fun Activity.addRepositoryDialog( + repositoryName: String, + repositoryURL: String, + ) { + val repos = RepositoryManager.getRepositories() - fun Activity.downloadAllPluginsDialog(repositoryUrl: String, repositoryName: String) { - runOnUiThread { - val context = this - val builder: AlertDialog.Builder = AlertDialog.Builder(this) - builder.setTitle( - repositoryName - ) - builder.setMessage( - R.string.download_all_plugins_from_repo - ) - builder.apply { - setPositiveButton(R.string.download) { _, _ -> - downloadAll(context, repositoryUrl, null) - } - - setNegativeButton(R.string.no) { _, _ -> } + // navigate to newly added repository on pressing Open Repository + fun openAddedRepo() { + if (repos.isNotEmpty()) { + navigate( + R.id.global_to_navigation_settings_plugins, + PluginsFragment.newInstance( + repositoryName, + repositoryURL, + false, + ) + ) + } + } + + runOnUiThread { + AlertDialog.Builder(this).apply { + setTitle(repositoryName) + setMessage(R.string.download_all_plugins_from_repo) + setPositiveButton(R.string.open_downloaded_repo) { _, _ -> + openAddedRepo() + } + setNegativeButton(R.string.dismiss, null) + show().setDefaultFocus() } - builder.show().setDefaultFocus() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt index c57600ee..45acbab4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt @@ -26,7 +26,7 @@ object BiometricAuthenticator { private var biometricManager: BiometricManager? = null var biometricPrompt: BiometricPrompt? = null var promptInfo: BiometricPrompt.PromptInfo? = null - var authCallback: BiometricAuthCallback? = null // listen to authentication success + var authCallback: BiometricCallback? = null // listen to authentication success private fun initializeBiometrics(activity: Activity) { val executor = ContextCompat.getMainExecutor(activity) @@ -141,14 +141,14 @@ object BiometricAuthenticator { // function to start authentication in any fragment or activity fun startBiometricAuthentication(activity: Activity, title: Int, setDeviceCred: Boolean) { initializeBiometrics(activity) - authCallback = activity as? BiometricAuthCallback + authCallback = activity as? BiometricCallback if (isBiometricHardWareAvailable()) { - authCallback = activity as? BiometricAuthCallback + authCallback = activity as? BiometricCallback authenticationDialog(activity, title, setDeviceCred) promptInfo?.let { biometricPrompt?.authenticate(it) } } else { if (deviceHasPasswordPinLock(activity)) { - authCallback = activity as? BiometricAuthCallback + authCallback = activity as? BiometricCallback authenticationDialog(activity, R.string.password_pin_authentication_title, true) promptInfo?.let { biometricPrompt?.authenticate(it) } @@ -165,7 +165,7 @@ object BiometricAuthenticator { } } - interface BiometricAuthCallback { + interface BiometricCallback { fun onAuthenticationSuccess() fun onAuthenticationError() } diff --git a/app/src/main/res/menu/repository.xml b/app/src/main/res/menu/repository.xml index be99b1a8..7aa1f200 100644 --- a/app/src/main/res/menu/repository.xml +++ b/app/src/main/res/menu/repository.xml @@ -21,5 +21,6 @@ android:id="@+id/download_all" android:icon="@drawable/netflix_download" android:title="@string/batch_download" - app:showAsAction="collapseActionView|ifRoom" /> + app:showAsAction="collapseActionView|ifRoom" + android:visible="false"/> \ 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 d0df339b..fafb6968 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -111,6 +111,27 @@ app:argType="boolean" /> + + + + + + Livestream NSFW Video + Music + Audio Book + Media Source error Remote error Renderer error @@ -617,7 +620,7 @@ View community repositories Public list Uppercase all subtitles - Download all plugins from this repository? + Warning: CloudStream 3 does not take any responsibility for using third-party extensions and does not provide any support for them! %s (Disabled) Tracks Audio tracks @@ -668,6 +671,8 @@ Yes No OK + Dismiss + Open repository Disable Battery optimization To ensure uninterrupted downloads and notifications for subscribed TV shows, CloudStream needs permission to run in background. By pressing "OK", you\'ll be directed to App info. @@ -775,11 +780,8 @@ Password/PIN Authentication Biometric authentication is not supported on this device Unlock the app with Fingerprint, Face ID, PIN, Pattern and Password. - This screen was closed due to multiple failed attempts. Please restart the application. + After a few failed attempts, the prompt will close. Simply restart the app to try again. Your CloudStream data has been backed up now. Although the possibility of this is very low, all devices can behave differently. In the rare case, that you get locked out from accessing the app, clear the app data completely and restore from a backup. We are very sorry for any inconvenience arising from this. - Music - Audio Book - Media Reset CloudStream Wiki Visit %s on your smartphone or computer and enter the above code diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fc2d0f86..2968a1b2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Apr 30 17:11:15 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From b9746c2b17be02f2d33c12d20907cb9301a8815c Mon Sep 17 00:00:00 2001 From: "imgbot[bot]" <31301654+imgbot[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 19:03:55 +0200 Subject: [PATCH 505/570] [ImgBot] Optimize images (#1144) /app/src/main/res/drawable/example_qr.png -- 45.27kb -> 1.28kb (97.17%) Signed-off-by: ImgBotApp Co-authored-by: ImgBotApp --- app/src/main/res/drawable/example_qr.png | Bin 46354 -> 1313 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/src/main/res/drawable/example_qr.png b/app/src/main/res/drawable/example_qr.png index 18decbac49533dcd2890ac8112305b19d05cfa11..764cb9660e4edb5e3957576e4e0f24ed3b412d6d 100644 GIT binary patch literal 1313 zcmeAS@N?(olHy`uVBq!ia0y~yVEzxnj6eZ~eU*;CffP%+qpu?a!^VE@KZ&eBzCyA` zkS_y6l^O#>Lkk1LFQ8Dv3kHT#0|tgy2@DKYGZ+}e3+C(!v;j&mC3(BMFfiWj5?%u2 zv6p!Iy0X7u6Xw$p*=CmM3^ZHQ)5S5Q;?~={j(N8Y1XwR{?s;*^{X<~QmM0mOUHuPI z#TsvNbVyB-@(P=<$l-6ApoE}l-Mu=VlN$fG<$)auG!_O7{?>L*oi%-5U5#CBUES{= zH8bRWy64%8pZWUQ|Nm4gm@*h`(oifFAJVW=Z{uRV$o5NrSzr=SxR> zpnaFoEqmbS5IOt838g7Vnb7sgGo1O`|F`G(oC#~V(6zl|Uh~dy=CApy+TVwsek<4~ zf8z<7{tx$=+pfB63vG3GFYqB6zA=?atMO=t^`LPXFxcWq7i3-p7u$9I5}hUK=Q) z+tZQ7?HT-6ZOREX6D2dggA(rz(N(O--dbN`?PB(N8t3O0H19mv$herX=h?IgpXUko zU2S-emlP+YI-x}y!-S*af&2g6an(31*mri7@iYv1HIwjD9O^iZEs9gx|GcEoD?PS-&EcidXet;Aq~nsMYMnOS+P^OQvi-L^ z#h`qP1c2VC*{4*wcgjIc$@O=eT%CfLk5&61DF89$q&Li2-_a4PEaRMbq(~aw83*_d z#C&dgHsxI1vBx*mRZJ6pilQ07uv>Ui$+5*#)^~d>;&PC3P%)N6S7H5a#+O4|{IZSZ zZY#^6N%r$fgng^JRnM`)-QpD=JDO}m(RbVB_3P0@8S?fs9;*!tnzmqn98gNN#5JNM zC9x#cD!C{XNHG{07@6oAnCTi?h8UVy8Jk-fn`s*uSQ!|^y#8;Cq9HdwB{QuOw+7v9 R?u{VJJzf1=);T3K0RSlGT`~Xw literal 46354 zcmeFa3pmtk_cxv~gJ$H^FvwvhVn_xlr-X5?90nDoCgqfBqm !=O#FGnJfjN;>Qk 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 Date: Mon, 24 Jun 2024 12:04:45 -0600 Subject: [PATCH 506/570] Downloads: performance improvements and merge adapters (#1145) --- .../ui/download/DownloadAdapter.kt | 223 ++++++++++++++ .../ui/download/DownloadButtonSetup.kt | 2 - .../ui/download/DownloadChildAdapter.kt | 94 ------ .../ui/download/DownloadChildFragment.kt | 56 ++-- .../ui/download/DownloadFragment.kt | 289 ++++++++---------- .../ui/download/DownloadHeaderAdapter.kt | 149 --------- .../ui/download/DownloadViewModel.kt | 58 ++-- .../cloudstream3/ui/result/EpisodeAdapter.kt | 36 +-- .../ui/result/ResultFragmentPhone.kt | 24 +- .../ui/result/ResultViewModel2.kt | 46 +-- .../cloudstream3/ui/search/SearchHelper.kt | 20 +- .../cloudstream3/utils/VideoDownloadHelper.kt | 12 +- .../res/layout/download_header_episode.xml | 6 +- 13 files changed, 488 insertions(+), 527 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt new file mode 100644 index 00000000..8f496b3c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -0,0 +1,223 @@ +package com.lagradost.cloudstream3.ui.download + +import android.annotation.SuppressLint +import android.text.format.Formatter.formatShortFileSize +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding +import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppUtils.getNameFull +import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual +import com.lagradost.cloudstream3.utils.UIHelper.setImage +import com.lagradost.cloudstream3.utils.VideoDownloadHelper + +const val DOWNLOAD_ACTION_PLAY_FILE = 0 +const val DOWNLOAD_ACTION_DELETE_FILE = 1 +const val DOWNLOAD_ACTION_RESUME_DOWNLOAD = 2 +const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3 +const val DOWNLOAD_ACTION_DOWNLOAD = 4 +const val DOWNLOAD_ACTION_LONG_CLICK = 5 + +abstract class VisualDownloadCached( + open val currentBytes: Long, + open val totalBytes: Long, + open val data: VideoDownloadHelper.DownloadCached +) { + + // Just to be extra-safe with areContentsTheSame + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is VisualDownloadCached) return false + + if (currentBytes != other.currentBytes) return false + if (totalBytes != other.totalBytes) return false + if (data != other.data) return false + + return true + } + + override fun hashCode(): Int { + var result = currentBytes.hashCode() + result = 31 * result + totalBytes.hashCode() + result = 31 * result + data.hashCode() + return result + } +} + +data class VisualDownloadChildCached( + override val currentBytes: Long, + override val totalBytes: Long, + override val data: VideoDownloadHelper.DownloadEpisodeCached, +): VisualDownloadCached(currentBytes, totalBytes, data) + +data class VisualDownloadHeaderCached( + override val currentBytes: Long, + override val totalBytes: Long, + override val data: VideoDownloadHelper.DownloadHeaderCached, + val child: VideoDownloadHelper.DownloadEpisodeCached?, + val currentOngoingDownloads: Int, + val totalDownloads: Int, +): VisualDownloadCached(currentBytes, totalBytes, data) + +data class DownloadClickEvent( + val action: Int, + val data: VideoDownloadHelper.DownloadEpisodeCached +) + +data class DownloadHeaderClickEvent( + val action: Int, + val data: VideoDownloadHelper.DownloadHeaderCached +) + +class DownloadAdapter( + private val clickCallback: (DownloadHeaderClickEvent) -> Unit, + private val mediaClickCallback: (DownloadClickEvent) -> Unit, +) : ListAdapter(DiffCallback()) { + + companion object { + private const val VIEW_TYPE_HEADER = 0 + private const val VIEW_TYPE_CHILD = 1 + } + + inner class DownloadViewHolder( + private val binding: ViewBinding, + private val clickCallback: (DownloadHeaderClickEvent) -> Unit, + private val mediaClickCallback: (DownloadClickEvent) -> Unit, + ) : RecyclerView.ViewHolder(binding.root) { + + @SuppressLint("SetTextI18n") + fun bind(card: VisualDownloadCached?) { + when (binding) { + is DownloadHeaderEpisodeBinding -> binding.apply { + if (card == null || card !is VisualDownloadHeaderCached) return@apply + val d = card.data + + downloadHeaderPoster.apply { + setImage(d.poster) + setOnClickListener { + clickCallback.invoke(DownloadHeaderClickEvent(1, d)) + } + } + + downloadHeaderTitle.text = d.name + val mbString = formatShortFileSize(itemView.context, card.totalBytes) + + if (card.child != null) { + downloadHeaderGotoChild.isVisible = false + + downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback) + downloadButton.isVisible = true + + episodeHolder.setOnClickListener { + mediaClickCallback.invoke( + DownloadClickEvent( + DOWNLOAD_ACTION_PLAY_FILE, + card.child + ) + ) + } + } else { + downloadButton.isVisible = false + downloadHeaderGotoChild.isVisible = true + + try { + downloadHeaderInfo.text = + downloadHeaderInfo.context.getString(R.string.extra_info_format) + .format( + card.totalDownloads, + if (card.totalDownloads == 1) downloadHeaderInfo.context.getString( + R.string.episode + ) else downloadHeaderInfo.context.getString( + R.string.episodes + ), + mbString + ) + } catch (t: Throwable) { + // You probably formatted incorrectly + downloadHeaderInfo.text = "Error" + logError(t) + } + + episodeHolder.setOnClickListener { + clickCallback.invoke(DownloadHeaderClickEvent(0, d)) + } + } + } + + is DownloadChildEpisodeBinding -> binding.apply { + if (card == null || card !is VisualDownloadChildCached) return@apply + val d = card.data + + val posDur = DataStoreHelper.getViewPos(d.id) + downloadChildEpisodeProgress.apply { + if (posDur != null) { + val visualPos = posDur.fixVisual() + max = (visualPos.duration / 1000).toInt() + progress = (visualPos.position / 1000).toInt() + isVisible = true + } else isVisible = false + } + + downloadButton.setDefaultClickListener(card.data, downloadChildEpisodeTextExtra, mediaClickCallback) + + downloadChildEpisodeText.apply { + text = context.getNameFull(d.name, d.episode, d.season) + isSelected = true // Needed for text repeating + } + + downloadChildEpisodeHolder.setOnClickListener { + mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) + } + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadViewHolder { + val binding = when (viewType) { + VIEW_TYPE_HEADER -> { + DownloadHeaderEpisodeBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + } + VIEW_TYPE_CHILD -> { + DownloadChildEpisodeBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + } + else -> throw IllegalArgumentException("Invalid view type") + } + return DownloadViewHolder(binding, clickCallback, mediaClickCallback) + } + + override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + override fun getItemViewType(position: Int): Int { + val card = getItem(position) + return if (card is VisualDownloadChildCached) VIEW_TYPE_CHILD else VIEW_TYPE_HEADER + } + + class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean { + return oldItem.data.id == newItem.data.id + } + + override fun areContentsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean { + return oldItem == newItem + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index 10ce67a7..880d5f6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.download -import android.app.Activity import android.content.DialogInterface import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -22,7 +21,6 @@ import com.lagradost.cloudstream3.utils.VideoDownloadManager object DownloadButtonSetup { fun handleDownloadClick(click: DownloadClickEvent) { val id = click.data.id - if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return when (click.action) { DOWNLOAD_ACTION_DELETE_FILE -> { activity?.let { ctx -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt deleted file mode 100644 index 1d7b5a83..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.lagradost.cloudstream3.ui.download - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull -import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual -import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.VideoDownloadHelper - -const val DOWNLOAD_ACTION_PLAY_FILE = 0 -const val DOWNLOAD_ACTION_DELETE_FILE = 1 -const val DOWNLOAD_ACTION_RESUME_DOWNLOAD = 2 -const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3 -const val DOWNLOAD_ACTION_DOWNLOAD = 4 -const val DOWNLOAD_ACTION_LONG_CLICK = 5 - -data class VisualDownloadChildCached( - val currentBytes: Long, - val totalBytes: Long, - val data: VideoDownloadHelper.DownloadEpisodeCached, -) - -data class DownloadClickEvent(val action: Int, val data: VideoDownloadHelper.DownloadEpisodeCached) - -class DownloadChildAdapter( - var cardList: List, - private val clickCallback: (DownloadClickEvent) -> Unit, -) : RecyclerView.Adapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return DownloadChildViewHolder( - DownloadChildEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false), - clickCallback - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is DownloadChildViewHolder -> { - holder.bind(cardList[position]) - } - } - } - - override fun getItemCount(): Int { - return cardList.size - } - - class DownloadChildViewHolder - constructor( - val binding: DownloadChildEpisodeBinding, - private val clickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(binding.root) { - - /*private val title: TextView = itemView.download_child_episode_text - private val extraInfo: TextView = itemView.download_child_episode_text_extra - private val holder: CardView = itemView.download_child_episode_holder - private val progressBar: ContentLoadingProgressBar = itemView.download_child_episode_progress - private val progressBarDownload: ContentLoadingProgressBar = itemView.download_child_episode_progress_downloaded - private val downloadImage: ImageView = itemView.download_child_episode_download*/ - - - fun bind(card: VisualDownloadChildCached) { - val d = card.data - - val posDur = getViewPos(d.id) - binding.downloadChildEpisodeProgress.apply { - if (posDur != null) { - val visualPos = posDur.fixVisual() - max = (visualPos.duration / 1000).toInt() - progress = (visualPos.position / 1000).toInt() - visibility = View.VISIBLE - } else { - visibility = View.GONE - } - } - - binding.downloadButton.setDefaultClickListener(card.data, binding.downloadChildEpisodeTextExtra, clickCallback) - - binding.downloadChildEpisodeText.apply { - text = context.getNameFull(d.name, d.episode, d.season) - isSelected = true // is needed for text repeating - } - - - binding.downloadChildEpisodeHolder.setOnClickListener { - clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) - } - } - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index f54c8698..7734cb08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -5,7 +5,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick @@ -40,7 +39,8 @@ class DownloadChildFragment : Fragment() { super.onDestroyView() } - var binding: FragmentChildDownloadsBinding? = null + private var binding: FragmentChildDownloadsBinding? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -48,7 +48,7 @@ class DownloadChildFragment : Fragment() { ): View { val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false) binding = localBinding - return localBinding.root//inflater.inflate(R.layout.fragment_child_downloads, container, false) + return localBinding.root } private fun updateList(folder: String) = main { @@ -60,7 +60,11 @@ class DownloadChildFragment : Fragment() { }.mapNotNull { val info = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(ctx, it.id) ?: return@mapNotNull null - VisualDownloadChildCached(info.fileLength, info.totalBytes, it) + VisualDownloadChildCached( + currentBytes = info.fileLength, + totalBytes = info.totalBytes, + data = it, + ) } }.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 } if (eps.isEmpty()) { @@ -68,9 +72,7 @@ class DownloadChildFragment : Fragment() { return@main } - (binding?.downloadChildList?.adapter as DownloadChildAdapter? ?: return@main).cardList = - eps - binding?.downloadChildList?.adapter?.notifyDataSetChanged() + (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(eps) } } @@ -98,31 +100,39 @@ class DownloadChildFragment : Fragment() { setAppBarNoScrollFlagsOnTV() } - val adapter: RecyclerView.Adapter = - DownloadChildAdapter( - ArrayList(), - ) { click -> - handleDownloadClick(click) + val adapter = DownloadAdapter( + {}, + { downloadClickEvent -> + handleDownloadClick(downloadClickEvent) + if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { + setUpDownloadDeleteListener(folder) + } } + ) + binding?.downloadChildList?.apply { + setHasFixedSize(true) + setItemViewCacheSize(20) + this.adapter = adapter + setLinearListLayout( + isHorizontal = false, + nextRight = FOCUS_SELF, + nextDown = FOCUS_SELF, + ) + } + + updateList(folder) + } + + private fun setUpDownloadDeleteListener(folder: String) { downloadDeleteEventListener = { id: Int -> - val list = (binding?.downloadChildList?.adapter as DownloadChildAdapter?)?.cardList + val list = (binding?.downloadChildList?.adapter as? DownloadAdapter)?.currentList if (list != null) { if (list.any { it.data.id == id }) { updateList(folder) } } } - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } - - binding?.downloadChildList?.adapter = adapter - binding?.downloadChildList?.setLinearListLayout( - isHorizontal = false, - nextDown = FOCUS_SELF, - nextRight = FOCUS_SELF - )//layoutManager = GridLayoutManager(context, 1) - - updateList(folder) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 31790b0f..de2d4f3c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -10,14 +10,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout +import android.widget.TextView import android.widget.Toast +import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding @@ -42,11 +43,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV -import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import java.net.URI - const val DOWNLOAD_NAVIGATE_TO = "downloadpage" class DownloadFragment : Fragment() { @@ -63,33 +62,30 @@ class DownloadFragment : Fragment() { private fun setList(list: List) { main { - (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list - binding?.downloadList?.adapter?.notifyDataSetChanged() + (binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(list) } } override fun onDestroyView() { - if (downloadDeleteEventListener != null) { - VideoDownloadManager.downloadDeleteEvent -= downloadDeleteEventListener!! - downloadDeleteEventListener = null + downloadDeleteEventListener?.let { + VideoDownloadManager.downloadDeleteEvent -= it } + downloadDeleteEventListener = null binding = null super.onDestroyView() } - var binding: FragmentDownloadsBinding? = null + private var binding: FragmentDownloadsBinding? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - downloadsViewModel = - ViewModelProvider(this)[DownloadViewModel::class.java] - + ): View { + downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] val localBinding = FragmentDownloadsBinding.inflate(inflater, container, false) binding = localBinding - return localBinding.root//inflater.inflate(R.layout.fragment_downloads, container, false) + return localBinding.root } private var downloadDeleteEventListener: ((Int) -> Unit)? = null @@ -97,7 +93,6 @@ class DownloadFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) hideKeyboard() - binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() observe(downloadsViewModel.noDownloadsText) { @@ -108,176 +103,148 @@ class DownloadFragment : Fragment() { binding?.downloadLoading?.isVisible = false } observe(downloadsViewModel.availableBytes) { - binding?.downloadFreeTxt?.text = - getString(R.string.storage_size_format).format( - getString(R.string.free_storage), - formatShortFileSize(view.context, it) - ) - binding?.downloadFree?.setLayoutWidth(it) + updateStorageInfo(view.context, it, R.string.free_storage, binding?.downloadFreeTxt, binding?.downloadFree) } observe(downloadsViewModel.usedBytes) { - binding?.apply { - downloadUsedTxt.text = - getString(R.string.storage_size_format).format( - getString(R.string.used_storage), - formatShortFileSize(view.context, it) - ) - downloadUsed.setLayoutWidth(it) - downloadStorageAppbar.isVisible = it > 0 - } + updateStorageInfo(view.context, it, R.string.used_storage, binding?.downloadUsedTxt, binding?.downloadUsed) + binding?.downloadStorageAppbar?.isVisible = it > 0 } observe(downloadsViewModel.downloadBytes) { - binding?.apply { - downloadAppTxt.text = - getString(R.string.storage_size_format).format( - getString(R.string.app_storage), - formatShortFileSize(view.context, it) - ) - downloadApp.setLayoutWidth(it) - } + updateStorageInfo(view.context, it, R.string.app_storage, binding?.downloadAppTxt, binding?.downloadApp) } - val adapter: RecyclerView.Adapter = - DownloadHeaderAdapter( - ArrayList(), - { click -> - when (click.action) { - 0 -> { - if (click.data.type.isMovieType()) { - //wont be called - } else { - val folder = DataStore.getFolderName( - DOWNLOAD_EPISODE_CACHE, - click.data.id.toString() - ) - activity?.navigate( - R.id.action_navigation_downloads_to_navigation_download_child, - DownloadChildFragment.newInstance(click.data.name, folder) - ) - } - } - - 1 -> { - (activity as AppCompatActivity?)?.loadResult( - click.data.url, - click.data.apiName - ) - } - } - - }, - { downloadClickEvent -> - if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter - handleDownloadClick(downloadClickEvent) - if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { - context?.let { ctx -> - downloadsViewModel.updateList(ctx) - } - } - } - ) - - downloadDeleteEventListener = { id -> - val list = (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList - if (list != null) { - if (list.any { it.data.id == id }) { - context?.let { ctx -> - setList(ArrayList()) - downloadsViewModel.updateList(ctx) - } + val adapter = DownloadAdapter( + { click -> + handleItemClick(click) + }, + { downloadClickEvent -> + handleDownloadClick(downloadClickEvent) + if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { + setUpDownloadDeleteListener() } } - } - - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + ) binding?.downloadList?.apply { + setHasFixedSize(true) + setItemViewCacheSize(20) this.adapter = adapter setLinearListLayout( isHorizontal = false, nextRight = FOCUS_SELF, nextUp = FOCUS_SELF, - nextDown = FOCUS_SELF + nextDown = FOCUS_SELF, ) - //layoutManager = GridLayoutManager(context, 1) } - // Should be visible in emulator layout - binding?.downloadStreamButton?.isGone = isLayout(TV) - binding?.downloadStreamButton?.setOnClickListener { - val dialog = - Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom) - - val binding = StreamInputBinding.inflate(dialog.layoutInflater) - - dialog.setContentView(binding.root) - - dialog.show() - - // If user has clicked the switch do not interfere - var preventAutoSwitching = false - binding.hlsSwitch.setOnClickListener { - preventAutoSwitching = true - } - - fun activateSwitchOnHls(text: String?) { - binding.hlsSwitch.isChecked = normalSafeApiCall { - URI(text).path?.substringAfterLast(".")?.contains("m3u") - } == true - } - - binding.streamReferer.doOnTextChanged { text, _, _, _ -> - if (!preventAutoSwitching) - activateSwitchOnHls(text?.toString()) - } - - (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt( - 0 - )?.text?.toString()?.let { copy -> - val fixedText = copy.trim() - binding.streamUrl.setText(fixedText) - activateSwitchOnHls(fixedText) - } - - binding.applyBtt.setOnClickListener { - val url = binding.streamUrl.text?.toString() - if (url.isNullOrEmpty()) { - showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT) - } else { - val referer = binding.streamReferer.text?.toString() - - activity?.navigate( - R.id.global_to_navigation_player, - GeneratorPlayer.newInstance( - LinkGenerator( - listOf(BasicLink(url)), - extract = true, - referer = referer, - isM3u8 = binding.hlsSwitch.isChecked - ) - ) - ) - - dialog.dismissSafe(activity) - } - } - - binding.cancelBtt.setOnClickListener { - dialog.dismissSafe(activity) - } + binding?.downloadStreamButton?.apply { + isGone = isLayout(TV) + setOnClickListener { showStreamInputDialog(it.context) } } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { binding?.downloadList?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> - val dy = scrollY - oldScrollY - if (dy > 0) { //check for scroll down - binding?.downloadStreamButton?.shrink() // hide - } else if (dy < -5) { - binding?.downloadStreamButton?.extend() // show - } + handleScroll(scrollY - oldScrollY) } } downloadsViewModel.updateList(requireContext()) - fixPaddingStatusbar(binding?.downloadRoot) } + + private fun handleItemClick(click: DownloadHeaderClickEvent) { + when (click.action) { + 0 -> { + if (!click.data.type.isMovieType()) { + val folder = DataStore.getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString()) + activity?.navigate( + R.id.action_navigation_downloads_to_navigation_download_child, + DownloadChildFragment.newInstance(click.data.name, folder) + ) + } + } + 1 -> { + (activity as AppCompatActivity?)?.loadResult(click.data.url, click.data.apiName) + } + } + } + + private fun setUpDownloadDeleteListener() { + downloadDeleteEventListener = { id -> + val list = (binding?.downloadList?.adapter as? DownloadAdapter)?.currentList + if (list?.any { it.data.id == id } == true) { + context?.let { downloadsViewModel.updateList(it) } + } + } + downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + } + + private fun updateStorageInfo( + context: Context, + bytes: Long, + @StringRes stringRes: Int, + textView: TextView?, + view: View? + ) { + textView?.text = getString(R.string.storage_size_format).format(getString(stringRes), formatShortFileSize(context, bytes)) + view?.setLayoutWidth(bytes) + } + + private fun showStreamInputDialog(context: Context) { + val dialog = Dialog(context, R.style.AlertDialogCustom) + val binding = StreamInputBinding.inflate(dialog.layoutInflater) + dialog.setContentView(binding.root) + dialog.show() + + var preventAutoSwitching = false + binding.hlsSwitch.setOnClickListener { preventAutoSwitching = true } + + binding.streamReferer.doOnTextChanged { text, _, _, _ -> + if (!preventAutoSwitching) activateSwitchOnHls(text?.toString(), binding) + } + + (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt(0)?.text?.toString()?.let { copy -> + val fixedText = copy.trim() + binding.streamUrl.setText(fixedText) + activateSwitchOnHls(fixedText, binding) + } + + binding.applyBtt.setOnClickListener { + val url = binding.streamUrl.text?.toString() + if (url.isNullOrEmpty()) { + showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT) + } else { + val referer = binding.streamReferer.text?.toString() + activity?.navigate( + R.id.global_to_navigation_player, + GeneratorPlayer.newInstance( + LinkGenerator( + listOf(BasicLink(url)), + extract = true, + referer = referer, + isM3u8 = binding.hlsSwitch.isChecked + ) + ) + ) + dialog.dismissSafe(activity) + } + } + + binding.cancelBtt.setOnClickListener { + dialog.dismissSafe(activity) + } + } + + private fun activateSwitchOnHls(text: String?, binding: StreamInputBinding) { + binding.hlsSwitch.isChecked = normalSafeApiCall { + URI(text).path?.substringAfterLast(".")?.contains("m3u") + } == true + } + + private fun handleScroll(dy: Int) { + if (dy > 0) { + binding?.downloadStreamButton?.shrink() + } else if (dy < -5) { + binding?.downloadStreamButton?.extend() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt deleted file mode 100644 index 65a6441f..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt +++ /dev/null @@ -1,149 +0,0 @@ -package com.lagradost.cloudstream3.ui.download - -import android.annotation.SuppressLint -import android.text.format.Formatter.formatShortFileSize -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.UIHelper.setImage -import com.lagradost.cloudstream3.utils.VideoDownloadHelper -import java.util.* - -data class VisualDownloadHeaderCached( - val currentOngoingDownloads: Int, - val totalDownloads: Int, - val totalBytes: Long, - val currentBytes: Long, - val data: VideoDownloadHelper.DownloadHeaderCached, - val child: VideoDownloadHelper.DownloadEpisodeCached?, -) - -data class DownloadHeaderClickEvent( - val action: Int, - val data: VideoDownloadHelper.DownloadHeaderCached -) - -class DownloadHeaderAdapter( - var cardList: List, - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val movieClickCallback: (DownloadClickEvent) -> Unit, -) : RecyclerView.Adapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return DownloadHeaderViewHolder( - DownloadHeaderEpisodeBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ), - clickCallback, - movieClickCallback - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is DownloadHeaderViewHolder -> { - holder.bind(cardList[position]) - } - } - } - - override fun getItemCount(): Int { - return cardList.size - } - - class DownloadHeaderViewHolder - constructor( - val binding: DownloadHeaderEpisodeBinding, - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val movieClickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(binding.root) { - - /*private val poster: ImageView? = itemView.download_header_poster - private val title: TextView = itemView.download_header_title - private val extraInfo: TextView = itemView.download_header_info - private val holder: CardView = itemView.episode_holder - - private val downloadBar: ContentLoadingProgressBar = itemView.download_header_progress_downloaded - private val downloadImage: ImageView = itemView.download_header_episode_download - private val normalImage: ImageView = itemView.download_header_goto_child*/ - - @SuppressLint("SetTextI18n") - fun bind(card: VisualDownloadHeaderCached) { - val d = card.data - - binding.downloadHeaderPoster.apply { - setImage(d.poster) - setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(1, d)) - } - } - - binding.apply { - - binding.downloadHeaderTitle.text = d.name - val mbString = formatShortFileSize(itemView.context, card.totalBytes) - - //val isMovie = d.type.isMovieType() - if (card.child != null) { - //downloadHeaderProgressDownloaded.visibility = View.VISIBLE - - // downloadHeaderEpisodeDownload.visibility = View.VISIBLE - binding.downloadHeaderGotoChild.visibility = View.GONE - - downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, movieClickCallback) - downloadButton.isVisible = true - /*setUpButton( - card.currentBytes, - card.totalBytes, - downloadBar, - downloadImage, - extraInfo, - card.child, - movieClickCallback - )*/ - - episodeHolder.setOnClickListener { - movieClickCallback.invoke( - DownloadClickEvent( - DOWNLOAD_ACTION_PLAY_FILE, - card.child - ) - ) - } - } else { - downloadButton.isVisible = false - // downloadHeaderProgressDownloaded.visibility = View.GONE - // downloadHeaderEpisodeDownload.visibility = View.GONE - binding.downloadHeaderGotoChild.visibility = View.VISIBLE - - try { - downloadHeaderInfo.text = - downloadHeaderInfo.context.getString(R.string.extra_info_format).format( - card.totalDownloads, - if (card.totalDownloads == 1) downloadHeaderInfo.context.getString(R.string.episode) else downloadHeaderInfo.context.getString( - R.string.episodes - ), - mbString - ) - } catch (t: Throwable) { - // you probably formatted incorrectly - downloadHeaderInfo.text = "Error" - logError(t) - } - - - episodeHolder.setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(0, d)) - } - } - } - } - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt index 3a74a715..380430e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt @@ -39,6 +39,8 @@ class DownloadViewModel : ViewModel() { val availableBytes: LiveData = _availableBytes val downloadBytes: LiveData = _downloadBytes + private var previousVisual: List? = null + fun updateList(context: Context) = viewModelScope.launchSafe { val children = withContext(Dispatchers.IO) { val headers = context.getKeys(DOWNLOAD_EPISODE_CACHE) @@ -53,7 +55,6 @@ class DownloadViewModel : ViewModel() { // parentId : downloadsCount val totalDownloads = HashMap() - // Gets all children downloads withContext(Dispatchers.IO) { for (c in children) { @@ -69,7 +70,7 @@ class DownloadViewModel : ViewModel() { } } - val cached = withContext(Dispatchers.IO) { // wont fetch useless keys + val cached = withContext(Dispatchers.IO) { // Won't fetch useless keys totalDownloads.entries.filter { it.value > 0 }.mapNotNull { context.getKey( DOWNLOAD_HEADER_CACHE, @@ -79,7 +80,7 @@ class DownloadViewModel : ViewModel() { } val visual = withContext(Dispatchers.IO) { - cached.mapNotNull { // TODO FIX + cached.mapNotNull { val downloads = totalDownloads[it.id] ?: 0 val bytes = totalBytesUsedByChild[it.id] ?: 0 val currentBytes = currentBytesUsedByChild[it.id] ?: 0 @@ -91,32 +92,37 @@ class DownloadViewModel : ViewModel() { getFolderName(it.id.toString(), it.id.toString()) ) VisualDownloadHeaderCached( - 0, - downloads, - bytes, - currentBytes, - it, - movieEpisode + currentBytes = currentBytes, + totalBytes = bytes, + data = it, + child = movieEpisode, + currentOngoingDownloads = 0, + totalDownloads = downloads, ) }.sortedBy { (it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0) - } // episode sorting by episode, lowest to highest - } - try { - val stat = StatFs(Environment.getExternalStorageDirectory().path) - - val localBytesAvailable = stat.availableBytes//stat.blockSizeLong * stat.blockCountLong - val localTotalBytes = stat.blockSizeLong * stat.blockCountLong - val localDownloadedBytes = visual.sumOf { it.totalBytes } - - _usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes) - _availableBytes.postValue(localBytesAvailable) - _downloadBytes.postValue(localDownloadedBytes) - } catch (t : Throwable) { - _downloadBytes.postValue(0) - logError(t) + } // Episode sorting by episode, lowest to highest } - _headerCards.postValue(visual) + // Only update list if different from the previous one to prevent duplicate initialization + if (visual != previousVisual) { + previousVisual = visual + + try { + val stat = StatFs(Environment.getExternalStorageDirectory().path) + val localBytesAvailable = stat.availableBytes + val localTotalBytes = stat.blockSizeLong * stat.blockCountLong + val localDownloadedBytes = visual.sumOf { it.totalBytes } + + _usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes) + _availableBytes.postValue(localBytesAvailable) + _downloadBytes.postValue(localDownloadedBytes) + } catch (t: Throwable) { + _downloadBytes.postValue(0) + logError(t) + } + + _headerCards.postValue(visual) + } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index e4fd0559..62b1fdd1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -192,15 +192,15 @@ class EpisodeAdapter( downloadButton.isVisible = hasDownloadSupport downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - card.name, - card.poster, - card.episode, - card.season, - card.id, - card.parentId, - card.rating, - card.description, - System.currentTimeMillis(), + name = card.name, + poster = card.poster, + episode = card.episode, + season = card.season, + id = card.id, + parentId = card.parentId, + rating = card.rating, + description = card.description, + cacheTime = System.currentTimeMillis(), ), null ) { when (it.action) { @@ -343,15 +343,15 @@ class EpisodeAdapter( downloadButton.isVisible = hasDownloadSupport downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - card.name, - card.poster, - card.episode, - card.season, - card.id, - card.parentId, - card.rating, - card.description, - System.currentTimeMillis(), + name = card.name, + poster = card.poster, + episode = card.episode, + season = card.season, + id = card.id, + parentId = card.parentId, + rating = card.rating, + description = card.description, + cacheTime = System.currentTimeMillis(), ), null ) { when (it.action) { 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 fb5160a7..e185e75d 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 @@ -185,8 +185,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { } binding?.resultFullscreenHolder?.isVisible = !isSuccess && isFullScreenPlayer } - - //player_view?.apply { //alpha = 0.0f //ObjectAnimator.ofFloat(player_view, "alpha", 1f).apply { @@ -200,9 +198,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { // fillAfter = true //} //startAnimation(fadeIn) - // } - - + //} } private fun setTrailers(trailers: List?) { @@ -630,15 +626,15 @@ open class ResultFragmentPhone : FullScreenPlayer() { } downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - ep.name, - ep.poster, - 0, - null, - ep.id, - ep.id, - null, - null, - System.currentTimeMillis(), + name = ep.name, + poster = ep.poster, + episode = 0, + season = null, + id = ep.id, + parentId = ep.id, + rating = null, + description = null, + cacheTime = System.currentTimeMillis(), ), null ) { click -> 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 4285feb1..ac6527de 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 @@ -705,13 +705,13 @@ class ResultViewModel2 : ViewModel() { DOWNLOAD_HEADER_CACHE, parentId.toString(), VideoDownloadHelper.DownloadHeaderCached( - apiName, - url, - currentType, - currentHeaderName, - currentPoster, - parentId, - System.currentTimeMillis(), + apiName = apiName, + url = url, + type = currentType, + name = currentHeaderName, + poster = currentPoster, + id = parentId, + cacheTime = System.currentTimeMillis(), ) ) @@ -722,15 +722,15 @@ class ResultViewModel2 : ViewModel() { ), // 3 deep folder for faster acess episode.id.toString(), VideoDownloadHelper.DownloadEpisodeCached( - episode.name, - episode.poster, - episode.episode, - episode.season, - episode.id, - parentId, - episode.rating, - episode.description, - System.currentTimeMillis(), + name = episode.name, + poster = episode.poster, + episode = episode.episode, + season = episode.season, + id = episode.id, + parentId = parentId, + rating = episode.rating, + description = episode.description, + cacheTime = System.currentTimeMillis(), ) ) @@ -2776,13 +2776,13 @@ class ResultViewModel2 : ViewModel() { DOWNLOAD_HEADER_CACHE, mainId.toString(), VideoDownloadHelper.DownloadHeaderCached( - apiName, - validUrl, - loadResponse.type, - loadResponse.name, - loadResponse.posterUrl, - mainId, - System.currentTimeMillis(), + apiName = apiName, + url = validUrl, + type = loadResponse.type, + name = loadResponse.name, + poster = loadResponse.posterUrl, + id = mainId, + cacheTime = System.currentTimeMillis(), ) ) if (loadTrailers) 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 5b943105..66423982 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 @@ -25,7 +25,7 @@ object SearchHelper { SEARCH_ACTION_PLAY_FILE -> { if (card is DataStoreHelper.ResumeWatchingResult) { val id = card.id - if(id == null) { + if (id == null) { showToast(R.string.error_invalid_id, Toast.LENGTH_SHORT) } else { if (card.isFromDownload) { @@ -33,15 +33,15 @@ object SearchHelper { DownloadClickEvent( DOWNLOAD_ACTION_PLAY_FILE, VideoDownloadHelper.DownloadEpisodeCached( - card.name, - card.posterUrl, - card.episode ?: 0, - card.season, - id, - card.parentId ?: return, - null, - null, - System.currentTimeMillis() + name = card.name, + poster = card.posterUrl, + episode = card.episode ?: 0, + season = card.season, + id = id, + parentId = card.parentId ?: return, + rating = null, + description = null, + cacheTime = System.currentTimeMillis(), ) ) ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt index d1614bc1..30f66f83 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt @@ -3,17 +3,21 @@ package com.lagradost.cloudstream3.utils import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.TvType object VideoDownloadHelper { + abstract class DownloadCached( + @JsonProperty("id") open val id: Int, + ) + data class DownloadEpisodeCached( @JsonProperty("name") val name: String?, @JsonProperty("poster") val poster: String?, @JsonProperty("episode") val episode: Int, @JsonProperty("season") val season: Int?, - @JsonProperty("id") val id: Int, @JsonProperty("parentId") val parentId: Int, @JsonProperty("rating") val rating: Int?, @JsonProperty("description") val description: String?, @JsonProperty("cacheTime") val cacheTime: Long, - ) + override val id: Int, + ): DownloadCached(id) data class DownloadHeaderCached( @JsonProperty("apiName") val apiName: String, @@ -21,9 +25,9 @@ object VideoDownloadHelper { @JsonProperty("type") val type: TvType, @JsonProperty("name") val name: String, @JsonProperty("poster") val poster: String?, - @JsonProperty("id") val id: Int, @JsonProperty("cacheTime") val cacheTime: Long, - ) + override val id: Int, + ): DownloadCached(id) data class ResumeWatching( @JsonProperty("parentId") val parentId: Int, diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index 21f79ca6..a0b64ce3 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -59,12 +59,12 @@ Date: Mon, 24 Jun 2024 18:05:34 +0000 Subject: [PATCH 507/570] Improve tests (#1142) --- .../com/lagradost/cloudstream3/MainAPI.kt | 2 +- .../lagradost/cloudstream3/plugins/Plugin.kt | 1 + .../cloudstream3/plugins/PluginManager.kt | 3 +- .../ui/settings/testing/TestResultAdapter.kt | 50 ++++- .../ui/settings/testing/TestViewModel.kt | 2 +- .../cloudstream3/utils/ExtractorApi.kt | 2 +- .../cloudstream3/utils/TestingUtils.kt | 186 ++++++++++++------ app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 2 + 9 files changed, 172 insertions(+), 77 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 07a82583..91da2ed0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -622,7 +622,7 @@ abstract class MainAPI { /**Used for testing and can be used to disable the providers if WebView is not available*/ open val usesWebView = false - /** Determines which plugin a given provider is from */ + /** Determines which plugin a given provider is from. This is the full path to the plugin. */ var sourcePlugin: String? = null open val hasMainPage = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index 6b7dc90b..7f08af92 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -67,6 +67,7 @@ abstract class Plugin { * This will contain your resources if you specified requiresResources in gradle */ var resources: Resources? = null + /** Full file path to the plugin. */ var __filename: String? = null /** 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 a30af11c..a5631500 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -18,7 +18,6 @@ import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.removePluginMapping import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider @@ -518,7 +517,7 @@ object PluginManager { return true } - pluginInstance.__filename = fileName + pluginInstance.__filename = file.absolutePath if (manifest.requiresResources) { Log.d(TAG, "Loading resources for ${data.internalName}") // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt index 83480542..023ecb4c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt @@ -2,26 +2,31 @@ package com.lagradost.cloudstream3.ui.settings.testing import android.app.AlertDialog import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ProviderTestItemBinding import com.lagradost.cloudstream3.mvvm.getAllMessages import com.lagradost.cloudstream3.mvvm.getStackTracePretty +import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.TestingUtils +import java.io.File class TestResultAdapter(override val items: MutableList>) : AppUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProviderTestViewHolder( - ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent,false) + ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) //LayoutInflater.from(parent.context) // .inflate(R.layout.provider_test_item, parent, false), ) @@ -36,7 +41,8 @@ class TestResultAdapter(override val items: MutableList } + + api.sourcePlugin?.let { path -> + val pluginFile = File(path) + // Cannot delete a deleted plugin + if (!pluginFile.exists()) return@let + + builder.setNegativeButton(R.string.delete_plugin) { _, _ -> + ioSafe { + val success = PluginManager.deletePlugin(pluginFile) + + runOnMainThread { + if (success) { + showToast(R.string.plugin_deleted, Toast.LENGTH_SHORT) + } else { + showToast(R.string.error, Toast.LENGTH_SHORT) + } + } + } + } + } + + builder.show() } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt index 9e6f8a06..818f1fd7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt @@ -95,7 +95,7 @@ class TestViewModel : ViewModel() { providers.clear() updateProgress() - TestingUtils.getDeferredProviderTests(scope ?: return, apis, ::println) { api, result -> + TestingUtils.getDeferredProviderTests(scope ?: return, apis) { api, result -> addProvider(api, result) } } 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 1302453a..12b8837a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -1015,7 +1015,7 @@ abstract class ExtractorApi { abstract val mainUrl: String abstract val requiresReferer: Boolean - /** Determines which plugin a given extractor is from */ + /** Determines which plugin a given provider is from. This is the full path to the plugin. */ var sourcePlugin: String? = null //suspend fun getSafeUrl(url: String, referer: String? = null): List? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index dd973538..5e2b2bc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -13,16 +13,55 @@ object TestingUtils { } } - class TestResultSearch(val results: List) : TestResult(true) - class TestResultLoad(val extractorData: String) : TestResult(true) + class Logger { + enum class LogLevel { + Normal, + Warning, + Error; + } - class TestResultProvider(success: Boolean, val log: String, val exception: Throwable?) : + data class Message(val level: LogLevel, val message: String) { + override fun toString(): String { + val level = when (this.level) { + LogLevel.Normal -> "" + LogLevel.Warning -> "Warning: " + LogLevel.Error -> "Error: " + } + return "$level$message" + } + } + + private val messageLog = mutableListOf() + + fun getRawLog(): List = messageLog + + fun log(message: String) { + messageLog.add(Message(LogLevel.Normal, message)) + } + + fun warn(message: String) { + messageLog.add(Message(LogLevel.Warning, message)) + } + + fun error(message: String) { + messageLog.add(Message(LogLevel.Error, message)) + } + } + + class TestResultList(val results: List) : TestResult(true) + class TestResultLoad(val extractorData: String, val shouldLoadLinks: Boolean) : TestResult(true) + + class TestResultProvider( + success: Boolean, + val log: List, + val exception: Throwable? + ) : TestResult(success) @Throws(AssertionError::class, CancellationException::class) suspend fun testHomepage( api: MainAPI, - logger: (String) -> Unit + logger: Logger ): TestResult { if (api.hasMainPage) { try { @@ -31,22 +70,33 @@ object TestingUtils { api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages)) when { homepage == null -> { - logger.invoke("Homepage provider ${api.name} did not correctly load homepage!") + logger.error("Provider ${api.name} did not correctly load homepage!") } + homepage.items.isEmpty() -> { - logger.invoke("Homepage provider ${api.name} does not contain any items!") + logger.warn("Provider ${api.name} does not contain any homepage rows!") } + homepage.items.any { it.list.isEmpty() } -> { - logger.invoke("Homepage provider ${api.name} does not have any items on result!") + logger.warn("Provider ${api.name} does not have any items in a homepage row!") } } + val homePageList = homepage?.items?.flatMap { it.list } ?: emptyList() + return TestResultList(homePageList) } catch (e: Throwable) { - if (e is NotImplementedError) { - Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") - } else if (e is CancellationException) { - throw e + when (e) { + is NotImplementedError -> { + Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") + } + + is CancellationException -> { + throw e + } + + else -> { + e.message?.let { logger.warn("Exception thrown when loading homepage: \"$it\"") } + } } - logError(e) } } return TestResult.Pass @@ -54,11 +104,13 @@ object TestingUtils { @Throws(AssertionError::class, CancellationException::class) private suspend fun testSearch( - api: MainAPI + api: MainAPI, + testQueries: List, + logger: Logger, ): TestResult { - val searchQueries = listOf("over", "iron", "guy") - val searchResults = searchQueries.firstNotNullOfOrNull { query -> + val searchResults = testQueries.firstNotNullOfOrNull { query -> try { + logger.log("Searching for: $query") api.search(query).takeIf { !it.isNullOrEmpty() } } catch (e: Throwable) { if (e is NotImplementedError) { @@ -72,12 +124,11 @@ object TestingUtils { } return if (searchResults.isNullOrEmpty()) { - Assert.fail("Api ${api.name} did not return any valid search responses") + Assert.fail("Api ${api.name} did not return any search responses") TestResult.Fail // Should not be reached } else { - TestResultSearch(searchResults) + TestResultList(searchResults) } - } @@ -85,31 +136,27 @@ object TestingUtils { private suspend fun testLoad( api: MainAPI, result: SearchResponse, - logger: (String) -> Unit + logger: Logger ): TestResult { try { - Assert.assertEquals( - "Invalid apiName on SearchResponse on ${api.name}", - result.apiName, - api.name - ) + if (result.apiName != api.name) { + logger.warn("Wrong apiName on SearchResponse: ${api.name} != ${result.apiName}") + } val loadResponse = api.load(result.url) if (loadResponse == null) { - logger.invoke("Returned null loadResponse on ${result.url} on ${api.name}") + logger.error("Returned null loadResponse on ${result.url} on ${api.name}") return TestResult.Fail } - Assert.assertEquals( - "Invalid apiName on LoadResponse on ${api.name}", - loadResponse.apiName, - result.apiName - ) - Assert.assertTrue( - "Api ${api.name} on load does not contain any of the supportedTypes: ${loadResponse.type}", - api.supportedTypes.contains(loadResponse.type) - ) + if (loadResponse.apiName != api.name) { + logger.warn("Wrong apiName on LoadResponse: ${api.name} != ${loadResponse.apiName}") + } + + if (!api.supportedTypes.contains(loadResponse.type)) { + logger.warn("Api ${api.name} on load does not contain any of the supportedTypes: ${loadResponse.type}") + } val url = when (loadResponse) { is AnimeLoadResponse -> { @@ -117,39 +164,43 @@ object TestingUtils { loadResponse.episodes.keys.isEmpty() || loadResponse.episodes.keys.any { loadResponse.episodes[it].isNullOrEmpty() } if (gotNoEpisodes) { - logger.invoke("Api ${api.name} got no episodes on ${loadResponse.url}") + logger.error("Api ${api.name} got no episodes on ${loadResponse.url}") return TestResult.Fail } (loadResponse.episodes[loadResponse.episodes.keys.firstOrNull()])?.firstOrNull()?.data } + is MovieLoadResponse -> { val gotNoEpisodes = loadResponse.dataUrl.isBlank() if (gotNoEpisodes) { - logger.invoke("Api ${api.name} got no movie on ${loadResponse.url}") + logger.error("Api ${api.name} got no movie on ${loadResponse.url}") return TestResult.Fail } loadResponse.dataUrl } + is TvSeriesLoadResponse -> { val gotNoEpisodes = loadResponse.episodes.isEmpty() if (gotNoEpisodes) { - logger.invoke("Api ${api.name} got no episodes on ${loadResponse.url}") + logger.error("Api ${api.name} got no episodes on ${loadResponse.url}") return TestResult.Fail } loadResponse.episodes.firstOrNull()?.data } + is LiveStreamLoadResponse -> { loadResponse.dataUrl } + else -> { - logger.invoke("Unknown load response: ${loadResponse.javaClass.name}") + logger.error("Unknown load response: ${loadResponse.javaClass.name}") return TestResult.Fail } } ?: return TestResult.Fail - return TestResultLoad(url) + return TestResultLoad(url, loadResponse.type != TvType.CustomMedia) // val loadTest = testLoadResponse(api, load, logger) // if (loadTest is TestResultLoad) { @@ -174,7 +225,7 @@ object TestingUtils { private suspend fun testLinkLoading( api: MainAPI, url: String?, - logger: (String) -> Unit + logger: Logger ): TestResult { Assert.assertNotNull("Api ${api.name} has invalid url on episode", url) if (url == null) return TestResult.Fail // Should never trigger @@ -182,7 +233,7 @@ object TestingUtils { var linksLoaded = 0 try { val success = api.loadLinks(url, false, {}) { link -> - logger.invoke("Video loaded: ${link.name}") + logger.log("Video loaded: ${link.name}") Assert.assertTrue( "Api ${api.name} returns link with invalid url ${link.url}", link.url.length > 4 @@ -190,7 +241,7 @@ object TestingUtils { linksLoaded++ } if (success) { - logger.invoke("Links loaded: $linksLoaded") + logger.log("Links loaded: $linksLoaded") return TestResult(linksLoaded > 0) } else { Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") @@ -200,8 +251,9 @@ object TestingUtils { is NotImplementedError -> { Assert.fail("Provider has not implemented loadLinks()") } + else -> { - logger.invoke("Failed link loading on ${api.name} using data: $url") + logger.error("Failed link loading on ${api.name} using data: $url") throw e } } @@ -212,53 +264,57 @@ object TestingUtils { fun getDeferredProviderTests( scope: CoroutineScope, providers: Array, - logger: (String) -> Unit, callback: (MainAPI, TestResultProvider) -> Unit ) { providers.forEach { api -> scope.launch { - var log = "" - fun addToLog(string: String) { - log += string + "\n" - logger.invoke(string) - } - fun getLog(): String { - return log.removeSuffix("\n") - } + val logger = Logger() val result = try { - addToLog("Trying ${api.name}") + logger.log("Trying ${api.name}") // Test Homepage - val homepage = testHomepage(api, logger).success - Assert.assertTrue("Homepage failed to load", homepage) + val homepage = testHomepage(api, logger) + Assert.assertTrue("Homepage failed to load", homepage.success) + val homePageList = (homepage as? TestResultList)?.results ?: emptyList() // Test Search Results - val searchResults = testSearch(api) + val searchQueries = + // Use the first 3 home page results as queries since they are guaranteed to exist + (homePageList.take(3).map { it.name } + + // If home page is sparse then use generic search queries + listOf("over", "iron", "guy")).take(3) + + val searchResults = testSearch(api, searchQueries, logger) Assert.assertTrue("Failed to get search results", searchResults.success) - searchResults as TestResultSearch + searchResults as TestResultList // Test Load and LoadLinks // Only try the first 3 search results to prevent spamming val success = searchResults.results.take(3).any { searchResponse -> - addToLog("Testing search result: ${searchResponse.url}") - val loadResponse = testLoad(api, searchResponse, ::addToLog) + logger.log("Testing search result: ${searchResponse.url}") + val loadResponse = testLoad(api, searchResponse, logger) if (loadResponse !is TestResultLoad) { false } else { - testLinkLoading(api, loadResponse.extractorData, ::addToLog).success + if (loadResponse.shouldLoadLinks) { + testLinkLoading(api, loadResponse.extractorData, logger).success + } else { + logger.log("Skipping link loading test") + true + } } } if (success) { - logger.invoke("Success ${api.name}") - TestResultProvider(true, getLog(), null) + logger.log("Success ${api.name}") + TestResultProvider(true, logger.getRawLog(), null) } else { - logger.invoke("Error ${api.name}") - TestResultProvider(false, getLog(), null) + logger.error("Link loading failed") + TestResultProvider(false, logger.getRawLog(), null) } } catch (e: Throwable) { - TestResultProvider(false, getLog(), e) + TestResultProvider(false, logger.getRawLog(), e) } callback.invoke(api, result) } diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 7c9ccebe..a37dfad2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -88,4 +88,5 @@ #48E484 #ea596e + #FF9800 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b39006ad..f577d6e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -304,6 +304,7 @@ Start Failed Passed + Warning Resume -30 +30 @@ -609,6 +610,7 @@ plugins This will also delete all repository plugins Delete repository + Delete plugin Download the list of sites you want to use Downloaded: %d Disabled: %d From 0d40b5ebe3f6a88b2408149e4e44e3de8a8dfe91 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 24 Jun 2024 21:03:09 +0200 Subject: [PATCH 508/570] Translations update from Hosted Weblate (#1042) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aaditya Bhandari Co-authored-by: Adrian Hermida Co-authored-by: Akhlak Ur Rahman Co-authored-by: Alexander Svärd Co-authored-by: Anarchydr Co-authored-by: Andre Costa Co-authored-by: Antonio N Co-authored-by: Azgar Co-authored-by: Colgrave Co-authored-by: Dan Co-authored-by: Eji-san Co-authored-by: Ettore Atalan Co-authored-by: Evgeniy Khramov <65224669+thejenja@users.noreply.github.com> Co-authored-by: FUTURE Co-authored-by: Fikri Akbar Co-authored-by: Fjuro Co-authored-by: Huzaifah Asif Co-authored-by: Itsmechinmoy Co-authored-by: Jose Delvani Co-authored-by: Konstantinos Tranoudis Co-authored-by: Krisna A. Prayoga Co-authored-by: Luna712 <142361265+Luna712@users.noreply.github.com> Co-authored-by: Marian Turba Co-authored-by: Massimo Pissarello Co-authored-by: Matthaiks Co-authored-by: Milo Ivir Co-authored-by: Mæve Rey Co-authored-by: Naga Co-authored-by: Nicoara Alex Co-authored-by: Nuno Ferreira Co-authored-by: Only1337 Co-authored-by: Pizza Party Co-authored-by: Putra Iskandar Co-authored-by: Qareen Skoll Co-authored-by: Rex_sa Co-authored-by: SeMih Budur Co-authored-by: Semih Co-authored-by: Sufyan Zahoor Jutt Co-authored-by: Waheed Nazir Co-authored-by: Walter H Co-authored-by: Wei-Cheng Yeh (IID) Co-authored-by: amir Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: gallegonovato Co-authored-by: hugoalh Co-authored-by: ngocanhtve Co-authored-by: programutox Co-authored-by: rwi Co-authored-by: stojkovskistefan Co-authored-by: streaming s Co-authored-by: tuan041 Co-authored-by: user0020 <855309c256@gmail.com> Co-authored-by: ΣΤΑΥΡΟΣ ΔΑΛΙΑΚΟΠΟΥΛΟΣ Co-authored-by: Сергей (MrSabin) Co-authored-by: தமிழ்நேரம் Co-authored-by: 电棍老板 Co-authored-by: 구병우 --- app/src/main/res/values-ajp/strings.xml | 64 +- app/src/main/res/values-ar/strings.xml | 28 +- app/src/main/res/values-as/strings.xml | 624 ++++++++++++++++++ app/src/main/res/values-bg/strings.xml | 4 +- app/src/main/res/values-bn/strings.xml | 138 +++- app/src/main/res/values-bp/strings.xml | 23 +- app/src/main/res/values-cs/strings.xml | 23 +- app/src/main/res/values-de/strings.xml | 16 +- app/src/main/res/values-el/strings.xml | 109 ++- app/src/main/res/values-es/strings.xml | 41 +- app/src/main/res/values-fr/strings.xml | 33 +- app/src/main/res/values-hi/strings.xml | 19 +- app/src/main/res/values-hr/strings.xml | 414 ++++++------ app/src/main/res/values-hu/strings.xml | 4 +- app/src/main/res/values-in/strings.xml | 13 +- app/src/main/res/values-it/strings.xml | 25 +- app/src/main/res/values-iw/strings.xml | 4 +- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-ko/strings.xml | 115 +++- app/src/main/res/values-lv/strings.xml | 4 +- app/src/main/res/values-mk/strings.xml | 71 +- app/src/main/res/values-ml/strings.xml | 4 +- app/src/main/res/values-ms/strings.xml | 4 +- app/src/main/res/values-my/strings.xml | 4 +- app/src/main/res/values-ne/strings.xml | 49 +- app/src/main/res/values-nl/strings.xml | 4 +- app/src/main/res/values-no/strings.xml | 4 +- app/src/main/res/values-pl/strings.xml | 23 +- app/src/main/res/values-pt/strings.xml | 10 +- app/src/main/res/values-ro/strings.xml | 102 ++- app/src/main/res/values-ru/strings.xml | 10 +- app/src/main/res/values-sk/strings.xml | 26 +- app/src/main/res/values-so/strings.xml | 4 +- app/src/main/res/values-sv/strings.xml | 54 +- app/src/main/res/values-ta/strings.xml | 604 +++++++++++++++-- app/src/main/res/values-tr/strings.xml | 97 +-- app/src/main/res/values-uk/strings.xml | 23 +- app/src/main/res/values-ur/strings.xml | 151 +++-- app/src/main/res/values-vi/strings.xml | 32 +- app/src/main/res/values-zh-rTW/strings.xml | 84 ++- app/src/main/res/values-zh/strings.xml | 24 +- fastlane/metadata/android/as/changelogs/2.txt | 1 + .../metadata/android/as/full_description.txt | 10 + .../metadata/android/as/short_description.txt | 1 + fastlane/metadata/android/as/title.txt | 1 + .../android/el-GR/short_description.txt | 2 +- .../metadata/android/id/full_description.txt | 10 +- fastlane/metadata/android/ur/changelogs/2.txt | 2 +- .../metadata/android/ur/full_description.txt | 12 +- .../metadata/android/ur/short_description.txt | 2 +- .../metadata/android/zh-TW/changelogs/2.txt | 1 + .../android/zh-TW/full_description.txt | 10 + .../android/zh-TW/short_description.txt | 1 + fastlane/metadata/android/zh-TW/title.txt | 1 + 54 files changed, 2554 insertions(+), 587 deletions(-) create mode 100644 app/src/main/res/values-as/strings.xml create mode 100644 fastlane/metadata/android/as/changelogs/2.txt create mode 100644 fastlane/metadata/android/as/full_description.txt create mode 100644 fastlane/metadata/android/as/short_description.txt create mode 100644 fastlane/metadata/android/as/title.txt create mode 100644 fastlane/metadata/android/zh-TW/changelogs/2.txt create mode 100644 fastlane/metadata/android/zh-TW/full_description.txt create mode 100644 fastlane/metadata/android/zh-TW/short_description.txt create mode 100644 fastlane/metadata/android/zh-TW/title.txt diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index 734d5644..554fae9c 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -76,7 +76,7 @@ سَتِنگز الترجمة لون العلبة عمول ستريم للتورنت - تلقائيًا نَزِل كل الإضافات من الريپويات يلي نزادِت. + تلقائيًا نَزِل كل الإضافات من الريپويات يللي نزادِت. محي بلش في تِلِفونات ما فيا تعوز الطريقة الجديدة لتجديد الآپات. جربو \"الطريقة القديمة\" إذا ما عم تنزل التجديدات. @@ -90,7 +90,7 @@ متصفح الوَب كبوس مرتين على اليمين أو الشمال حتى تقرب أو ترَجِع الڤيديو ما نلاقى وصف الأحداث - الحلقة يلي بَعدا + الحلقة يللي بَعدها فرجي تجديدات الآپ رفّ آپ من نفس المطورين للروايات الخفيفة، بدل من الڤيديوات @@ -218,7 +218,7 @@ بث مباشر المشغل مبين - مدة التقديم مشكلة مش متوقع بمشغل الڤيديو (Unexpected player error) - بسبب أعطال إذا نحط على مستوى عالي كتير على الأجهزة يلي ما بتساع كتير، متل تلفزيون \"أندرويد\". + بسبِب أعطال إذا نحط على مستوى عالي كتير على الأجهزة يللي ما بتساع كتير، متل تلفزيون \"أندرويد\". شي غير أفي هيدا التجديد نسوخ الرابط @@ -244,7 +244,7 @@ طول التخزين المتوقت حلقة \"كروم كاست\" دراما آسيوية - بسبب أعطال إذا نحط على مستوى عالي كتير على الأجهزة يلي ذِكرتا زغيرة، متل تلفزيون \"أندرويد\". + بسبِب أعطال إذا نحط على مستوى عالي كتير على الأجهزة يللي ذِكرتها زغيرة، متل تلفزيون \"أندرويد\". مشكلة بالمصدر التخزين الموقت للڤيديو على الديسك فلم وثائقي @@ -283,7 +283,7 @@ إشارات المرجعية بَلَش التنزيل فتّو على الأكونت \"%s\" - وقِف الإعلان الأتوماتيكي عن المشاكل يلي بالآپ + وقِف الإعلان الأتوماتيكي عن المشاكل يللي بالآپ محل عنوان الپوستر الشكل %1$d ساعة %2$d ديقة @@ -314,7 +314,7 @@ مدبلج أوتوماتيك عدلو الأكونت - الأرقام السرية يلي نحطت مش صحيحة. جرب مرة أخرى. + الأرقام السرية يللي نحطت مش صحيحة. جرب مرة أخرى. الشكل المعمول للتلفزيون نلغى التنزيل أكونت @@ -366,9 +366,9 @@ زِدت %s المفضلين \"%s\" نزاد ع المفضل - العشوائي يلي بعدو + العشوائي يللي بعده خيال - عم نجدِد المثلثلات يلي مشتركينلا + عم نجدِد المثلثلات يللي مشتركينلها مبين إنو في عنصر متل هيدا موجود بالمكتبة عندكن: \n \n%s @@ -481,7 +481,7 @@ نزلو المصادر بالجملة فتاح بـ راينيگ - محي الريپو كمان بمحي الإضافات يلي في + محي الريپو كمان بمحي الإضافات يللي فيه اللغة شترك نمحت الإضافة @@ -490,7 +490,7 @@ الإضافات شيل الإعلانات من الترجمة رفّكن فاضي ☹ -\nفوتو على أكونت فيا رفّ الڤيديوات يلي حضرينا أو زيدو ڤيديوات بالرفّ المحلي. +\nفوتو على أكونت فيا رفّ الڤيديوات يللي حضرينها أو زيدو ڤيديوات بال رفّ المحلي. إسم الريپوزيتاري الجودات بيانات مش صالحة @@ -530,13 +530,13 @@ Blu-ray %d/10 النص كبير كتير. ما فينا ننسخو. - عدد الإضافيات يلي تجددت: %d + عدد الإضافيات يللي تجددت: %d رح يتجدد الآپ وقتا تطلعو مِنو پلاي ليست \"ايش أل أس\" زيد ريپوزيتوري علم إنو حضرتو شو بَدَك تشوف - شيل المعلومات يلي محطوطة بالترجمة ليلي عندن فقد سمعي + شيل المعلومات يللي محطوطة بال ترجمة ل يللي عندهن فقد سمعي ما لقينا ريپو. تأكدو إنو الرابط صح وجربو تعوزو \"ڤي پي أن\" (VPN) فشل الفوت ع الأكونت \"%s\" الحد الأعلى @@ -544,7 +544,7 @@ إضافات ما قدرنا نفتح %s رايتينگ: %s - بدكن تنزلو كل الإضافات من هيدا الريپو؟ + تحزير: \"كلاود ستريم 3\" مش مسؤولة عن الإضافات المش رسمية، وما بتدعمن أبدًا! الحالة محي الريپو مشغل الڤيديو @@ -567,8 +567,8 @@ الجودة عين الافتراضي المرجع (إختياري) - المشغل يلي بـ\"كلود ستريم\" - نزل لايحة المواقع يلي بدك تعوزن + المشغل يللي ب \"كلود ستريم\" + نزل لايحة المواقع يللي بدك تعوزهن حطو الأرقام السرية فَلتِر حسب اللغة المفضلة أكيد بدكون تطلعو؟ @@ -576,13 +576,13 @@ @string/home_play شيلو من لايحة المحتوى الحاضرينو الإعتمادات - فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يلي عندو أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يلي بتفضلوّا. + فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يللي عنده أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يللي بتفضلوها. \n -\nمثلًا: -\nإذا المصدر \"أ\" بتفضلوّ، بتعطوّ كتير نقات (مثلًا 8). -\nإذا الجودة 480 ما بتحبوّا، بتعطوّا نقات قليلة (مثلًا 1). +\nمتلًا: +\nإزا المصدر \"أ\" بتفضلوه، بتعطوه كتير نقات (متلًا 8). +\nإزا الجودة 480 ما بتحبوها، بتعطوها نقات قليلة (متلًا 1). \n -\nعلامت المصدر والجودة تبعو بينجمعو مع بعض (8 + 1 = 9). يلي علامتو 10 أو أعلى، بينحط تلقائيًا، من دون ما ينعمل لود لكل المصادر! +\nعلامت المصدر وال جودة تبعه بينجمعو مع بعض (8 + 1 = 9). يللي علامته 10 أو أعلى، بينحط تلقائيًا، من دون ما ينعمل لود لكل المصادر! حطو الأرقام السرية الحالية صوت حط كبسة لبرم إتجاه الشاشة @@ -603,7 +603,7 @@ قفل بواسطة المقاييس الحيوية رمز/كلمة مرور للمصادقة فتاح التطبيق باستعمال البصمة، آي دي الوج، پِن، النمط، إو الپاسورد. - تسَكرت هيدي الواجهة من ورا محاولات فاشلة عديدة. پليز، سكر الآپ ورجاع فتحه. + بعد كذا محاولة فاشلة، هيدا الشباك رح يسكر. بكل بساطة، سكر الآپ ورجاع فتحه حتى تجرب بعد مرة. %s \nباقي المصادقة البيومترية مش مدعومة ع هالجهاز @@ -621,5 +621,23 @@ موسيقى أوديو بوك الميديا - لتضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يلي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي بـ الباكگروند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". ملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآن بـ الباكگروند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ بـ«الإعدادات العامة» \"General settings\"، إزا غيرت رأيك. - + ت تضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يللي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي ب الباكگراوند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". +\nملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآپ بال باكگراوند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ ب «الإعدادات العامة» \"General settings\"، إزا غيرت رأيك. + ريسات + رح ينزل ب %s + الحلقة ال %2$d من الجزء ال%1$d رح تنزل ب + كاست مراية + إف كاست + نقي جهاز الكاست + ويكي \"كلود ستريم\" + أكونتات + سكوريتي + صورة الـ\"كيو آر\" كود + فشلنا ب فتح پِن الجهاز، جرب تفوت ع أكونت محليًا + خلصت مدة الپِن! + بتخلص مدة الپِن ب %1$d دقايق و%2$d ثانية + فوت عال أكونت محليًا + تجاهل + متاح الريپوزيتوري + فتاح %s ع تلفونك أو كمپيوترك، وحط الكود اللي فوق + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 8681398d..e253ed93 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -220,7 +220,7 @@ أوفا تورنت وثائقي - دراما آسيوية + الدراما الآسيوية بث حي +18 فيديو @@ -435,7 +435,7 @@ عرض مستودعات المجتمع قائمة عامة جميع الترجمات حروف كبيرة - تحميل جميع الإضافات من هذا المستودع\? + تحذير: لا يتحمل CloudStream 3 أي مسؤولية عن استخدام ملحقات الطرف الثالث ولا يقدم أي دعم لها! %s (معطل) المسارات مسار الصوت @@ -631,7 +631,7 @@ افتح التطبيق باستخدام بصمة الإصبع ومعرف الوجه ورقم التعريف الشخصي والنمط وكلمة المرور. فتح سحابة البث مصادقة كلمة المرور/رقم التعريف الشخصي - تم إغلاق هذه الشاشة بسبب عدة محاولات فاشلة. الرجاء إعادة تشغيل التطبيق. + بعد عدة محاولات فاشلة، سيتم إغلاق المطالبة. ما عليك سوى إعادة تشغيل التطبيق للمحاولة مرة أخرى. لقد تم الآن نسخ بيانات CloudStream احتياطيًا. على الرغم من أن احتمال حدوث ذلك منخفض جدًا، إلا أن جميع الأجهزة يمكن أن تتصرف بشكل مختلف. في الحالات النادرة، التي يتم فيها منعك من الوصول إلى التطبيق، قم بمسح بيانات التطبيق بالكامل واستعادتها من نسخة احتياطية. نحن نأسف جدًا لأي إزعاج ناتج عن هذا. %s \nمتبقي @@ -646,8 +646,24 @@ غير قادر على فتح معلومات تطبيق CloudStream. كتاب صوتي حسناً - لضمان عدم انقطاع التنزيلات والإشعارات للبرامج التلفزيونية المشتركة، يحتاج CloudStream إلى إذن للتشغيل في الخلفية. بالضغط على موافق، سيتم توجيهك إلى معلومات التطبيق. هناك، انتقل إلى استخدام بطارية التطبيق -\nواضبط استخدام البطارية على غير مقيد. يرجى ملاحظة أن هذا الإذن لا يعني أن CS3 سوف يستنزف البطارية. ولن يعمل إلا في الخلفية عند الضرورة، كما هو الحال عند تلقي الإشعارات أو تنزيل مقاطع الفيديو من الملحقات الرسمية. إذا اخترت الإلغاء، فيمكنك ضبط هذا الإعداد لاحقًا في الإعدادات العامة. + لضمان عدم انقطاع التنزيلات والإشعارات للبرامج التلفزيونية المشتركة، يحتاج CloudStream إلى إذن للتشغيل في الخلفية. بالضغط على موافق، سيتم توجيهك إلى معلومات التطبيق. هناك، انتقل إلى 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 واضبط استخدام البطارية على 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. يرجى ملاحظة أن هذا الإذن لا يعني أن CS3 سوف يستنزف البطارية. ولن يعمل إلا في الخلفية عند الضرورة، كما هو الحال عند تلقي الإشعارات أو تنزيل مقاطع الفيديو من الامتدادات الرسمية. إذا اخترت الإلغاء، فيمكنك ضبط هذا الإعداد لاحقًا في 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. موسيقى الوسائط - + اعادة تعيين + قادم خلال %s + سيتم إصدار الحلقة %1$d من الموسم %2$d في + مرآة البث + بث ف + حدد جهاز البث + CloudStream ويكي + إعدادات الأمان + الحسابات + صورة رمز الاستجابة السريعة + تجاهَل + فتح المستودع + لقد انتهت صلاحية الرمز السري الآن! + تحقق محليا + قم بزيارة %s على هاتفك الذكي أو جهاز الكمبيوتر وأدخل الرمز أعلاه + لا يمكن الحصول على رمز PIN للجهاز، حاول المصادقة المحلية + تنتهي صلاحية الرمز خلال %1$dm %2$ds + \ No newline at end of file diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml new file mode 100644 index 00000000..8d151d71 --- /dev/null +++ b/app/src/main/res/values-as/strings.xml @@ -0,0 +1,624 @@ + + + %1$s এপি %2$d + পোস্টাৰ + এপিস\'দ পোস্টাৰ + মুখ্য পোস্টাৰ + পিছলৈ যাওক + ৰেটিং: %.1f + ছেটিংছ + অনুসন্ধান কৰক… + %s ত অনুসন্ধান কৰক… + ডাউনল\'ড কৰি আছে + পৰামৰ্শ চাওক + প্লেয়াৰৰ গতি + সাবটেক্সটৰ ৰং + এজেৰ ধৰণ + চালনালৈ অবন্ধ কৰক + প্লট + লগ ক্যাট চাওক + লগ + বেকআপ ত্ৰুটি %s + অনুসন্ধান + গ্ৰন্থাগাৰ + হিসাব আৰু সুৰক্ষা + উন্নয়ন আৰু বেকআপ + স্বয়ংক্ৰিয়ভাৱে প্লাগইন ডাউনল\'ড কৰক + প্লাগইন ডাউনল\'ড ম’ড নিৰ্বাচন কৰক + নিষ্ক্ৰিয়ত আৰম্ভকৰ্তাৰ আদৰ্শ সংগ্ৰহসমূহতৰ প্ৰয়াস হৈছে, আইন্সটল স্বয়ংক্ৰিয়ভাৱে যোগ কৰা হয়। + কাৰ্টুনসমূহ + এনিমে + টৰেণ্টসমূহ + OVA + এছিয়ান ড্ৰামা + লাইভস্ট্ৰিম + NSFW + অন্যান্য + এপত বাজাওক + লিংকসমূহ পুনঃ ল\'ড কৰক + সাবটাইটেল ডাউনল\'ড কৰক + HD চিহ্ন + শিৰোনাম + কোনো উন্নীতকৰণ পোৱা নাই + উন্নীতকৰণ পৰীক্ষা কৰক + আৰম্ভ কৰক না + ভিডিঅ\' প্লেয়াৰৰ শিৰোনামৰ সীমা + বিদ্যমান চাবৰ এটা ক্লোন যোগ কৰক, একটু ভিন্ন urlৰ সৈতে + লিংকসমূহ + আউটলৈন + স্বয়ংক্ৰিয় + password123 + ব্যৱহাৰকাৰীনাম + hello@world.com + 127.0.0.1 + লগ আউট + সমন্বয় সংখ্যা + %d / 10 + /?? + /%d + %s প্ৰমাণিত + %s ত লগ ইন কৰিব পৰা নহয় + অবৈধ ডেটা + অবৈধ url + ত্ৰুটি + সাবটাইটেলৰ পৰা ক্যাপশনসমূহ দূৰ কৰক + উল্লেখক (ঐচ্ছিক) + পৰৱৰ্তী + প্ৰদাতাৰ ভাষা পছন্দ কৰক + গভেয়ান ডাউনলোড + সেটআপ এক্সটেনশনসমূহ + ডাউনলোড হৈছিল: %d + ক্লিপবোৰ্ড অনুমতি দিবলৈ ত্ৰুটি, অনুগ্ৰহ কৰি পুনৰ চেষ্টা কৰক। + বৰ্ণানুক্রমিক (জ থকা অ পৰা অ) + %d প্ৰকাৰ মুকলো! + সাবস্ক্ৰাইব কৰক + সাবস্ক্ৰাইব পৰা মুকা + প্ৰ’ফাইল %d + পিন এন্টাৰ কৰক + একাউন্ট সম্পাদনা কৰক + মিডিয়া + পুনৰায় ছেট কৰক + অভিনেতা: %s + দেখা হৈ আছে + অবিৰাম দেখিব + সাবটাইটেল + চিত্ৰমূল্যৰ সিষ্টেম ব্যৱহাৰ কৰক + বেকআপ অনুমতি অনুপলব্ধ। অনুগ্ৰহ কৰি পুনৰায় চেষ্টা কৰক। + অল্প + এচডি + সমাপ্ত + সাৰদাৰী ভাণ্ডা + অব্যাহত্‌ + VLCত বাজাওক + বেটাৰী অপটিমাইজেশন অসমৰ্থ + সাবস্ক্ৰাইব কৰা বস্তুসমূহ + মান + যোগ কৰক + পুনৰ্স্থাপন কৰক + ক্লাউডষ্ট্ৰীম অনলক কৰক + বায়োমেট্ৰিক ছেটিংসমূহ + পাছৱাৰ্ড/PIN পুনৰ্প্ৰমাণৰ + সংগীত + অডিঅ’ বুক + পৰৱৰ্তী পৰ্ব %d + পৰৱৰ্তী মৌচৰা %1$d পৰ্ব %2$d + %1$dd %2$dh %3$dm + %1$dh %2$dm + %dm + পোস্টাৰ + প্ৰদাতা সলনি কৰক + পূৰ্বালোচনাৰ পৃষ্ঠপুতী + পৰৱৰ্তী যিনিমূলক + গতি (%.2fx) + পৰৱৰ্তী পৰ্ব + %d মিনিট + ক্লাউডষ্ট্ৰীম + মূল + কোনো ডাটা নাই + অধিক বিকল্পসমূহ + শেয়াৰ কৰক + ব্ৰাউজাৰত খোলক + ব্ৰাউজাৰ + দেখিব প্লেন কৰা হৈছে + টৰেণ্ট ষ্ট্ৰীম কৰক + সংযোগ পুনঃচেষ্টা কৰক… + পিছলৈ যাওক + উৎস বাছনি কৰক + ডাউনলোড হোৱা + ডাউনলোড থমা কৰা হৈছে + ডাউনলোড আৰম্ভ হোৱা + ডাউনলোড বিফল হৈছে + ডাউনলোড বাতিল হৈছে + ডাউনলোড সম্পন্ন হৈছে + আপডেট আৰম্ভ হোৱা + নেটৱৰ্ক ষ্ট্ৰীম + লিংক লোড কৰা বিফল + লিংক পুনৰাবৃত্তি হোৱা + অভ্যন্তৰীণ ষ্ট’ৰেজ + ডাব + ছাবটাইটেল + ফাইল মুছি দিব + ফাইল প্লে কৰক + স্বয়ংক্ৰিয় বাগৰ প্ৰতিবেদন নিষ্ক্ৰিয় কৰক + ডাউনলোড পুনৰাৰম্ভ কৰক + ডাউনলোড থমা কৰক + অধিক তথ্য + লুকুৱা কৰক + খেলা + তথ্য + অপশনটো সংগ্ৰহ বাদ কৰক + সংগ্ৰহৰ তলত যোগ কৰক + সংগ্ৰহৰ ফিল্টাৰ কৰক + সংগ্ৰহ ত্ৰুটি + কপি কৰক + বন্ধ কৰক + পৰিষ্কাৰ কৰক + সংৰক্ষণ কৰক + ভৰসৰ্বক্ষণৰ নাম আৰু URL + প্ৰতিলিপি কৰা হৈছে! + নতুন সদস্যতা + অন্যান্য এক্সটেনছনসমূহত অনুসন্ধান কৰক + সাবটাইটেল ছেটিংছ + আউটলাইনৰ ৰং + পৃষ্ঠপুতীৰ ৰং + উইণ্ডোৰ ৰং + সাবটাইটেলৰ উঁচুতি + ফন্ট + ফন্ট আকাৰ + প্ৰকাৰসমূহে অনুসন্ধান কৰক + সৰব সাধনসমূহে অনুসন্ধান কৰক + %d ডেভল\'পাৰ পৰা বেনেন দিয়া হৈছে + কোনো বেনেন দিয়া নাই + স্বয়ংক্ৰিয় ভাষা বাছনি কৰক + ভাষা ডাউনলোড কৰক + ছাবটাইটেলৰ ভাষা + ডিফল্টৰ পুনৰাৰম্ভ কৰিবলৈ ধাৰণ কৰক + এই প্ৰদাতাৰ সঠিকভাবে কাম কৰিবলৈ এটা VPN প্ৰয়োজন হবে + এই প্ৰদাতা এটা ট\'ৰেণ্ট হৈছে, এটা VPN পৰামৰ্শ দিয়া হল + ফন্ট ইম্প\'ৰ্ট কৰিবলৈ %sত ত থকা প্ৰতিষ্ঠান কৰক + অধিক তথ্য + \@string/home_play + মেটাডাটা ঠিকানাৰ সৈতে প্ৰদান কৰা নাই, যদি এটা ঠিকানাৰ মেটাডাটা নাই তেন্তি ভিডিঅ\' ল\'ড হোৱা নাই। + কোনো বিবৰণ পোৱা নাই + কোনো বিৱৰণ পোৱা নাই + প্লেয়াৰৰ আকাৰ বটাম + কৃষ্টি বোৰ্ডাৰ প্ৰস্তুতি কৰক + ছবি-মাধ্যমে ছবি + অন্য এপ্‌সমূহৰ ওপৰত এটা সন্নিহিত প্লেয়াৰত অবিৰত প্লে কৰা হৈছে + প্লেয়াৰৰ ছাবটাইটেল ছেটিংছ + ক্ৰ\'মকাস্ট ছাবটাইটেল + ক্ৰ\'মকাস্ট ছাবটাইটেল ছেটিংছ + প্লেবেক গতি + প্লেয়াৰত গতিৰ বিকল্প যোগ কৰে + সোধক হওৱাৰ বাবে স্বাইপ কৰক + ভিডিঅ\' ত আপোনাৰ অৱস্থান নিয়ন্ত্ৰণ কৰিবলৈ পাছত থকা দিশত স্বাইপ কৰক + ছেটিংছ সলনী কৰিবলৈ স্বাইপ কৰক + প্ৰকাৰ বা আওতাৰ স্থানত ওপৰত অথবা তলত স্বাইপ কৰক উজ্জ্বলতা অথবা ভলিউম সলনী সলন কৰিবলৈ + স্বয়ংক্ৰিয় পৰবৰ্তী সংযোগ + বর্তমান এটা শেষ হোৱা সময় পৰবৰ্তী সংযোগ আৰম্ভ কৰা + দ্বিগুন টেপ কৰি সন্ধান কৰক + দ্বিগুন টেপ কৰি থম কৰক + প্লেয়াৰ সন্ধান পৰিমাণ (ছেকেন্ড) + অগ্ৰগামী বা পিছত সন্ধান কৰিবলৈ সোহদৰ বা বাঁয়া দিকত দুইবাৰ টেপ কৰক + থমবা নিছবা পৰিমাণ দুইবাৰ টেপ কৰক + অ্যাপ প্লেয়াৰত সিষ্টেমৰ আলোক অভিলাই ব্যৱহাৰ কৰিবলৈ বিষ্টা উলঙ্ঘনস্থিতি ব্যৱহাৰ কৰক + এপিস\'ড সংমিলিত কৰা + আপোনাৰ বৰ্তমান পৰ্বৰ প্ৰগতি স্বয়ংক্ৰিয়ভাবে সমতলীয়া কৰা + বেকআপৰ তথ্য পুনৰায় স্থাপন কৰক + তথ্য বেকআপ কৰক + বেকআপ সংখ্যা + ফাইল %sৰ পৰা তথ্য পুনৰায় স্থাপন কৰা নহয় + তথ্য সংৰক্ষিত হৈছে + তথ্য + বেকআপ ফাইল ল\'ড হৈছে + সুধাৰি অনুসন্ধান + প্ৰদানকাৰীৰ পৰা সন্ধান ফলাফল সোমোৱা + কেৱল দুৰ্ঘটনাত ডাটা পঠাৱ + কোনো ডাটা পঠাৱ নহয় + অনুপ্রেৰণাৰ অধ্যাপক দেখাওঁক + ট্ৰেলাৰ দেখাওঁক + Kitsuৰ পোষ্টাৰ দেখাওঁক + অনুসন্ধান ফলাফলত নিৰ্বাচিত ভিডিঅ’ গুণত্ব লুকাওক + স্বয়ংক্ৰিয় প্লাগইন উন্নয়ন + এপ্লিকেশন উন্নয়ন দেখাওঁক + সেটআপ প্ৰক্ৰিয়া পুনৰাবৃত্তি কৰক + প্ৰি-ৰিলিজ আপডেট কৰক + APK Installer + কিছু ফ’নসমূহ নতুন পেকেজ ইনষ্টলাৰ সমৰ্থন কৰে নাই। আপডেট ইনষ্টল নহয় পৰা পৰীক্ষা কৰক লেগেচি বিকল্প প্ৰয়োগ কৰক। + গিটহাব + একেই ডেভল\'পাৰকৰ লাইট নভেল এপ্‌ + ডিসকৰ্ডত যোগদান কৰক + কোনো লিংক পোৱা নাই + ডেভল\'পাৰকলৈ এখন বিনম্রতা দাওঁক + দিয়া বিনম্রতা + এপ্‌ ভাষা + এই প্ৰদাতাৰ কোনো চ্ৰোমকাছ্ট সমৰ্থন নাই + লিংক ক্লিপবৰ্ডত প্ৰতিলিপি কৰা হৈছে + এপিস\'ড বাজাওঁক + দুঃখিত, অ্যাপ্লিকেশন সংঘটিত হৈছে। অজ্ঞাত বাগ ৰিপ’ৰ্ট ডেভেলপাৰসমূহলৈ পঠাওক + ঋতু + ডিফ’ল্ট মান পুনৰাবৃত্তি কৰক + %1$s %2$d%3$s + কোনো ঋতু নাই + এপিস\'ড + এপিস\'ডসমূহ + %1$d-%2$d + %1$d %2$s + %sত আগবাঢ়িছে + ঋতু + এপিস\' + কোনো এপিস\'ড পোৱা নাই + ফাইল মচি দিব + মচি দিব + বাতিল কৰক + অধিপ্ত কৰক + আৰম্ভ কৰক + অসফল + সফল + আবাৰ আৰম্ভ কৰক + -৩০ + +৩০ + এইটো স্থায়ীভাৱে %s মচি দিব +\nআপুনি নিশ্চিত? + %dm +\nবাকি আছে + %s +\nবাকি আছে + চলিত আছে + সম্পন্ন হৈছে + অবস্থা + বছৰ + ৰেটিং + সময় + চাইট + সাৰাংশ + অপেক্ষাৰত + সাবটাইটেল নাই + ডিফ’ল্ট + মুক্ত + ব্যৱহাৰ কৰা + এপ্‌ স্টোৰেজ + চলচ্চিত্ৰসমূহ + টিভি চলচ্চিত্ৰসমূহ + ডকুমেণ্টাৰিসমূহ + এছিয়ান ড্ৰামা + লাইভস্ট্ৰিমসমূহ + এনএসএফডবলিউ + অন্যান্য + চলচ্চিত্ৰ + সিৰিজ + কাৰ্টুন + অ্যানিমে + OVA + টৰেণ্ট + ডকুমেণ্টাৰী + উৎস ত্ৰুটি + দূৰবৰ্তী ত্ৰুটি + ৰেন্ডাৰাৰ ত্ৰুটি + ডাউনল\'ড ত্ৰুটি, সংৰক্ষণ অনুমতিসমূহ পৰীক্ষা কৰক + অপ্ৰত্যাশিত প্লেয়াৰ ত্ৰুটি + চ্ৰোমকাছ্ট এপিস\'ড + চ্ৰোমকাছ্ট আইনমিৰৰ + আইনমিৰ আদৰণি + ডাব চিহ্ন + %sত বাজাওক + ব্ৰাউজাৰত বাজাওক + লিংক প্ৰতিলিপি কৰক + স্বয়ংক্ৰিয় ডাউনল\'ড + আদৰণি ডাউনল\'ড কৰক + উপশিৰোনিৰ চিহ্ন + প\'ষ্টাৰৰ UI উপাদানসমূহ ট\'গল কৰক + ল\'ক + ৰিসাইজ + উৎস + আপডেট + পছন্দসই দেখাৰ গুণত্ব (WiFi) + অপ স্কিপ কৰক + এই আপডেটটো প্ৰস্থান কৰক + পছন্দসই দেখাৰ গুণত্ব (মোবাইল ডেটা) + ভিডিঅ\' প্লেয়াৰৰ আৰুণিমা + ভিডিঅ\' বাফাৰৰ আকাৰ + ভিডিঅ\' বাফাৰৰ লম্বা + ডিস্কত ভিডিঅ\' কেচ কৰক + ভিডিঅ\' আৰু চিত্ৰ কেচ মুক্ত কৰক + প্লেয়াৰ দৃশ্যমান হ\'লে ব্যৱহাৰ কৰা সন্ধান পৰিমাণ + প্লেয়াৰ লুকুৱাওলৈ - সন্ধান পৰিমাণ + প্লেয়াৰ দেখা হোৱালৈ - সন্ধান পৰিমাণ + প্লেয়াৰ লুকুৱা হ\'লে ব্যৱহাৰ কৰা সন্ধান পৰিমাণ + যদি স্তন্যপান উচ্চত নিৰ্ধাৰিত হ\'লে, অন্যত্ৰ সঞ্চয় স্থানত সন্দেহ ঘটিব। উদাহৰণস্বৰূপ, এছিয়ান টিভি। + DNS ওভাৰ HTTPS + যদি স্তন্যপান উচ্চত নিৰ্ধাৰিত হ\'লে, অন্যত্ৰ মেম\'ৰী সন্ধানৰ যন্ত্ৰসমূহত সন্দেহ ঘটিব। উদাহৰণস্বৰূপ, এছিয়ান টিভি। + আইএছপি ব্লক পাৰ কৰিবলৈ কাৰ্যকাৰী + চাব চাওঁক + GitHub প্ৰক্সি + GitHub প্ৰাপ্য নাই। jsDelivr প্ৰক্সি অন কৰা হচ্ছে… + jsDelivr ব্যৱহাৰ কৰি সোধ গুগল url ব্লক অনলাইন কৰক। কিছুদিনৰ মাজত আপডেট ল\'ওক দিব পাৰে। + চাব আঁতৰাওক + ডাউনল\'ড পাথ + NGINX চাৰ্ভাৰ url + ডাব কৰা/সাব কৰা অ্যানিমে প্ৰদৰ্শন কৰক + স্ক্ৰীনলৈ সজাব কৰক + পৰবৰ্তী কৰক + অকৃত্রিম নোটিশ + আইএছপি পাৰ কৰা + জুম কৰক + এপ্ আপডেটসমূহ + বেকাপ + এক্সটেনচনসমূহ + ক্ৰিয়াসমূহ + কেচ + এণ্ড্ৰোইড টিভি + হাস্য + প্লেয়াৰৰ বৈশিষ্ট্য + উপশিৰোতা + লে\'আ\'উট + ডিফ’ল্টসমূহ + দেখুৱাই + বৈশিষ্ট্যসমূহ + সাধাৰণ + যিতা বুটাম + হোমপেজ আৰু লাইব্ৰেৰিত যিতা বুটাম দেখোৱা + সুপালৈক ভাষাসমূহ + এপ্ লে\'আ\'উট + পছন্দসই মিডিয়া + সুবিধা দিয়া সময় NSFW সক্ষম কৰক + উপশিৰোতা ক\'ডিং + প্ৰদাতা + প্ৰদাতা পৰীক্ষা + সকলো এক্সটেনচনসমূহ পৰীক্ষা কৰক + এই পৰীক্ষা কেৱল উন্নীতকাৰীসমূহলৈ পৰমিট আৰু বাৰ্তা কৰা হৈছে আৰু কেইকোনো এক্সটেনচনৰ কাৰ্যক্ষম বা অকাৰ্যক্ষমতা প্ৰমাণিত নহয়। + টিভি লে\'আ\'উট + এমুলেটৰ লে\'আ\'উট + প্ৰাথমিক ৰং + এপ্ থীম + প\'ষ্টাৰ শিৰোনাম অৱস্থান + ফোন লে\'আ\'উট + প\'ষ্টাৰ তলত শিৰোনাম দিয়ক + নতুন সাইটৰ নাম + https://example.com + ভাষা ক\'ড (en) + %1$s %2$s + একাউন্ট + লগ ইন + একাউন্ট পৰিবৰ্তন কৰক + একাউন্ট যোগ কৰক + একাউন্ট সৃষ্টি কৰক + ট্ৰেকিং যোগ কৰক + কোনোবিলাক + সাধাৰিত + %s যোগ কৰা হৈছে + সিঙ্ক + অক্ষম কৰক + সকল + অধিকতম + আউটলাইন + ডিপ্ৰেছড + ছাড়া + 1000 মিলিছেকেন্ড + উঁচুহোৱা + উপশিৰোতা সিংক + উপশিৰোতা দেলাই + যদি উপশিৰোতা %d মিলিছেকেন্ড পূৰ্ববৰ্তী দেখা যায় তেন্ত এইটো ব্যৱহাৰ কৰক + কোনো উপশিৰোতা বিলম্ব নাই + যদি উপশিৰোতা %d মিলিছেকেন্ড পূৰ্ববৰ্তী দেখা যায় না তেন্ত এইটো ব্যৱহাৰ কৰক + দ্রুত মাৰেল ভুঁটি সোমালী কুকুৰা ধীৰে + অনুমোদিত + %s লোড কৰা হৈছে + ফাইলৰ পৰা লোড কৰক + ইন্টাৰনেটৰ পৰা লোড কৰক + পছৱা + ডাউনলোড কৰা ফাইল + উৎস + প্ৰধান + যাত্রা + শীঘ্ৰই আসিব… + সহায়ক + ক্যাম + ক্যাম + ক্যাম + এইচকিউ + এইচডি + টিএছ + ব্লু-ৰে + ডব + টিসি + ডিভিডি + ৪কে + ইউএচডি + এইচডিৰ + এসডিৰ + ওয়েব + পোষ্টাৰ ছবি + প্লেয়াৰ + দৰ্শনীয়তা আৰু শীৰ্ষক + শীৰ্ষক + দৰ্শনীয়তা + অবৈধ আইডি + পছন্দৰ মিডিয়া ভাষাৰ দ্বাৰা ফিল্টাৰ কৰক + সাবটাইটেলত ব্লোট দূৰ কৰক + অতিৰিক্ত + ট্ৰেলাৰ + ক্ৰেশ ৰিপ\'টিং + https://example.com/example.mp4 + পূৰ্বৱৰ্তী + ছেটআপ প্ৰস্থান কৰক + আপোনাৰ ডিভাইচ অনুযায়ী এপ্পৰ প্ৰস্তুতি সলনি কৰক + আপুনি কি চাব + এক্সটেনছনসমূহ + প্ৰায়ণশাল যোগ কৰক + প্ৰায়ণশালৰ নাম + প্ৰায়ণশাল URL + প্লাগইন লোড হোৱা হৈছে + প্লাগইন ডাউনলোড হোৱা হৈছে + প্লাগইন মুছিলো + %s লোড কৰিব পৰা নহয় + ১৮+ + %1$d %2$s ডাউনলোড আৰম্ভ কৰা হ’ল… + %1$d %2$s ডাউনলোড কৰা হ’ল + সকলো %s ইতিমধ্যে ডাউনলোড কৰা হৈছে + ৰিপ’চ্যুটৰীত কোনো প্লাগইন পোৱা নাই + প্লাগইন + প্লাগইনসমূহ + ৰিপ’চ্যুটৰি পোৱা নাই, URL চেক কৰক আৰু VPN চেক কৰক + এইটো আৰুও ৰিপ’চ্যুটৰীত সকলো প্লাগইন মুছিব + ৰিপ’চ্যুটৰি মুছিব + %d প্লাগইন আপডেট কৰা হ’ল + নিষ্ক্ৰিয় কৰা হ’ল: %d + ডাউনলোড হোৱা নাই: %d + ক্লাউডস্ট্ৰীমত ডিফ’ল্টত কোনো বেছি সাইট ইনষ্টল কৰা নাই। আপোনাৰ এটি পৰিৱেশনত থকা সাইটসমূহ ইনষ্টল কৰিব লাগিব। +\n +\nআমাৰ ডিসক’ৰ্ডত যোগদান কৰক অথবা অনলাইনত সন্ধান কৰক। + সম্প্রদায়ৰ প্ৰায়ণশালসমূহ চাওক + সকলো সাবটাইটেলৰ মাজতে আপাৰ আকাৰত লিখক + এই প্ৰায়ণশালৰ সকলো প্লাগিন ডাউনলোড কৰিব? + %s (অক্ষম কৰা আছে) + পথসমূহ + অডিঅ’ পথসমূহ + ভিডিঅ’ পথসমূহ + প্ৰযোগ সলনি কৰিবলৈ এপ্‌লিকেছন পুনৰ আৰম্ভ কৰক। + পুনৰ আৰম্ভ কৰক + নিৰাপদ প্ৰণামী চালু + এপ্প সমস্থ একটি দ্বিঘাতৰ ফলে সমস্থ এক্সটেনছন নিষ্ক্ৰিয় কৰা হৈছিল যিতে আপোনাক কিবা সমস্যা আছে তা চেনা নিব পাৰে। + দ্বিঘাতৰ তথ্য চাওক + ৰেটিং: %s + বিৱৰণ + সংস্কৰণ + অৱস্থা + আকাৰ + লেখক + সমৰ্থিত + ভাষা + শীৰ্ষকত প্লাগিনটো ইনষ্টল কৰক + এপ্‌লিকেছন পোৱা নহয় + সকলো ভাষা + %s অপচাৰ কৰক + ওপনিং + শেষ + পুনৰাবৃত্তি + HLS প্লেলিষ্ট + পছন্দৰ ভিডিঅ’ প্লেয়াৰ + আইণ্টাৰনেল প্লেয়াৰ + MPV + Web ভিডিঅ’ কাষ্ট + Fcast + Web ব্ৰাউজাৰ + কাষ্ট ডিভাইচ নিৰ্বাচন কৰক + মিছিণ শেষ + মিছিণ আৰম্ভ + ইতিহাস মুকা + শ্ৰেয়া + প্ৰস্তাবনা + ইতিহাস + ডাটাবেছত প্ৰবেশ/শেষৰ বাবে পপাপ দেখাওক + বেছি পাঠ্য। ক্লিপব’ৰ্ডত সংৰক্ষণ কৰিব নোৱা যায়। + কপি কৰিবলৈ ত্ৰুটি, অনুগ্ৰহ কৰি লগকেট কপি কৰি অ্যাপ সমৰ্থনকৰ্তাৰ সৈতে যোগাযোগ কৰক। + চাওক হিচাপে চিহ্নিত কৰক + চাওক হিচাপৰ পৰা মুকা + হয় + নহয় + ঠিক আছে + আপুনি নিশ্চিত হৈছে যে আপুনি প্ৰস্থান কৰিব বিচাৰে? + অ্যাপ্‌ৰ বেটেৰি ব্যৱহাৰ ইতিমধ্যে অসীমিত হ’লে + সাবস্ক্ৰাইব কৰা TV সেৰাৰ বাবে অবিচ্ছিন্ন ডাউনলোড আৰু বিজ্ঞাপনৰ বাবে নোটিফিকেশনৰ বাবে, ক্লাউডষ্ট্ৰিমৰ অনুমতি প্ৰয়াপ্ত কৰিবলৈ পৃষ্ঠভূমিত চলকৰ বাবে অনুমতি প্ৰয়াপ্ত কৰিবলৈ লাগে। ঠিক আছে টিপিবলৈ, আপুনি এপ তথ্যলৈ দীঘল হৈ যাব। তত্ত্বাবধানে, ইয়াৰ ব্যৱহাৰ ক্লাউডষ্ট্ৰিমক আপোনাৰ বেটাৰী সেক্ষাৰ কৰিব নাই অৰু। ইয়া শুধু প্ৰয়োজনীয় হোৱা সময়ত বেক্গ্‌গ্‌ত অতিৰিক্ত কাৰ্য কৰিবলৈ অপাৰে, যেনে নোটিফিকেশন সোধা আৰু আধিকাৰিক প্ৰস্তাবনাৰ ভিডিঅ’স ডাউনলোড কৰিবলৈ। যদি আপুনি বাতিল কৰিব বাচন কৰিছে, আপুনি পৰেন্তু সেটিং ইয়াৰ পিছতে ক্রোয়জবলৈ চাব পাৰে জেনে গওঁক সেটিংছ। + ক্লাউডষ্ট্ৰীমৰ অ্যাপ্‌ তথ্য খোলাত অসমৰ্থ + অ্যাপ্‌ৰ নতুন সংস্কৰণ ইনষ্টল কৰিব পৰা নাই + অ্যাপ্‌ আপডেট ডাউনলোড হৈছে… + অ্যাপ্‌ আপডেট ইনষ্টল হৈছে… + পুৰণৰূপ + পেকেজ ইনষ্টলাৰ + অ্যাপ্‌ প্ৰস্থানত আপডেট হৈ যাব + ক্ৰমৰ অনুযায়ী + সাজোৱা + ৰেটিং (উচ্চ থকাৰ পৰা নিম্ন) + ৰেটিং (নিম্ন থকাৰ পৰা উচ্চ) + আপডেট হোৱা (নতুনৰ পৰা পুৰণৰূপ) + আপডেট হোৱা (পুৰণৰূপৰ পৰা নতুন) + বৰ্ণমালা (এ থকা জে পৰা জে) + গোটি বাছক কৰক + সৈতে খোলক + আপোনাৰ গোটিটো খালি আছে :( +\nগোটিত প্ৰৱেশ কৰিবলৈ এখনকা একাউণ্টত লগিন কৰক নাইবা আপোনাৰ স্থানীয় গোটিত চইক যোগ কৰক। + এই তালিকাটো খালি আছে। আৰু এটা অন্য এটা তালিকাত স্থানান্তৰ কৰিব চেষ্টা কৰক। + নিৰাপত ম’ড ফাইল পোৱা গৈছে! +\nফাইল মুকা হোৱা পৰা প্ৰাৰম্ভত কোনো এক্সটেনচন লোড কৰা হ’ব নহয়। + পুনৰ প্ৰয়াণ কৰক + সাবস্ক্ৰাইব কৰা প্ৰদর্শন আপডেট হৈ আছে + %s সাবস্ক্ৰাইব কৰা হ\'ল + %s পৰা সাবস্ক্ৰাইব কৰা হ\'ল + ওয়াইফাই + মোবাইল ডাটা + ডিফ’ল্ট ঠিকনা কৰক + ব্যৱহাৰ কৰক + সম্পাদনা কৰক + প্ৰ’ফাইলসমূহ + সাহায্য + ইয়াত আপুনি সৃষ্টিগত উৎসসমূহ কেমানে সাজোৱা পৰিবৰ্তন কৰিব পাৰে। যদি এটা ভিডিঅ’ এটা উচ্চ অগ্ৰাধিকাৰ থাকে তেনেহলে ই উচ্চ মানৰ উৎস বাছনি তথ্যত প্ৰদৰ্শিত হ\'ব। উৎসৰ প্ৰাথমিকতা আৰু মানৰ প্ৰাথমিকতাৰ যোগফল ভিডিঅ’ৰ প্ৰাথমিকতা হ’ব। +\n +\nউৎস A: 3 +\nগুণগত মান B: 7 +\n10 এৰি মানৰ এটা সংযুক্ত ভিডিঅ’ প্ৰাথমিকতা থাকিব। +\n +\nনোট: যদি যোগফল 10 বা তাতো অধিক হ\'ব তেনেহলে প্লেয়াৰ আটোমেটিকলি সেই লিংক লোড হোৱা সময়ত লোড কৰিব নোৱা হ\'ব! + প্ৰ’ফাইল পৃষ্ঠভূমি + ইউআই সঠিকভাৱে সৃষ্টি কৰা নাই, এটা মুখ্য বুগ আৰু অধীৰ অনুমতি দিয়া হোৱা উচিত এইটো পৰ্যন্ত প্ৰতিবেদন কৰক %s + আপুনি ইতিমধ্যে ভোট দিয়া আছে + প্ৰিয়মূল্য বিষয়বস্তুসমূহ + %s প্ৰিয়মূল্য বিষয়বস্তুসমূহত যোগ কৰা হ\'ল + %s প্ৰিয়মূল্য বিষয়বস্তুসমূহৰ পৰা আঁতৰাই কৰা হ\'ল + প্ৰিয়মূল্য বিষয়বস্তুসমূহত যোগ কৰক + প্ৰিয়মূল্য বিষয়বস্তুসমূহৰ পৰা আঁতৰাই কৰক + সম্ভাৱ্য ডুপ্লিকেট পোৱা গৈছে + সকলোত প্ৰতিস্থাপন কৰক + আপোনাৰ গোটিত এটা সম্ভাব্য ডুপ্লিকেট আইটেম অলপ অস্তিত্বত আছে: \'%s\' +\n +\nআপুনি ই আইটেমটো সংযোগ কৰিব বিচাৰে নে, বৰং ইতিমধ্যে থকা আইটেমটো সংস্কৰণ কৰিব নে, নাইবা অ্যাকছনটো বাতিল কৰিব? + আপোনাৰ গোটিত এক সম্ভাব্য ডুপ্লিকেট আইটেম অনেক অস্তিত্বত আছে: +\n +\n%s +\n +\nআপুনি ই আইটেমটো সংযোগ কৰিব বিচাৰে নে, বৰং ইতিমধ্যে থকা আইটেমটোসমূহ সংস্কৰণ কৰিব নে, নাইবা অ্যাকছনটো বাতিল কৰিব? + %sৰ বাবে PIN এন্টাৰ কৰক + বৰ্তমান পিন এন্টাৰ কৰক + প্ৰ’ফাইল লক কৰক + পিন + অশুদ্ধ পিন। অনুগ্ৰহ কৰি পুনৰ চেষ্টা কৰক। + পিনটো 4 বৰ্ণ থাকিব লাগিব + এটা একাউন্ট নিৰ্বাচন কৰক + একাউন্টসমূহ পৰিচালনা কৰক + প্ৰস্থানত একাউন্ট নিৰ্বাচন পাছ দিব + ডিফ’ল্ট একাউন্ট ব্যৱহাৰ কৰক + ঘূৰাওক + %sৰ হিচাপে লগ ইন কৰা হৈছে + স্ক্ৰীনৰ অৰিএণ্টেশ্বনৰ বাবে ট’গল বুটাম প্ৰদৰ্শন কৰক + ভিডিঅ’ৰ অৰিএণ্টেশ্বনৰ ভিত্তিত স্বয়ংক্ৰিয় স্ক্ৰীনৰ অৰিএণ্টেশ্বন প্ৰবণ কৰক + স্বয়ংক্ৰিয় ঘূৰাওক + প্ৰিয়মূল্যহীন + এই ডিভাইচত বায়োমেট্ৰিক পুনৰ্প্ৰমাণৰ সমৰ্থন কৰা নাই + আঙুলি ছাঁচ ব্যৱহাৰ কৰি, মুখৰ চিত্ৰ, PIN, প্ৰণালী আৰু পাছৱাৰ্ডৰ সৈতে অ্যাপ্‌ আনলক কৰক। + প্ৰিয়মূল্য + এই স্ক্ৰীন একাধিক অসফল চেষ্টাৰ কাৰণে বন্ধ হৈছিল। অনুগ্ৰহ কৰি অ্যাপ্লিকেশ্বন পুনৰ্‌ আৰম্ভ কৰক। + আপোনাৰ CloudStream ডাটা এতিয়া বেকআপ কৰা হৈছে। হৈচঁদিক এই সম্ভাবনা বেছি নাই, সকলো ডিভাইচত বিভিন্নভাৱে আচৰণ কৰিব পাৰে। যদিচয় আপুনি এপ্পটো প্ৰৱেশ কৰাৰ বন্ধ হৈ যাওৱাৰ অভাবতে, অ্যাপ্‌ৰ তথ্য পূৰ্ণভাৱে মচলা কৰক আৰু এক বেকআপৰ পৰা প্ৰতিস্থাপন কৰক। এই বিষয়ত উদ্ভাবিত যোৱা অসুবিধাৰ বাবে আমি অত্যন্ত দুঃখী। + ডাউনলোড কৰক + এপ্‌টোক আৰম্ভ কৰাৰ পাছত নতুন উন্নয়নসমূহ স্বয়ংক্ৰিয়ভাৱে খোঁজি পাওক। + পূৰ্ণ মুক্তি প্ৰাপ্ত কৰিবলৈ প্ৰি-ৰিলিজ উন্নয়ন খোঁজি পাওক। + একেই ডেভল\'পাৰকৰ অনিমে এপ্‌ + নতুন আপডেট পাইছো! +\n%1$s -> %2$s + ফিলাৰ + CloudStream ৰ জৰিয়তে খেলোৱা + সন্ধান + ডাউনলোডসমূহ + টেগসমূহ + লোডিং এৰি যাওক + লোড হৈ আছে… + ধৰি ৰখা হৈ আছে + সম্পন্ন হৈ গ\'ল + এৰি দিয়া হৈছে + পুনৰাবৃত্তি কৰা হৈ আছে + চলচ্চিত্ৰ খেলাওক + ট্ৰেলাৰ খেলাওক + লাইভষ্ট্ৰীম খেলাওক + ছাবটাইটেল বাছনি কৰক + পৰ্ব খেলাওক + প্ৰয়োগ কৰক + \ 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 2be08369..5a104444 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -422,7 +422,7 @@ Вижте хранилищата на общността Публичен списък Всички субтитри с главни букви - Изтегляне на всички добавки от това хранилище\? + Изтегляне на всички добавки от това хранилище? %s (Деактивиран) Потоци Аудио потоци @@ -601,4 +601,4 @@ Покажи предложения Добавя опция за промяна на скоростта в плеъра Този тест е направен за програмисти и не проверява работата на никакви добавки. - + \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 867dd4ed..ccd9e433 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -56,7 +56,7 @@ ডাউনলোড শুরু ডাউনলোড বাদ ডাউনলোড শেষ - স্ট্রিম + নেটওয়ার্ক স্ট্রিম লিংক লোডিং ব্যর্থ ডাব সাব @@ -119,7 +119,7 @@ ক্রোমক্যাস্ট এ সাবটাইটেল সমূহের সেটিংস কালো প্রান্ত অপসারণ করুন অনুসন্ধান করুন - অ্যাকাউন্টসমূহ + অ্যাকাউন্টসমূহ এবং নিরাপত্তা কোনো উপাত্ত পাঠাবে না বিরতি দিতে মাঝে দুইবার চাপুন সিস্টেম এর উজ্জ্বলতা ব্যবহার করুন @@ -143,7 +143,7 @@ পোস্টার @string/home_play আগাতে ডবল ট্যাপ করুন - আইজেনগ্রাভি মোড + প্লেব্যাক এর গতি আপডেট শুরু হয়েছে ব্রাউজার লগ @@ -229,4 +229,134 @@ আপনার বর্তমান পর্বের অগ্রগতি স্বয়ংক্রিয়ভাবে সিঙ্ক করুন প্লাগইন ডাউনলোড ফিল্টার করতে মোড নির্বাচন করুন লিঙ্ক পুনরায় লোড হয়েছে - + সুইচ অ্যাকাউন্ট + ব্রাউজারে প্লে করুন + দাবিত্যাগ + এশিয়ান ড্রামা + সোর্স + এক্সটেনশন + লিংকস + এনএসএফডব্লিউ + ডাউনলোড মিরর + অপ্রত্যাশিত প্লেয়ার এর সমস্যা + সাবটাইটেল ডাউনলোড করুন + রিপোজিটরির নাম এবং ইউ আর এল + কপি করা হয়েছে! + অ্যান্ড্রয়েড টিভির মতো, কম মেমরির ডিভাইসে খুব বেশি সেট করা হলে সমস্যা করবে। + ক্লোন সাইট + প্লেয়ারের ফিচার + MAL AniList TMDB IMDB Kitsu Trakt %1$s%2$s + অ্যাপ থিম + রিকমেন্ডেশনগুলো দেখাও + প্লেয়ারে গতির বিকল্প যোগ কর + %1$s %2$d%3$s + কার্টুন + এনিমে + পোস্টারে ইউ আই উপাদান টগল করুন + আপডেট চেক করুন + রিসাইজ + ওপেনিং স্কিপ করুন + ডিক্সের ভিডিও ক্যাশ + ভিডিও এবং ইমেজ ক্যাশ পরিস্কার করুন + অ্যান্ড্রয়েড টিভির মতো, কম মেমরির ডিভাইসে খুব বেশি সেট করা হলে ক্র্যাশ করবে + ডিএনএস ওভার এইচটিটিপিএস + একটি ভিন্ন URL সহ একটি বিদ্যমান সাইটের, একটি ক্লোন যোগ করুন + স্ক্রিনে ফিট করুন + ক্যাশ + লেআউট + টিভি লেআউট + ব্যবহারকারীর নাম + %1$d-%2$d + NewSiteName + ডিফল্ট + OVA + ওভিয়ে + টরেন্ট + এপিসোড ক্রোমকাস্ট করুন + প্লে হচ্ছে %s সময়ের মধ্যে + লিঙ্ক কপি করুন + স্বয়ংক্রিয় ডাউনলোড + টাইটেল + প্লেয়ার দেখা যাচ্ছে - সিকের পরিমাণ + রিমুভ সাইট + NGINX সার্ভারের ইউআরএল + আইএসপি বাইপাস + অ্যান্ড্রয়েড টিভি + সমর্থিত এক্সটেনশনগুলিতে NSFW সক্ষম করুন + সাবটাইটেল এনকোডিং + দেখার ধরন + ফিচার সমূহ + হোমপেজ এবং লাইব্রেরিতে এলোমেলো বোতাম দেখান + প্রদানকারী + প্রদানকারী পরীক্ষা + স্বয়ংক্রিয় + ফোন লেআউট + পোস্টার শিরোনামের অবস্থান + পোস্টারের নীচে শিরোনাম রাখুন + প্রবেশ করুন + অ্যাকাউন্ট তৈরি করা + একাউন্ট যোগ করা + যখন প্লেয়ার হিডেন থাকবে তখন সিকের পরিমান + ক্রোমকাস্ট মিরর + এক্সটেনশন ভাষা + লেআউট + সাবটাইটেল + অ্যাকশন + ভিডিও প্লেয়ারের টাইটেল এ সর্বোচ্চ ক্যারেক্টার + ডকুমেন্টারি + অ্যাপ লেআউট + ভিডিও + বেকাপ + E + S + hello@world.com + https://example.com + নতুন এপিসোডের নোটিফিকেশন + অন্য এক্সটেনশনের মধ্যে খুঁজুন + কোন আপডেট পাওয়া যায়নি + password123 + আসছে %s সময়ের মধ্যে + বাতিল করুন + %s +\nঅবশিষ্ট + লাইভ স্ট্রিম + সোর্স সমস্যা + রিমোট সমস্যা + রেন্ডারের সমস্যা + ডাউনলোডের সমস্যা, স্ট্রোরেজদের পারমিশন চেক করুন + অ্যাপ এ প্লে করুন + লিংক রিলোড করুন + কোয়ালিটি লেবেল + সাব লেবেল + ডাব লেবেল + লক + আর দেখাবেন না + এই আপডেট স্কিপ করুন + আপডেট + ওয়াইফাই তে যে কোয়ালিটিতে দেখতে চান + মোবাইল ডাটায় যে কোয়ালিটিতে দেখতে চান + ভিডিও প্লেয়ারে রেজুলেশন + ভিডিও বাফার সাইজ + ভিডিও বাফার লেনথ + যখন আইএসপি ব্লক করবে তখন কার্যকরী + গিডহাব প্রক্সি + ডাউনলোডের পথ + ডাব/সাব এনিমে দেখান + স্ট্রেচ + বড় করুন + অ্যাপ আপডেট + গ্যাসচার + ডিফল্ট + সাধারণ + এলোমেলো বোতাম + পছন্দের মিডিয়া + সমস্ত এক্সটেনশন পরীক্ষা করুন + এই পরীক্ষাটি শুধুমাত্র ডেভেলপারদের জন্য এবং কোন এক্সটেনশনের কাজ যাচাই বা অস্বীকার করে না। + এমুলেটর লেআউট + প্রাইমারি রং + 127.0.0.1 + Language code (en) + অ্যাকাউন্ট + প্রস্থান + %1$d%2$s + \ 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 40847edf..3042fa21 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -422,7 +422,7 @@ Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas - Transferir todos os plugins deste repositório\? + Atenção: CloudStream 3 não assume nenhuma responsabilidade pelo uso de extensões de terceiros e não fornece nenhum suporte para elas! %s (Desativado) Reproduzir automaticamente próximo episódio Começa o próximo episódio quando o atual termina @@ -622,7 +622,7 @@ Autenticação de Senha/PIN A autenticação biométrica não é compatível com este dispositivo Desbloquear o aplicativo com impressão digital, ID facial, PIN, padrão e senha. - Esta tela foi fechada devido a diversas tentativas malsucedidas. Por favor reinicie o aplicativo. + Após algumas tentativas fracassadas, o prompt será fechado. Basta reiniciar o aplicativo para tentar novamente. %s \nrestante(s) Favorito @@ -639,4 +639,21 @@ Música Áudio-livro Mídia - + Redefinir + Próximos em %s + Temporada %1$d Episódio %2$d será lançado em + Fcast + Selecione o dispositivo de transmissão + Espelhar transmissão + CloudStream Wiki + Segurança + Contas + Autenticação local + Imagem do código QR + Descartar + Abrir repositório + Acesse %s em seu smartphone ou computador e digite o código acima + Não é possível obter o código PIN do dispositivo, tente a autenticação local + O código PIN expirou! + O código expira em %1$dm %2$ds + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 0a8cf997..e1c51874 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -489,7 +489,7 @@ Přidat klon existujícího webu s jinou adresou URL https://example.com Kód jazyka (cs) - Stáhnout všechny doplňky z tohoto repozitáře\? + Varování: CloudStream 3 nenese žádnou zodpovědnost za používání rozšíření třetích stran a neposkytuje pro ně žádnou podporu! %s (zakázáno) Stopy NSFW @@ -623,7 +623,7 @@ Ověření heslem/PINem Biometrické ověření není na tomto zařízení podporováno Odemkněte aplikaci otiskem prstu, obličejem, PINem, gestem nebo heslem. - Tato obrazovka byla po několika nezdařilých pokusech uzavřena. Restartujte prosím aplikaci. + Po několika nezdařilých pokusech se okno zavře. Pro opětovný pokus restartujte aplikaci. Vaše data z aplikace CloudStream byla nyní zálohována. Ačkoli je tato možnost velmi malá, různá zařízení se mohou chovat různě. Ve výjimečném případě, že se vám přístup k aplikaci zablokuje, data aplikace zcela vymažte a obnovte je ze zálohy. Velmi se omlouváme za případné nepříjemnosti z toho plynoucí. Odebrat z oblíbených %s @@ -641,4 +641,21 @@ Zakažte optimalizace baterie Aby bylo zajištěno nepřetržité stahování a upozornění na odebírané seriály, potřebuje aplikace CloudStream povolení ke spuštění na pozadí. Stisknutím tlačítka OK budete přesměrováni na informace o aplikaci. Tam přejděte na položku Využití baterie aplikací a nastavte možnost Využití baterie na hodnotu Neomezené. Upozorňujeme, že toto povolení neznamená, že CS3 bude vybíjet baterii. Na pozadí bude pracovat pouze v případě potřeby, například při přijímání oznámení nebo stahování videí z oficiálních rozšíření. Pokud se rozhodnete toto nastavení zrušit, můžete jej později upravit v Obecných nastaveních. Audiokniha - + Resetovat + Vychází %s + Epizoda %2$d ze série %1$d bude vydána za + Vysílat zrcadlení + Fcast + Vyberte zařízení k vysílání + CloudStream Wiki + Zabezpečení + Obrázek QR kódu + Zavřít + Otevřít repozitář + Navštivte %s na vašem zařízení nebo počítači a zadejte kód výše + Nepodařilo se získat PIN kód, zkuste místní ověření + Kód vyprší za %1$dm %2$ds + Účty + Lokální ověření + PIN kód vypršel! + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5a871217..d111ed68 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -151,7 +151,7 @@ Speicherberechtigungen fehlen. Bitte erneut versuchen. Suche Konten und Sicherheit - Updates und Datensicherung + Aktualisierungen und Datensicherung Info Erweiterte Suche Liefert die Suchergebnisse getrennt nach Anbietern @@ -416,12 +416,12 @@ Community-Repositories anzeigen Öffentliche Liste Alle Untertitel in Großbuchstaben - Alle Plugins aus diesem Repository herunterladen\? + Alle Plugins aus diesem Repository herunterladen? %s (Deaktiviert) Spuren Audiospuren Videospuren - Bei Neustart anwenden + Starte die App neu, um die Änderungen zu sehen. Abgesicherter Modus aktiviert Alle Erweiterungen wurden aufgrund eines Absturzes deaktiviert, damit Sie diejenige finden können, welche Probleme verursacht. Absturzinfo ansehen @@ -607,4 +607,12 @@ Beim kopieren ist ein Fehler aufgetreten, bitte kopieren sie logical und wenden sich an den Support. Fehler beim zugriff auf die Zwischenablage, bitte erneut versuchen. Repository Name und URL - + OK + Akku-Optimierung deaktivieren + Musik + Hörbuch + Medien + Zurücksetzen + Akkuverbrauch der App ist bereits auf unbeschränkt eingestellt + CloudStreams App-Info kann nicht geöffnet werden. + \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index a539f374..dbf03fb8 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -91,7 +91,7 @@ Ρυθμίσεις υποτίτλων του προγράμματος αναπαραγωγής Υπότιτλοι για Chromecast Ρυθμίσεις υποτίτλων για Chromecast - Eigengravy Mode + Ταχύτητα Προβολής Σύρετε για αναζήτηση Σύρετε από πλευρά σε πλευρά για να ελέγξετε το σημείο του βίντεο στο οποίο βρίσκεστε Σύρετε για να αλλάξετε ρυθμίσεις @@ -130,7 +130,7 @@ Τα δεδομένα αποθηκεύτηκαν Δεν έχει δοθεί άδεια για πρόσβαση στον αποθηκευτικό χώρο. Παρακαλώ προσπαθήστε ξανά. Σφάλμα δημιουργίας αντιγράφων ασφαλείας %s - Λογαριασμοί + Λογαριασμοί και Ασφάλεια Ενημερώσεις και αντίγραφα ασφαλείας Εμφάνιση filler επεισοδίου για άνιμε Εμφάνιση trailer @@ -201,7 +201,7 @@ Επαναφόρτωση συνδέσμων Λήψη υποτίτλων Ποιότητα - Dub + Ετικέτα Dub Sub Τίτλος Εναλλαγή γραφικών στοιχείων στην αφίσα @@ -233,11 +233,11 @@ Αποποίηση ευθυνών Γενικά Κουμπί τυχαίας δράσης - Εμφάνιση κουμπιού τυχαίας δράσης στην Αρχική Οθόνη + Εμφάνιση κουμπιού τυχαίας προβολής στην Αρχική Οθόνη Γλώσσες παρόχων Διάταξη εφαρμογής Προτιμώμενα μέσα - Ενεργοποίηση NSFW σε υποστηριζόμενους παρόχους + Ενεργοποίηση ακατάλληλου περιεχομένου σε υποστηριζόμενους παρόχους Κωδικοποίηση υποτίτλων Πάροχοι Διάταξη @@ -302,8 +302,8 @@ Φιλτράρισμα ανά την προτεινόμενη γλώσσα του μέσου Έξτρα Τρέιλερ - Σύνδεσμος για stream - Παραπομπή + Https://Παράδειγμα.com/Παράδειγμα.mp4 + Παραπομπή (προαιρετική) Επόμενο Παρακολούθηση βίντεο σε αυτή την γλώσσα Προηγούμενο @@ -334,8 +334,6 @@ Ενημερώθηκαν %d πρόσθετα Το CloudStream δεν έχει προεγκατεστημένους ιστότοπους. Πρέπει να εγκαταστήσετε ιστότοπους μέσω ορισμένων αποθετηρίων. \n -\nΛόγω ενός χαζού DMCA takedown από μέρους των Sky UK Limited 🤮 δεν μπορούμε να προσθέσουμε απευθείας σύνδεσμο προς τα προαναφερόμενα αποθετήρια εντός της εφαρμογής. -\n \nΒρείτε μας στο Discord ή ψάξτε στο διαδίκτυο. Προβολή αποθετηρίων κοινότητας Δημόσια λίστα @@ -345,7 +343,7 @@ Κομμάτια Ηχητικά κομμάτια Κομμάτια βίντεο - Εφαρμογή στην επανεκκίνηση + Κάντε επανεκκίνηση της εφαρμογής για να δείτε τις αλλαγές. Η ασφαλής λειτουργία ενεργοποιήθηκε Όλα τα extensions απενεργοποιήθηκαν , ώστε να μπορέσετε να διαπιστώσετε ποιο από αυτά προκάλεσε τη κατάρρευση. Προβολή πληροφορίας κατάρρευσης @@ -392,7 +390,7 @@ Αυτόματη ενημέρωση plugin Αυτόματη λήψη plugin DNS μέσω HTTPS - παράδειγμα.com + https://παράδειγμα.com HQ TS TC @@ -412,7 +410,7 @@ NSFW Chromecast mirror Σύνδεσμος NGINX σέρβερ - ΟΚουλΙστότοποςΜου + ΝεοΟνομαΙστοτοπου /\?\? /%d %d / 10 @@ -434,7 +432,7 @@ Δεν βρέθηκε ενημέρωση Έλεγχος για ενημέρωση κωδικός123 - ΤοΚουλΨευδώνυμοΜου + ΤοΚουλΟνομαΜου γειασου@κόσμε.com Η γρήγορη, καφέ αλεπού πηδάει πάνω από τον τεμπέλη σκύλο / The quick brown fox jumps over the lazy dog Cam @@ -521,7 +519,7 @@ \nΣΗΜΕΙΩΣΗ: Εάν το άθροισμα είναι 10 ή περισσότερο, η συσκευή αναπαραγωγής θα παραλείψει αυτόματα τη φόρτωση όταν φορτωθεί αυτός ο σύνδεσμος! Δοκιμή παρόχου Προτιμώμενη ποιότητας παρακολούθησης (Δεδομένα τηλεφώνου) - Διακομιστής μεσολάβησης raw.githubusercontent.com + Διακομιστής μεσολάβησης GitHub Android TV Ενημέρωση εγγεγραμμένων εκπομπών Έγινε εγγραφή σε %s @@ -546,4 +544,85 @@ Επιλέξτε κατάσταση για φιλτράρισμα επεκτάσεων για λήψη Απενεργοποιημένο Τέλος - + Συχνότητα δημιουργίας αντιγράφων ασφαλείας + Οι σύνδεσμοι επαναφορτώθηκαν + αντιγράφηκε! + Αναζήτηση σε άλλες επεκτάσεις + Ειδοποίηση για νέο επεισόδιο + Εισαγωγή Κωδικού + Όνομα \"αποθήκης\" και λινκ + Εμφάνιση προτάσεων + Προσθήκη στα Αγαπημένα + Εγγραφή + Αντικατάσταση Όλων + Χρησιμοποίηση Βασικού λογαριασμού + Αφαίρεση από τα Αγαπημένα + Κρυμμένο Πλέιερ - Δευτερόλεπτα Σκιπ + Δευτερόλεπτα Σκιπ όταν ο αναπαραγωγέας είναι κρυφός + Εντάξει + Απενεργοποιήση της εξοικονόμησης της μπαταρίας + Έχετε ήδη ψηφίσει + Φαίνεται πως ένα πιθανό αντίγραφο βρίσκεται στη βιβλιοθήκη σας: \'%s.\' +\n +\nΘα επιθυμούσατε να το προσθέσετε, να το αντικαταστήσετε, ή να ακυρώσετε την ενέργεια; + Εισαγωγή Τρέχον Κωδικού + Κλείδωμα Προφίλ + Ξεκλείδωμα Cloudstream + ΚωδικόςPIN Αυθεντικότητας + Προσθέτει επιλογή ταχύτητας στον αναπαραγωγέα + Σύνδεση ως %s + Περιστροφή + Απεγγραφή + Διαχείριση λογαριασμών + Ενεργοποίηση αυτόματης περιστροφής οθόνης αναλόγως του βίντεο + Αυτόματη περιστροφή + Σεζόν %1$dΕπεισόδιο%2$d θα κυκλοφορήσει + Δευτερόλεπτα Σκιπ όταν φαίνεται ο αναπαραγωγέας (πλειερ) + Δοκιμή όλων των παροχών + Αυτό το τεστ προορίζεται μόνο για τους προγραμματιστές και δε επαληθείει ούτε απορρίπτει την λειτουργία οποιουδήποτε παρόχου. + Fcast + Επιλογή συσκευής για αναμετάδοση + Πρόβλημα στην πρόσβαση στο Clipboard, Παρακαλώ προσπαθήστε ξανά. + Πρόβλημα στην αντιγραφή , Παρακαλούμε αντιγράψτε το logcat και επικοινωνήστε με την υποστήριξη. + Η χρήση μπαταρίας έχει ήδη τεθεί χωρίς περιορισμό + Αδύνατο άνοιγμα των στοιχείων της εφαρμογής Cloudstream. + Αγαπημένα + %s προστέθηκε στα αγαπημένα + %s αφαιρέθηκε από τα αγαπημένα + Πιθανό αντίγραφο βρέθηκε + Προσθήκη + Αντικατάσταση + Πιθανά διπλά αρχεία βρέθηκαν στην βιβλιοθήκη: +\n +\n%s +\n +\nΘα επιθυμούσατε να προσθέσετε αυτό το αρχείο ούτως η άλλως, να αντικαταστήσετε τα ήδη υπάρχοντα, ή να ακυρώσετε την ενέργεια? + Εισαγωγή Κωδικού για %s + Κωδικός + Εσφαλμένος Κωδικός. Προσπαθήστε ξανά. + Ο κωδικός να περιέχει 4 χαρακτήρες + Επιλογή λογαριασμού + Αφαίρεση από αγαπημένα + Κλείδωμα με βιομετρικά + Έρχεται σε %s + Εμφάνιση Πλέιερ - Δευτερόλεπτα Σκιπ + Παράκαμψη απαγόρευσης από raw github URLs χρησιμοποιώντας jsDelivr. Μπορεί να καθυστερήσει τις ενημερώσεις για μερικές μέρες. + Εμφάνιση κουμπιού για περιστροφή οθόνης + Αγαπημένο + %s +\nαπομένουν + Βιομετρική αυθεντικοποίηση δεν υποστηρίζεται από τη συσκευή + Καστ ταινίας + Για να σιγουρέψουμε πως οι λήψεις ταινιών και οι ειδοποιήσεις για σειρές στο Cloudstream δεν έχουν πρόβλημα, το Cloudstream χρειάζεται άδεια να τρέχει στο παρασκήνιο. Πατώντας ΟΚ θα μεταφερθείτε στις λεπτομέρειες εφαρμογής, από κει πηγαίνετε στην Χρήση μπαταρίας από εφαρμογές και θέσετε την χρήση σε μη περιορισμένη. Να έχετε στο νου σας πως το Cloudstream δε θα καταναλώσει την μπαταρία σας. Απλά θα λειτουργήσει μόνο όταν χρειάζεται, όπως για την ειδοποίηση για ανερχόμενες σειρές ή της λήψεις σας μέσω των παροχών. Άμα θέλετε να ακυρώσετε, μπορείτε να αλλάξετε αυτή τη ρύθμιση μέσω των γενικών ρυθμίσεων. + Ξεκλείδωμα εφαρμογής με δακτυλικό αποτύπωμα, Face ID, PIN, Μοτίβο και Κωδικό. + Η οθόνη έκλεισε λόγω πολλαπλών ανεπιτυχών ενεργειών. Κάντε επανεκκίνηση της εφαρμογής. + Επεξεργασία λογαριασμού + Παράλειψη επιλογής λογαριασμού στην εκκίνηση της εφαρμογής + Μουσική + Ακουστικό Βιβλίο + Μέσα + Επαναφορά + Τα δεδομένα σας στο CloudStream έχουν κάνει back up. Αν και η πιθανότητα είναι πολύ χαμηλή, όλες οι συσκευές συμπεριφέρονται διαφορετικά. Στη σπάνια περίπτωση, που απαγορευτεί η πρόσβασή σας από την εφαρμογή, διαγράψτε τα δεδομένα εφαρμογής και επαναφέρετέ τα από ένα ήδη υπάρχον backup. Συγνώμη για οποιαδήποτε ταλαιπωρία. + Λογαριασμοί + Ασφάλεια + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 20484cd9..011762ba 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -176,7 +176,7 @@ Tipo de Borde Elevación de Subtítulo Buscar usando proveedores - Continúa la reproducción en un reproductor miniatura encima de otras aplicaciones + Continúa la reproducción en una imagen pequeña encima de otras aplicaciones Botón de cambio de tamaño del reproductor Eliminar bordes negros Seleccionar idioma automáticamente @@ -232,7 +232,7 @@ Mostrar los resultados de la búsqueda por proveedor Solo envíar los datos si la App se cierra / falla inesperadamente No enviar datos - Mostrar los trailers + Mostrar avances Mostrar pósters de Kitsu Actualizar a las versiones preliminares Buscar actualizaciones preliminares (beta) en lugar de solo versiones completas (stable releases) @@ -289,7 +289,7 @@ CloudStream no tiene sitios instalados por defecto. Necesitas instalar los sitios desde los repositorios. \n \nÚnase a nuestro Discord o busque en línea. - ¿Descargar todos los plugins de este repositorio? + Advertencia: ¡CloudStream 3 no asume ninguna responsabilidad por el uso de extensiones de terceros y no brinda ningún soporte para ellas! Mostrar actualizaciones de la aplicación Instalador de APK Algunos dispositivos no soportan el nuevo instalador de paquetes. Pruebe la opción antigua (legacy) si las actualizaciones no se instalan. @@ -339,7 +339,7 @@ Alternar elementos de la interfaz de usuario en el póster No se encontró ninguna actualización General - Color primario + Color principal Tema de la aplicación hola@mundo.com %1$s %2$s @@ -449,13 +449,13 @@ Descarga por lotes plugin plugins - Actualizados %d plugins + %d plugins actualizados Ver repositorios de la comunidad Lista pública Pistas Pistas de audio Pistas de video - Modo seguro ON + Modo seguro activado Ver información de fallos Puntaje:%s Versión @@ -483,7 +483,7 @@ Si La aplicación se actualizará al salir Actualización iniciada - Complemento descargado + Plugin descargado Quitar de visto Ordenar por Ordenar @@ -512,7 +512,7 @@ Registro Empezar Aprobado - Prueba del proveedor + Verificar al proveedor Reiniciar Suscrito Suscrito a %s @@ -545,7 +545,7 @@ La interfaz de usuario no se ha podido crear correctamente, se trata de un GRAN BUG y debe ser reportado inmediatamente %s Seleccionar modo para filtrar los plugins descargados Deshabilitar - No se encontraron complementos en el repositorio + No se encontraron plugins en el repositorio Repositorio no encontrado, comprueba la URL y prueba la VPN Ya has votado Frecuencia de la copia de seguridad @@ -599,7 +599,7 @@ Desbloquea la aplicación con huella dactilar, Face ID, PIN, patrón y contraseña. Desbloquear CloudStream La autenticación biométrica no es compatible con este dispositivo - Esta pantalla se cerró después de algunos intentos fallidos. Reinicie la aplicación. + Después de algunos intentos fallidos, el mensaje se cerrará. Simplemente reinicie la aplicación para volver a intentarlo. Ahora se ha realizado una copia de seguridad de sus datos de CloudStream. Aunque la posibilidad de que esto ocurra es muy baja, todos los dispositivos pueden comportarse de forma diferente. En el raro caso de que no puedas acceder a la aplicación, borra completamente los datos de la aplicación y restaura desde una copia de seguridad. Sentimos mucho las molestias que esto pueda ocasionarte. Favorito No favorito @@ -616,5 +616,22 @@ No se puede abrir la información de la aplicación CloudStream. Media Audiolibro - Para garantizar descargas y notificaciones ininterrumpidas para programas de televisión suscritos, CloudStream necesita permiso para ejecutarse en segundo plano. Al presionar OK, se le dirigirá a información de la aplicación. Allí, desplácese hasta Uso de la batería de la aplicación y establezca el uso de la batería en Sin restricciones. Tenga en cuenta que este permiso no significa que CS3 agotará su batería. Solo funcionará en segundo plano cuando sea necesario, como cuando reciba notificaciones o descargue videos de extensiones oficiales. Si decide cancelar, puede ajustar esta configuración más adelante en los ajustes generales. - + Para garantizar descargas y notificaciones ininterrumpidas para programas de televisión suscritos, CloudStream necesita permiso para ejecutarse en segundo plano. Al presionar OK, se le dirigirá a información de la aplicación. Allí, desplácese hasta 𝙐𝙨𝙤 𝙙𝙚 𝙡𝙖 𝙗𝙖𝙩𝙚𝙧í𝙖 𝙙𝙚 𝙡𝙖 𝙖𝙥𝙡𝙞𝙘𝙖𝙘𝙞ó𝙣 y establezca el uso de la batería en 𝙎𝙞𝙣 𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙘𝙞𝙤𝙣𝙚𝙨. Tenga en cuenta que este permiso no significa que CS3 agotará su batería. Solo funcionará en segundo plano cuando sea necesario, como cuando reciba notificaciones o descargue videos de extensiones oficiales. Si decide cancelar, puede ajustar esta configuración más adelante en 𝙡𝙤𝙨 𝙖𝙟𝙪𝙨𝙩𝙚𝙨 𝙜𝙚𝙣𝙚𝙧𝙖𝙡𝙚𝙨. + Reset + Próximamente en %s + La temporada %1$d y el episodio %2$d se estrenarán en + Seleccionar el dispositivo para transmitir + Fcast + Espejo de transmisión + Wiki de CloudStream + Seguridad + Cuentas + Autenticación local + Imagen del código QR + Descartar + Repositorio abierto + Visita %s en tu smartphone o ordenador e introduce el código anterior + ¡El código PIN ya ha caducado! + El código caduca en %1$d mín y %2$d s + No puedo obtener el código PIN del dispositivo; intente con la autenticación local + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 77c3db15..91d23b61 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -280,7 +280,7 @@ Erreur de sauvegarde %s Recherche Comptes et Sécurité - Mises à jour et sauvegarde + Mises à jour et Sauvegarde Info Recherche avancée Vous donne les résultats de la recherche séparés par fournisseur @@ -419,7 +419,7 @@ Télécharger la liste de sites que vous voulez utiliser Téléchargé : %d Pistes vidéo - Appliqué au redémarrage + Redémarrez l\'application pour voir les changements. Toutes les extensions ont été désactivé à cause d\'un crash pour vous aider à trouver l\'extension causant le problème. Mode sans échec activé Taille @@ -446,7 +446,7 @@ Désactivé : %d Non téléchargé : %d %d plugins mis-à-jour - Télécharger tous les plugins de ce repository \? + Télécharger tous les plugins de ce repository ? %s (Désactivé) Pistes Pistes audio @@ -595,4 +595,29 @@ Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. Copié ! Nom du dépôt et adresse internet - + Favori + Vos données CloudStream viennent d\'être sauvegardées. Bien que cette éventualité soit très faible, tous les appareils peuvent se comporter différemment. Dans le rare cas où l\'accès à l\'application est bloqué, effacez complètement les données de l\'application et restaurez à partir d\'une sauvegarde. Nous sommes sincèrement désolés pour les désagréments occasionnés par cette situation. + Désactiver l\'optimisation de la batterie + Impossible d\'ouvrir les informations de l\'application CloudStream. + Déverrouiller CloudStream + Musique + %s +\nrestants + Erreur d\'accès au presse-papiers, veuillez réessayer. + OK + L\'authentification biométrique n\'est pas prise en charge sur cet appareil + Livre Audio + Mot de passe/Code PIN + Erreur de copie, Veuillez copier le logcat et contacter le support de l\'application. + Déverrouiller l\'appli avec l\'empreinte digitale, l\'identification faciale, le code PIN, le motif et le mot de passe. + Cet écran a été fermé en raison de plusieurs tentatives infructueuses. Veuillez relancer l\'application. + Pour garantir des téléchargements ininterrompus et des notifications pour les émissions de télévision auxquelles vous êtes abonné, CloudStream a besoin d\'une autorisation pour fonctionner en arrière-plan. En appuyant sur OK, vous serez dirigé vers App info. Faites défiler jusqu\'à 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 et réglez l\'utilisation de la batterie sur 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Veuillez noter que cette autorisation ne signifie pas que CS3 épuisera votre batterie. Il ne fonctionnera en arrière-plan que lorsque cela sera nécessaire, par exemple lors de la réception de notifications ou du téléchargement de vidéos à partir d\'extensions officielles. Si vous choisissez d\'annuler, vous pouvez ajuster ce paramètre ultérieurement dans 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. + L\'utilisation de la batterie de l\'application est déjà réglée sur illimitée + Supprimer des favoris + Média + Réinitialiser + À venir dans %s + Verrouillage biométrique + Sélectionnez un appareil de diffusion + Saison %1$d Episode %2$d sera publié dans + \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 8ce224b3..b16292ba 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -192,4 +192,21 @@ लिंक पुन्ह खुली वर्तमान पिन दर्ज करें नेटवर्क स्ट्रीम - + साफ़ करें + उपशीर्षक सेटिंग्स + अक्षर का माप + बंद करें + रिपॉजिटरी का नाम और यूआरएल + कॉपी! + सहेजें + नये एपिसोड की अधिसूचना + अन्य एक्सटेंशन में खोजें + सुझाव दिखाएं + पृष्ठभूमि का रंग + रूपरेखा प्रकार + अक्षर का रंग + बॉक्स का रंग + रूपरेखा रंग + उपशीर्षक ऊंचाई + अक्षर शैली + \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index ea6a80eb..6e35506d 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -11,41 +11,41 @@ %d %.1f/10.0 %d - %1$s Ep %2$d - Cast: %s - Epizoda %d će izaći - %1$dd %2$dh %3$dm - %1$dh %2$dm - %dm + %1$s epizoda %2$d + Glumačka postava: %s + Epizoda %d će izaći za + %1$dd %2$dh %3$dmin + %1$dh %2$dmin + %dmin Poster Poster - Episode Poster - Main Poster - Next Random - Go back - Change Provider - Preview Background + Poster epizode + Glavni poster + Sljedeće slučajno odabrano + Idi natrag + Promijeni pružatelja usluge + Pregled slike pozadine - Brzina (%.2fx) + Brzina (%.2f×) Ocjena: %.1f - Pronađeno novo ažuriranje! + Pronađeno je novo ažuriranje! \n%1$s -> %2$s Umetak %d min CloudStream - Reproduciraj s CloudStream-om + Reproduciraj s CloudStreamom Početna stranica - Pretraži + Traži Preuzimanja Postavke - Pretraži… - Pretraži %s… + Traži … + Traži %s … Nema podataka - Više postavki + Više opcija Sljedeća epizoda Žanrovi - Podijeli + Dijeli Otvori u pregledniku Preskoči učitavanje Učitavanje … @@ -53,35 +53,35 @@ Na čekanju Dovršeno Ispušteno - Planiram pogledati - Ponovno gledam - Pokreni Film + Planiram gledati + Ponovo gledam + Pokreni film Pokreni LiveStream Pokreni Torrent Izvori Titlovi - Ponovno pokušaj povezivanje… + Ponovni pokušaj povezivanja … Idi natrag - Pokreni Epizodu + Pokreni epizodu Preuzmi Preuzeto - Trenutno preuzimam + Preuzimanje u tijeku Preuzimanje pauzirano Preuzimanje započeto - Preuzimanje nije uspjelo + Preuzimanje neuspjelo Preuzimanje otkazano Preuzimanje dovršeno Mrežni stream - Pogreška pri učitavanju veza - Unutarnja pohrana - Dub - Sub + Pogreška pri učitavanju poveznica + Interna pohrana + Sinkronizacija + Titlovi Izbriši datoteku Otvori datoteku Nastavi preuzimanje Pauziraj preuzimanje - Onemogući automatsko izvješćivanje o bugovima + Onemogući automatsko izvješćivanje o greškama Više informacija Sakrij Pokreni @@ -93,99 +93,99 @@ Primijeni Kopiraj Zatvori - Očisti + Izbriši Spremi Brzina playera Postavke titlova Boja teksta - Boja obruba - Pozadinska boja + Boja konture + Boja pozadine Boja prozora - Tip ruba + Vrsta ruba Visina titlova Font Veličina fonta - Pretraži s uslugama - Pretraži s tipovima - %d banana dano developerima + Traži koristeći pružatelje usluga + Traži koristeći vrste + %d banana dano programerima Nisi dao ni jednu bananu Automatski odabir jezika Preuzmi jezike Jezik titlova - Držite za vraćanje na zadane postavke - Uvezi fontove tako da ih postavite u %s - Nastavite s gledanjem + Pritisni za vraćanje na zadane postavke + Uvezi fontove postavljanjem u %s + Nastavi gledati Ukloni Više informacija @string/home_play - Za ispravan rad ovog pružatelja usluga može biti potreban VPN + Za ispravan rad ovog pružatelja usluga je možda 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. + Stranica ne sadrži metapodatke. Učitavanje videa neće uspjeti ako ne postoje na stranici. Opis - Plot nije pronađen + Radnja nije pronađena Opis nije pronađen - Prikaži LogMačku 🐈 - Picture-in-picture - Nastavlja reprodukciju u minijaturnom playeru povrh drugih aplikacija - Gumb za promjenu veličine playera - Uklaja crne rubove + Prikaži Logcat 🐈 + Slika u slici + Nastavlja reprodukciju u minijaturnom playeru ispred drugih aplikacija + Gumb za mijenjenje veličine playera + Ukloni crne rubove Titlovi Postavke titlova playera - Chromecast Titlovi + Chromecast titlovi Postavke Chromecast titlova Brzina reprodukcije - Prijeđi prstom za traženje - Prijeđite prstom ulijevo ili udesno kako biste kontrolirali player - Klizni za promjenu postavki - Kliznite prstom ulijevo ili udesno za promjenu svjetline ili glasnoće - Automatski započni sljedeću epizodu - Započne sljedeću epizodu kad trenutna završi - Dodirni dvaput za traženje + Klizni prstom za pomicanje + Klizni prstom ulijevo ili udesno za postavljanje pozicije videa + Klizni prstom za mijenjanje postavki + Klizni prstom prema gore ili dolje na lijevoj ili desnoj strani za mijenjanje svjetline ili glasnoće + Automatski pokreni sljedeću epizodu + Pokreni sljedeću epizodu kada trenutačna epizoda završi + Dodirni dvaput za pomicanje Dodirni dvaput za pauziranje - Iznos preskakanja u playeru (Sekunde) - Dvaput dodirni desnu ili lijevu stranu ekrana za pomicanje naprijed ili natrag - Dodirnite dvaput u sredinu zaslona za pauziranje - Koristi svijetlinu u sustavu + Količina pomicanja u playeru (sekunde) + Dodirni dvaput desnu ili lijevu stranu za pomicanje prema naprijed ili natrag + Dodirni dvaput u sredinu za pauziranje + Koristi svijetlinu sustava Koristi svjetlinu sustava u playeru aplikacija umjesto tamnog preklopa Ažuriraj napredak gledanja Automatski sinkronizira vaš trenutni napredak u filmu ili epizodi - Vraćanje podataka iz sigurnosne kopije + Obnovi podatke iz sigurnosne kopije Sigurnosno kopiranje podataka - Učitana datoteka sigurnosne kopije - Vraćanje podataka iz datoteke nije uspjelo %s + Datoteka sigurnosne kopije je učitana + Obnavljanje podataka iz datoteke %s nije uspjelo Podaci pohranjeni Nedostaju dozvole za pohranu, pokušaj ponovo. Pogreška pri sigurnosnom kopiranju %s - Pretraži + Traži Računi i sigurnost - Ažuriranja i sigurnosne kopije + Ažuriranja i sigurnosna kopija Informacije - Napredno pretraživanje - Daje rezultate pretraživanja odvojene prema pružatelju usluga + Napredna pretraga + Daje rezultate pretrage odvojene prema pružatelju usluga Šalje samo podatke o padovima aplikacije Ne šalje podatke Prikaži dodatnu epizodu za anime Prikaži trailere Prikaži postere iz Kitsua - Sakrij odabranu kvalitetu videozapisa u rezultatima pretraživanja + Sakrij odabranu kvalitetu videa u rezultatima pretrage Automatsko ažuriranje dodataka Prikaži ažuriranja aplikacije Automatski traži nova ažuriranja nakon pokretanja aplikacije. Ažuriranje na predizdanja - Tražite ažuriranja prije izdanja umjesto samo potpunih izdanja + Tražite ažuriranja predizdanja umjesto samo potpunih izdanja Github - Aplikacija za romane od istih developera - Anime aplikacija od istih developera - Uđi u naš Discord - Daj bananu developerima - Dana banana + Aplikacija za romane od istih programera + Anime aplikacija od istih programera + Pridruži se Discordu + Daj bananu programerima + Dane banane Jezik aplikacije Ovaj pružatelj usluga nema podršku za Chromecast - Nisu pronađene veze - Veza je kopirana u međuspremnik + Nisu pronađene poveznice + Poveznica 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 se aplikacija srušila. Anonimno izvješće o grešci će se poslati programerima Sezona Nema sezone Epizoda @@ -197,14 +197,14 @@ Nisu pronađene epizode Izbriši datoteku Izbriši - Poništi + Odustani Pauziraj Nastavi - -30 + −30 +30 Ovo će trajno izbrisati %s \nJeste li sigurni\? - %dm + %dmin \npreostalo U tijeku Završeno @@ -244,11 +244,11 @@ Livestream NSFW Video - Greška u izvoru - Pogreška remote-a - Pogreška renderera + Pogreška u izvoru + Pogreška eksternog računala + Pogreška u prikazu Neočekivana pogreška playera - Pogreška preuzimanja, provjeri dozvole za pohranu + Pogreška tijekom preuzimanja, provjeri dozvole za pohranu Chromecast epizoda Chromecast mirror Pokreni u aplikaciji @@ -257,11 +257,11 @@ Kopiraj poveznicu Automatsko preuzimanje Preuzmi zrcalo - Ponovno učitaj poveznice + Ponovo učitaj poveznice Preuzmi titlove - Oznaka kvalitete - Oznaka sinkronizacije - Oznaka titlova + Oznaka za kvalitetu + Oznaka za sinkronizaciju + Oznaka za titlove Naslov Uključi/isključi elemente korisničkog sučelja na posteru Nije pronađeno ažuriranje @@ -270,38 +270,38 @@ Promijeni veličinu Izvor Preskoči OP - Ne prikazuj više + Nemoj više prikazivati Preskoči ovo ažuriranje Ažuriraj - Preferirana kvaliteta streama + Preferirana kvaliteta gledanja (WiFi) Maksimalni broj znakova u naslovu video playera Rezolucija video playera - Veličina video međuspremnika - Duljina video međuspremnika - Video predmemorija na disku - Očisti predmemoriju videa i slika - Izazvat će nasumična rušenja ako se postavi previsoko. Nemojte mijenjati ako imate malu količinu RAM-a kao što je Android TV ili stari telefon. - Može uzrokovati probleme na sustavima s malo prostora za pohranu kao što su Android TV uređaji ako postavite previsoko. + Veličina međuspremnika videa + Duljina međuspremnika videa + Predmemorija videa na disku + Izbriši predmemoriju videa i slika + Uzrokuje rušenje aplikacije ako se postavi previsoko na uređajima s malom količinom RAM-a kao što je Android TV. + Uzrokuje probleme ako se postavi previsoko na uređajima s malom količinom memorije kao što je Android TV. DNS preko HTTPS-a Korisno za zaobilaženje blokada ISP-a Kloniraj web stranicu Ukloni web stranicu Dodajte klon postojeće web-lokacije s drugim url-om - Put preuzimanja + Putanja preuzimanja NGINX server URL Prikaži sinkronizirani anime ili s titlovima - Prilagodi zaslonu + Prilagodi veličini ekrana Rastegni Zoom - Obavijest + Pravna 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. Općenito - Random gumb - Prikaži gumb za slučajni odabir reprodukcija na početnoj stranici i biblioteci + Gumb za slučajni odabir + Prikaži gumb za slučajni odabir na početnoj stranici i biblioteci Jezici proširenja Izgled aplikacije Preferirani mediji - Omogućava NSFW na podržanim proširenjima + Omogući NSFW na podržanim proširenjima Kodiranje titlova Pružatelji usluga Raspored @@ -320,60 +320,60 @@ 127.0.0.1 NovoImeStranice https://primjer.com - Šifra jezika (en) + Šifra jezika (hr) %1$s %2$s račun Odjava Prijava Promijeni račun Dodaj račun - Napravi račun - Dodaj tracking + Stvori račun + Dodaj praćenje Dodano %s Sinkroniziraj Ocijenjeno %d / 10 /\?\? /%d - Ovjereno%s + %s ovjeren Nije moguće prijaviti se na %s Nijedan - Normal + Normalno Sve - Maksimalno - Minimalno - Obrub - Depresivno + Maks. + Min. + Kontura + Udubljeno Sjena - Podignuto + Izdignuto Sinkroniziraj titlove 1000 ms Kašnjenje titlova - Koristi ovo ako su titlovi prikazani %d ms prerano + Koristi ovo ako se titlovi prikazuju %d ms prerano Koristite ovo ako se titlovi prikazuju %d ms prekasno - Nema kašnjenja titlova + Bez kašnjenja titlova - The quick brown fox jumps over the lazy dog + Gojazni đačić s biciklom drži hmelj i finu vatu u džepu nošnje Preporučeno Učitano %s - Učitaj datoteku titlova - Učitaj sa interneta + Učitaj iz datoteke + Učitaj s interneta Preuzeta datoteka - Glavno - Podupiranje - Pozadina + Glavni + Sporedni + Statist Izvor - Random - Dolazi uskoro… - Cam - Cam - Cam + Slučajno + Dolazi uskoro … + Kamera + Kamera + Kamera HQ HD TS @@ -392,59 +392,59 @@ Rezolucija i naslov Naslov Rezolucija - ID je nevažeći + Nevažeći ID Nevažeći podaci - URL je nevažeći - Greška - Ukloni CC iz titlova - Ukloni reklame iz titlova - Filtriraj po željenom jeziku medija - Extras + Nevažeći URL + Pogreška + Ukloni titlove za gluhe osobe iz titlova + Ukloni nepotrebne elemente iz titlova (npr. oglase) + Filtriraj po preferiranom jeziku medija + Dodatni sadržaji Trailer https://primjer.com/primjer.mp4 - Referent (nije obavezno) + Referent (opcionalno) Sljedeće - Gledaj videozapise na ovim jezicima + Gledaj videa na ovim jezicima Prethodno Preskoči postavljanje - Promijeni izgled aplikacije kako bi odgovarao vašem uređaju + Promijeni izgled aplikacije kako bi odgovarao tvom uređaju Izvještavanje o rušenju - Što želite vidjeti + Što želiš vidjeti Gotovo - Ekstenzije - Dodaj repository - Ime repositorya - URL spremišta (repositorija) + Proširenja + Dodaj repozitorij + Ime repozitorija + URL repozitorija Dodatak učitan Dodatak izbrisan Nije moguće učitati %s 18+ - Započeto preuzimanje %1$d %2$s… + Započeto preuzimanje %1$d %2$s … Preuzeto %1$d %2$s - Sve %s je već preuzeto + Sve %s već preuzeto Skupno preuzimanje dodatak dodaci Ovo će također izbrisati sve dodatke repozitorija - Izbriši repository - Preuzmi popis stranica koje želite koristiti + Izbriši repozitorij + Preuzmi popis stranica koje želiš koristiti Preuzeto: %d Onemogućeno: %d Nepreuzeto: %d - CloudStream nema instalirane web stranice prema zadanim postavkama. Morate instalirati stranice iz repozitorija. + CloudStream standardno nema instalirane web stranice. Stranice morate instalirati iz repozitorija. \n \nPridružite se našem Discordu ili tražite online. - Pregledajte repozitorije zajednice + Prikaži repozitorije zajednice Javni popis - Svi titlovi pisani velikim slovima - Preuzeti sve dodatke iz ovog repozitorija\? - %s (Onemogućeno) - Zapis + Koristi velika slova za sve titlove + Preuzeti sve dodatke iz ovog repozitorija? + %s (onemogućeno) + Zapisi Audio zapis Video zapis - Primjenjuje se na ponovnom pokretanju - Sigurnosni način rada omogućen - Sve su ekstenzije isključene zbog rušenja aplikacije kako biste lakše pronašli ono koje uzrokuje probleme. + Za prikaz promjena ponovo pokreni aplikaciju. + Sigurnosni način rada uključen + Sva proširenja su isključena zbog rušenja aplikacije kako bi se pronašlo proširenje koje uzrokuje probleme. Pogledajte podatke o padu Ocjena: %s Opis @@ -454,38 +454,38 @@ Autori Podržano Jezik - HLS Playlista + HLS playlista Automatski instaliraj dodatke Zasluge Automatski instaliraj sve neinstalirane dodatke iz dodanih repozitorija. Preferirani video player Interni player - Prvo instalirajte ekstenziju + Najprije instaliraj proširenje VLC MPV - Web Video Cast + Emitiranje na webu Aplikacija nije pronađena Svi jezici Previše teksta. Nije moguće spremiti u međuspremnik. Označi kao gledano - Prikazuje skočni prozor za preskakanje početka ili završetka medija + Prikaži skočni prozor za uvod/kraj Da - Preuzimanje ažuriranja aplikacije… + Preuzimanje ažuriranja aplikacije … Jeste li sigurni da želite izaći\? Ne - Instaliranje ažuriranja aplikacije… + Instaliranje ažuriranja aplikacije … Nije moguće instalirati novu verziju aplikacije - Ažurirano %d dodataka - Mješoviti početak + Ažurirani dodaci: %d + Mješoviti uvod Uvod - Linkovi - Pokreni Trailer + Poveznice + Pokreni trailer Ponovi postupak postavljanja - Neki telefoni ne podržavaju novi program za instaliranje paketa. Isprobaj naslijeđenu opciju ako se ažuriranja ne instaliraju. + Neki telefoni ne podržavaju novi program za instaliranje paketa. Pokušaj sa starijom opcijom ako se ažuriranja ne instaliraju. Instalator APK-a Ažuriranja aplikacije Sigurnosna kopija - Ekstenzije + Proširenja Radnje Predmemorija Geste @@ -493,21 +493,21 @@ Titlovi Raspored Zadane postavke - Izgled + Izgledi Značajke Web preglednik Preskoči %s - Završetak - Zaključak - Mješoviti završetak - Obriši povijest + Kraj + Sažetak + Mješoviti kraj + Izbriši povijest Povijest Legacy - Otvaranje + Uvod PackageInstaller %1$s %2$d%3$s Aktualiziranje započeto - Program če se aktualizirati tijekom zatvaranja programa + Aplikacija će se aktualizirati tijekom zatvaranja Dodatak preuzet Ukloni iz pogledanog Preglednik @@ -525,19 +525,19 @@ Vaša je biblioteka prazna :( \nPrijavite se na račun biblioteke ili dodajte filmove / serije u svoju lokalnu biblioteku. Ova je lista prazna. Pokušajte se prebaciti na jednu drugu listu. - Pronađena datoteka sigurnog načina rada! -\nNe učitavaju se ekstenzije pri pokretanju dok se datoteka ne ukloni. - Prikazan player- iznos preskakanja - Količina preskakanja koja se koristi kada je player vidljiv - Player skriven - Količina preskakanja - Količina preskakanja koja se koristi kada je player skriven + Pronađena je datoteka sigurnog načina rada! +\nProširenja se ne učitavaju tijekom pokretanja dok se datoteka ne ukloni. + Prikazan player – Količina pomicanja + Količina pomicanja koja se koristi kada je player vidljiv + Player skriven – Količina pomicanja + Količina pomicanja koja se koristi kada je player skriven Android TV - Prošlo - Restart + Uspjelo + Pokreni ponovo Log - Početak - Neuspješno - Stop + Pokreni + Neuspjelo + Prekini Test pružatelja usluga Ažuriranje pretplaćenih emisija Epizoda %d izbačena! @@ -549,7 +549,7 @@ GitHub Proxy Neuspješno dohvaćanje GitHuba. Uključuje se jsdelivr proxy … Zaobilazi blokiranje neobrađenih GitHub URL-ova koristeći jsDelivr. Može uzrokovati kašnjenje ažuriranja nekoliko dana. - Preferirana kvaliteta gledanja (podatkovna mobilna mreža) + Preferirana kvaliteta gledanja (mobilni podaci) Profil %d Wi-Fi Mobilni podaci @@ -560,20 +560,20 @@ Pomoć Kvalitete Pozadina profila - Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s + Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA POGREŠKA i treba se odmah prijaviti %s Odaberi modus za filtriranje preuzimanja dodataka Onemogući U repozitoriju nisu pronađeni dodaci - Repozitorij nije pronađen, provjerite URL i pokušajte koristiti VPN - Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je video prioritet. + Repozitorij nije pronađen. Provjeri URL i pokušaj VPN + Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je prioritet videa. \n \nIzvor A: 3 \nKvaliteta B: 7 -\nImat će kombinirani prioritet videozapisa od 10. +\nImat će kombinirani prioritet videa od 10. \n \nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! Već si glasao/la - Učestalost rezervne kopije + Učestalost spremanja sigurnosne kopije %s uklonjeno iz favorita Favoriti %s dodano u favorite @@ -606,7 +606,7 @@ Preskoči odabir računa pri pokretanju Upravljanje računima Uredi račun - Linkovi ponovno učitani + Poveznice su ponovo učitane Rotiraj Prikaži gumb za prebacivanje orijentacije zaslona Omogućuje automatsko mijenjanje orijentacije zaslona na temelju orijentacije videa @@ -614,8 +614,8 @@ rotiraj_video_tipka automatski_rotiraj_video_tipka Obavijest za novu epizodu - Pretraži u ostalim proširenjima - Dodaje opciju brzine u playeru + Traži u drugim proširenjima + Dodaje opciju za brzinu u playeru Testiraj sva proširenja Ovaj je test namijenjen samo programerima i ne provjerava niti negira rad bilo kojeg proširenja. Prikaži preporuke @@ -624,9 +624,31 @@ Zaključaj s biometrijskim podatcima %s \npreostalo - Greška u pristupanju međuspremnika. Pokušaj ponovo. + Pogreška pri pristupanju međuspremnika. Pokušaj ponovo. Otključaj CloudStream Lozinka/PIN autentifikacija Ovaj uređaj ne podržava biometrijsku autentifikaciju Ovaj je ekran zatvoren zbog višestrukih neuspjelih pokušaja. Pokrenite aplikaciju ponovo. - + U redu + Deaktiviraj optimizaciju baterije + Audio knjiga + Medij + Korištenje baterije aplikacije već je postavljeno na neograničeno + Neuspjelo otvaranje podataka CloudStream aplikacije. + Favorit + Ukloni iz favorita + Glazba + Obnovi + Otključaj aplikaciju pomoću otiska prsta, ID-a lica, PIN-a, uzorka i lozinke. + Sljedeća u %s + Pogreška pri kopiranju. Kopirajte zapisnik i kontaktirajte podršku aplikacije. + Kako bi se osigurala neometana preuzimanja i obavijesti za pretplaćene TV emisije, CloudStream treba dopuštenje za rad u pozadini. Pritiskom gumba „U redu” bit ćete preusmjereni na informacije o aplikaciji. Tamo odaberite „Korištenje baterije aplikacije” i postavite potrošnju baterije na „Neograničeno”. Imajte na umu da ovo dopuštenje ne znači da će CS3 isprazniti vašu bateriju. Radit će u pozadini samo kada je potrebno, kao što je primanje obavijesti ili preuzimanje videa sa službenih proširenja. Ako odlučite otkazati, ovu postavku možete prilagoditi kasnije u „Opće postavke”. + Vaši CloudStream podaci su sada spremljeni u sigurnosnu kopiju. Iako je vjerojatnost mala, neki se uređaji mogu ponašati drugačije. Ako izgubite pristup aplikaciji, potpuno izbrišite podatke aplikacije i obnovite ih pomoću sigurnosne kopije. Ispričavamo se zbog mogućih neugodnosti. + Sezona %1$d epizoda %2$d izlazi + Cast mirror + Fcast + Odaberite uređaj za emitiranje + CloudStream Wiki + Računi + Sigurnost + \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 5533cdc0..717495a9 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -365,7 +365,7 @@ https://példa.hu/példa.mp4 Nem sikerült betölteni: %s Elkezdődött a(z) %1$d %2$s letöltése… - Töltse le az összes bővítményt ebből a tárolóból\? + Töltse le az összes bővítményt ebből a tárolóból? Biztonságos mód bekapcsolva Méret MPV @@ -592,4 +592,4 @@ A PIN 4 karakter hosszú kell legyen Auto elforgatás Az automatikus videó orientáció alapján való képernyő elforgatás bekapcsolása - + \ 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 d537a1d5..b570068c 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -428,7 +428,7 @@ Ganti subtitle jadi huruf besar semua Terunduh: %d Tidak terunduh: %d - Unduh semua plugin dari repositori ini\? + Unduh semua plugin dari repositori ini? Semua Umur %s (Tidak aktif) Trek @@ -638,4 +638,13 @@ Buku Audio Media Untuk memastikan unduhan dan pemberitahuan tanpa gangguan untuk acara TV berlangganan, CloudStream memerlukan izin untuk berjalan di latar belakang. Dengan menekan OK, Anda akan diarahkan ke Info aplikasi. Di sana, gulir ke Penggunaan baterai aplikasi dan atur penggunaan baterai ke Tidak Terbatas. Harap dicatat, izin ini tidak berarti CS3 akan menguras baterai Anda. Ini hanya akan beroperasi di latar belakang ketika diperlukan, seperti ketika menerima pemberitahuan atau mengunduh video dari ekstensi resmi. Jika Anda memilih untuk membatalkannya, Anda dapat menyesuaikan pengaturan ini nanti di Pengaturan Umum. - + Mengatur ulang + Musim %1$d Episode %2$d akan dirilis pada + Akan datang di %s + Cermin Cast + Pilih perangkat cast + Fcast + CloudStream Wiki + Keamanan + Akun + \ 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 040b0f31..1341b146 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -239,7 +239,7 @@ Errore del renderer Errore inaspettato nel player video Errore download, controlla i permessi di archiviazione - Chromecast + Episodio Chromecast Mirror Chromecast Riproduci in app Riproduci in %s @@ -427,7 +427,7 @@ Vedi le repository della community Lista pubblica Tutti i sottotitoli in maiuscolo - Scaricare tutti i plugin da questa repository\? + Attenzione: CloudStream 3 non si assume alcuna responsabilità per l\'utilizzo di estensioni di terze parti e non fornisce alcun supporto per esse! %s (Disabilitato) Tracce Traccia audio @@ -619,7 +619,7 @@ Autenticazione con password/PIN L\'autenticazione biometrica non è supportata su questo dispositivo Sblocca app con impronta digitale, Face ID, PIN, sequenza e password. - Questa schermata è stata chiusa a causa di più tentativi falliti. Riavvia l\'app. + Dopo alcuni tentativi falliti, il prompt si chiuderà. Riavvia semplicemente l\'app per riprovare. È stato eseguito il backup dei tuoi dati CloudStream. Sebbene questa possibilità sia molto bassa, tutti i dispositivi possono comportarsi in modo diverso. Nel raro caso in cui ti venga bloccato l\'accesso all\'app, cancella completamente i dati dell\'app e ripristina da un backup. Siamo molto spiacenti per qualsiasi inconveniente derivanti da questo. Non preferito %s @@ -637,4 +637,21 @@ L\'utilizzo della batteria dell\'app è già impostato su \"Senza restrizioni\" Musica Audiolibro - + Reimposta + Prossimamente tra %s + L\'episodio %2$d della stagione %1$d uscirà tra + Mirror cast + Seleziona dispositivo per cast + Fcast + Wiki di CloudStream + Conti + Sicurezza + Autenticazione locale + Immagine codice QR + Respingi + Apri repository + Visita %s sul tuo smartphone o computer e inserisci il codice sopra + Impossibile ottenere il codice PIN del dispositivo, prova l\'autenticazione locale + Il codice PIN è scaduto! + Il codice scadrà tra %1$dm %2$ds + \ 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 da2952a0..2af7c967 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -442,7 +442,7 @@ לא ניתן להתקין את הגרסה החדשה של האפליקציה הורדת אצווה תוסף - הורד את כל התוספים ממאגר זה\? + הורד את כל התוספים ממאגר זה? רצועות שמע מסלולים Web Video Cast @@ -550,4 +550,4 @@ \nיגרמו לעדיפות הסרטון להיות 10. \n \nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! - + \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index acb2cfc3..5c80d77e 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -242,4 +242,4 @@ 現在のエピソードが終了したら次のエピソードを開始する 長押しするとデフォルトにリセットされます ダウンロードを再開 - + \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 1a63050a..a8756d83 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -133,7 +133,7 @@ 백업 중 오류 %s 검색 라이브러리 - 계정 + 계정 및 보안 소스별로 구분된 검색 결과를 제공합니다 예고편 보기 Kitsu에서 포스터 보기 @@ -311,7 +311,7 @@ 커뮤니티 저장소 보기 공개 목록 모든 자막 대문자화 - 이 저장소에서 모든 플러그인을 다운로드하시겠습니까\? + 이 저장소에서 모든 플러그인을 다운로드하시겠습니까? %s (사용불가) 저장소 추가 저장소 이름 @@ -338,7 +338,7 @@ 로드된 백업 파일 정보 고급 검색 - 데이터를 보내지 않음 + 데이터를 보내지 않습니다 설정 프로세스 다시 실행 APK 인스톨러 Github @@ -527,4 +527,111 @@ 구독중 구독 %s 구독 취소 %s - + 보안 + 장부 + 리포지토리에서 플러그인을 찾을 수 없습니다 + 복사됨! + 레포지토리 이름 및 URL + 본 테스트는 개발자만을 대상으로 하며, 확장자의 작업을 확인하거나 거부하지 않습니다. + 클라우스스트림 위키 + 다시 기록된 링크 + 백업 빈도 + 즐겨찾기 + QR 이미지 + 모든 확장프로그램 테스트 + 로컬 인증 + 클립보드에 액세스하는 중 오류가 발생했습니다. 다시 시도하십시오. + 취소 + 저장소 열기 + 현재 PIN 입력 + 비디오 방향에 따라 화면 방향을 자동으로 전환합니다 + 장치 PIN 코드를 가져올 수 없습니다, 로컬 인증을 시도하세요 + PIN 코드가 만료되었습니다! + 코드 만료까지 남은 시간: %1$dm %2$ds + 리포지토리를 찾을 수 없습니다. URL을 확인하고 VPN을 시도하십시오 + 이미 투표했습니다 + UI를 올바르게 만들 수 없습니다. 이것은 주요 버그이며 %s 즉시 보고해야 합니다 + 즐겨찾기에 추가 + 와이파이 + 도움 + 품질 + 편집 + 프로필 + 확인 + 배터리 최적화 사용 안 함 + 앱 배터리 사용량이 이미 무제한으로 설정되었습니다 + CloudStream의 App 정보를 열 수 없습니다. + 즐겨찾기에 %s 추가 + 프로필 %d + 프로필 배경 + 대체 + PIN 입력 + PIN + PIN은 4자여야 합니다 + %s으로 로그인 됨 + 시작 시 계정 선택 건너뛰기 + 즐겨찾기 + 즐겨찾기 해제 + 잠금 해제 + 생체 인식으로 잠금 + 음악 + 오디오책 + 자동 회전 + 모바일 데이터 + 사용 불가능 + fcast + 캐스트 장치 선택 + 복사하는 중 오류가 발생했습니다. 로그캣을 복사하고 문의하십시오. + 구독 취소 + 기본값 설정 + 구독 + 사용 + 당신의 라이브러리에 이미 잠재적으로 중복된 항목이 존재합니다: \'%s\'. +\n +\n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? + 전부 대체 + 추가 + 즐겨찾기에서 %s 제거 + 당신의 라이브러리에 잠재적으로 중복된 항목이 발견되었습니다: +\n +\n%s +\n +\n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? + 계정 선택 + 기본 계정 사용 + 회전 + 화면 방향을 전환할 토글 버튼 표시 + 계정 관리 + 프로필 잠금 + 잘못된 PIN입니다. 다시 시도하세요. + 계정 편집 + 미디어 + 비밀번호/PIN 인증 + 이 장치에서는 생체 인식이 지원되지 않습니다 + 지문, 얼굴 ID, PIN, 패턴 또는 비밀번호로 앱을 잠급니다. + 여러 번 실패하면 프롬프트가 닫힙니다. 다시 시도하려면 앱을 다시 시작하세요. + 재설정 + 플러그인 다운로드를 필터링할 모드 선택 + 데이터가 백업되었습니다. 장치에 따라 동작이 다를 수 있으며 앱 접근이 차단될 경우 앱 데이터를 완전히 지우고 백업에서 복원하세요. 이로 인해 발생하는 불편을 사과드립니다. + 스마트폰이나 컴퓨터에서 %s를 방문하여 위의 코드를 입력하세요 + 구독 TV 프로그램에 대한 중단 없는 다운로드 및 알림을 보장하기 위해 CloudStream은 백그라운드에서 실행할 수 있는 권한이 필요합니다. 확인을 누르면 App info로 이동합니다. 거기서 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚로 스크롤하여 배터리 사용량을 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙로 설정합니다. 이 권한은 CS3가 배터리를 소모한다는 의미가 아닙니다. 알림을 받거나 공식 확장에서 동영상을 다운로드하는 등 필요할 때만 백그라운드에서 작동합니다. 취소를 선택한 경우 나중에 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨에서 이 설정을 조정할 수 있습니다. + 여기서 소스의 순서를 변경할 수 있습니다. 비디오의 우선 순위가 높은 경우에는 소스 선택에 더 높게 나타납니다. 소스 우선 순위와 품질 우선 순위의 합이 비디오 우선 순위입니다. +\n +\n참고 A: 3 +\n품질 B: 7 +\n총 비디오 우선 순위는 10입니다. +\n +\n참고: 합이 10 이상이면 해당 링크가 로드되면 플레이어는 자동으로 로드를 건너뜁니다! + 시즌 %1$d 에피소드 %2$d이(가) 출시됩니다 + 다른 확장자에서 검색 + 새로운 에피소드 알림 + 권장 사항 표시 + 플레이어에 속도 옵션을 추가합니다 + %s로 출시 예정 + %s +\n남음 + 잠재적 중복 발견 + %s의 PIN 입력 + 즐겨찾기에서 제거 + 캐스트미러 + \ No newline at end of file diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 49b333e3..7989654a 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -415,7 +415,7 @@ Skatīt kopienas krātuves Publisks saraksts Visi subtitri ar lielajiem burtiem - Vai lejupielādēt visus spraudņus no šīs krātuves\? + Vai lejupielādēt visus spraudņus no šīs krātuves? %s (atspējots) Tracks Audio dziesmas @@ -527,4 +527,4 @@ Abonēto šovu atjaunināšana Abonēts Abonēts %s - + \ 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 fe82a90b..05fc0900 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -89,7 +89,7 @@ Отстранете ги црните граници Преводи Поставки на плеерот за преводи - Режим на Eigengravy + Брзина на репродукција Повлечете за да барате Повлечете од страна на страна за да ја контролирате вашата позиција во видеото Повлечете за да ги промените поставките @@ -186,7 +186,7 @@ Зумирај Disclaimer Општи поставки - Јазици на провајдерите + Јазици на екстензиите Распоред на апликацијата Претпочитани медиуми Автоматски @@ -239,7 +239,7 @@ Видео Исчисти Положен - MyCoolSite + Име на сајт Неважечки податоци Поддршка Функции на плеерот @@ -250,11 +250,11 @@ Опис Апликацијата ќе се ажурира по излегувањето Отпишана е од %s - прокси raw.githubusercontent.com + GitHub прокси TC Претплатен на %s Преводи - Да се преземат сите приклучоци од ова складиште\? + Да се преземат сите приклучоци од ова складиште? Недостасуваат дозволи за складирање. Обидете се повторно. Зачувај Вчитај од датотека @@ -299,7 +299,7 @@ MPV Инсталатор на пакети ОВА - Ажурирања и резервни копии + Ажурирање и резервна копија Вашата библиотека е празна :( \nНајавете се на корисничка сметка или додадете серии. Не се пронајдени епизоди @@ -337,7 +337,7 @@ Додатоци Прикажи случајно копче на почетната страница и библиотеката Поддржано - Сметки + Сметки и безбедност Вовед Креирај сметка Отстрани од гледаното @@ -349,7 +349,7 @@ Ажурирани %d приклучоци Мешано отворање Екстензии - Овозможете NSFW на поддржани провајдери + Овозможете NSFW на поддржани екстензии Не успеа да стигне до GitHub. Вклучувам jsDelivr прокси… Филтрирајте по претпочитан медиумски јазик @string/home_play @@ -381,7 +381,7 @@ Прикажи постери од Kitsu Дали сте сигурни дека сакате да излезете\? Предизвикува проблеми ако е превисоко поставено на уреди со мал простор за складирање, како што е Android TV. - Користејќи jsDelivr, блокирањето на GitHub може да се заобиколи. Може да ги одложи ажурирањата за неколку дена. + Заобиколете го блокирањето на необработени URL-адреси на github користејќи jsDelivr. Може да предизвика ажурирањата да се одложат за неколку дена. Да Азбучно (Ш до А) WP @@ -398,7 +398,7 @@ Инсталатор на APK Екстензии UHD - Референт + Референт (опционално) Се отвора 127.0.0.1 Ова исто така ќе се избрише сите приклучоци за складиште @@ -409,7 +409,7 @@ Не успеа да ги врати податоците од датотеката %s Не успеа Документарец - Стрим + Мрежен проток %d мин Играј со CloudStream Пушти трејлер @@ -433,7 +433,7 @@ %dm \nпреостанува Видео кеш на дискот - Поврзување до пренос + https://example.com/example.mp4 Готово Додај складиште 18+ @@ -449,7 +449,7 @@ SDR Веб-прелистувач Апликацијата не е пронајдена - MyCoolUsername + Корисничко име Отвори со %1$s %2$d%3$s Повторете го процесот на поставување @@ -480,9 +480,7 @@ Приклучокот е преземен Не може да се вчита %s Преземете ја листата на сајтови што сакате да ги користите - CloudStream нема стандардно инсталирани локации. Треба да ги инсталирате сајтовите од складиштата. -\n -\nПоради отстранување на DMCA без мозок од страна на Sky UK Limited 🤮 не можеме да ја поврземе локацијата на складиштето во апликацијата. + CloudStream нема стандардно инсталирани екстензии. Треба сами да инсталирате екстензии. \n \nПридружете се на нашиот Discord или барајте онлајн. Песни @@ -507,9 +505,9 @@ Завршува Измешан крај HDR - example.com + https://example.com Синхронизирај преводи - Примени при рестартирање + Рестартирајте ја апликацијата за да ги видите промените. Наслов на видео плеер максимални знаци Увезете фонтови ставајќи ги во %s Врати ги податоците од резервна копија @@ -591,4 +589,39 @@ Зачестеност на зачувување на бекап Овозможете автоматско префрлување на ориентацијата на екранот врз основа на видео ориентација Автоматска ротација - + Име и URL на складиштето + копирано! + Тестирај ги сите екстензии + ОК + Користењето на батеријата на апликацијата е веќе поставено на неограничено + Неомилен + Омилен + Заклучување со биометрика + Музика + Известување за нова епизода + Пребарајте во други екстензии + Прикажи препораки + Додава опција за брзина во плеерот + Cast mirror + Овој тест е наменет само за програмери и не ја потврдува или негира работата на која било екстензија. + Не може да се отворат информациите за апликацијата CloudStream. + Лозинка/ПИН автентикација + Отклучете ја апликацијата со отпечаток од прст, ID на лице, PIN, шема и лозинка. + Сега е направена резервна копија на вашите податоци на CloudStream. Иако можноста за ова е многу мала, сите уреди можат да се однесуваат поинаку. Во ретки случаи, кога ќе се заклучите од пристап до апликацијата, целосно исчистете ги податоците на апликацијата и вратете ги од резервна копија. Многу ни е жал за какви било непријатности што произлегуваат од ова. + Ресетирај + Сезона %1$d Епизода %2$d ќе биде објавена за + Fcast + Одбери уред да кастираш + Оневозможи оптимизација на батерија + Отклучи CloudStream + Биометриската автентикација не е поддржана на овој уред + Овој екран беше затворен поради повеќе неуспешни обиди. Ве молиме рестартирајте ја апликацијата. + Медиуми + Претстои во %s + %s +\nпреостанати + За да обезбеди непрекинато преземања и известувања за претплатени ТВ-серии, на CloudStream му треба дозвола да работи во заднина. Со притискање на ОК, ќе бидете упатени до информации за апликацијата. Таму, дојдете до 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и поставете ја употребата на батеријата на Неограничено. Ве молиме имајте предвид, оваа дозвола не значи дека CS3 ќе ви ја испразни батеријата. Ќе работи само во заднина кога е потребно, како на пример при примање известувања или преземање видеа од официјални екстензии. Ако изберете да откажете, може да ја прилагодите оваа поставка подоцна во 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. + Грешка при пристапот до таблата со исечоци, обидете се повторно. + Грешка при копирање, копирајте го logcat и контактирајте со поддршката за апликацијата. + Аудио книга + \ No newline at end of file diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 279f5511..0a0f7bd7 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -167,7 +167,7 @@ ഔചിത്യ വീഡിയോ ക്വാളിറ്റി ചരിത്രം കണ്ടതാണെന്ന് അടയാളപ്പെടുത്തുക - %d ദിവസങ്ങൾ %d മണിക്കൂർ %d മിനിറ്റ് + %1$d ദിവസങ്ങൾ %2$d മണിക്കൂർ %3$d മിനിറ്റ് അധ്യായം%dൽ റിലീസ് ചെയ്യും %1$d മണിക്കൂർ %2$d മിനിറ്റ് %1$sഅധ്യാ%2$d @@ -280,4 +280,4 @@ എഡ്ജ് തരം ഔട്ട്ലൈൻ നിറം പശ്ചാത്തല നിറം - + \ No newline at end of file diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 0c90b0c2..8170a7ff 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -55,4 +55,6 @@ Kongsi Tetapan Tutup - + Ep + cuba + \ No newline at end of file diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index ef796f9f..4bf2a273 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -417,7 +417,7 @@ ဖြည့်စွက်များ ရီပိုစစ်ထရီ ဖြည့်စွက်များအားလုံးကိုဖျက်မည်ဖြစ်သည် ရီပိုစစ်ထရီ ကိုဖျက်ရန် - ဤရီပိုစစ်ထရီမှ ဖြည့်စွက်များအားလုံးကို ဒေါင်းလုဒ်လုပ်မှာလား\? + ဤရီပိုစစ်ထရီမှ ဖြည့်စွက်များအားလုံးကို ဒေါင်းလုဒ်လုပ်မှာလား? %s (ပိတ်ပြီး) ထောက်ပံ့ထားသော ဘာသာစကား @@ -550,4 +550,4 @@ သင်နဂိုတည်းကသတ်မှတ်ပြီး လိုက်ဘရီရွေးချယ်ရန် ဖြင့်ဖွင့်မည် - + \ No newline at end of file diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 1e23f8af..99694e91 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -53,7 +53,7 @@ डाउनलोड रद्द गरियो डाउनलोड भयो अपडेट सुरु - स्ट्रिम + नेटवर्क स्ट्रीम लिङ्क लोड गर्दा त्रुटि भयो लिङ्कहरू रिलोड गरियो भित्री स्टोरेज @@ -70,7 +70,7 @@ बुकमार्कहरू फिल्टर गर्नुहोस् बुकमार्कहरू हटाउनुहोस् - हेरेको स्थिति राख्नुहोस् + हेरेको स्थिति निर्धारण गर्नुहोस् कपी बन्द खाली गर्नुहोस् @@ -85,4 +85,47 @@ स्रोतहरू स्वचालित बग रिपोर्टिङ असक्षम गर्नुहोस् लागू गर्नुहोस् - + साइट ले मेटाडाटा दिएको छैन,मेटाडाटा बिना भिडियो लोड नहुन सक्छ। + प्रकरण %1$d प्रसङ्ग %2$d प्रशारण हुनेवाला छ + प्रोभाईडर उपयोग गरी खोज्नुहोस् + भाषा डाउनलाेड गर्नुहोस् + उपशीर्षकको भाषा + यो प्रोभाईडर torrent हो त्यसैले VPN प्रयाेग गर्नुहुन सिफारिश गरिन्छ + वर्णन + केही विषय भेटिएन + Chromecast को उपशीर्षकहरु + केहीपनि वर्णन भेटिएन + Logcat देखाउनुहोस + Log + Picture-in-picture + अरु एप माथी सानो प्लेयरमा पलेब्याक जारी राख्दछ + प्लेयर स्पीड + उपशीर्षकको सेटिङ + विन्डोको रंग + उपशीर्षक ऊंचाई + अक्षरको नाप + फन्ट + प्रकारको उपयोग गरी खोज्नुहोस् + %d केरा डेभलपर लाई दिइयो + एउटै पनी केरा दिइएन + हेर्न सुचारु राख्नुहोस + हटाउनुहोस् + कालो सीमा हटाउनुहोस + उपशीर्षक + नयाँ प्रसङ्ग को सूचना + अन्य एक्सटेन्सन मा खोज्नुहोस् + सुुझाव हरु + अक्षरको रंग + बाहिरी रेखा को रंग + पृष्ठभूमिको रंग + धारको प्रकार + भाषा अटो छनौट + रिसेट गर्न स्क्रिनमा थिचिराख्नुहोस् + फन्ट देखाउन %s मा राख्नुहोस् + अधिक जानकारी + यो प्रोभाईडर सही ढंगले प्रयोग गर्न VPN प्रयोग गर्नुपर्ने हुन सक्छ + प्लेयर resize गर्ने वटन + प्लेयरको उपशीर्षकको सेटिङ + रिपोजिटरी को नाम र यूआरएल + कपी गरियो! + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index fc537837..8844407a 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -487,7 +487,7 @@ Uitbreidingen Intro Publieke lijst - Alle plugins uit deze repository downloaden\? + Alle plugins uit deze repository downloaden? Beoordeling: %s Alle extensies zijn uitgeschakeld door een crash om u te helpen degene te vinden die problemen veroorzaakt. Bekijk de crash info @@ -608,4 +608,4 @@ Link opnieuw geladen Autoroteer Roteer - + \ 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 724f4a63..b1168c36 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -335,7 +335,7 @@ Last ned listen over sider du vil bruke Dette vil også slette alle pakkebrønnsprogramtillegg Vis gemenskapspakkebrønner - Last ned alle programtilleggene fra denne pakkebrønnen\? + Last ned alle programtilleggene fra denne pakkebrønnen? %s (avskrudd) Spor Fant ikke programmet @@ -538,4 +538,4 @@ Bruk Hjelp Profilbakgrunn - + \ 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 c61f0104..4980c235 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -400,7 +400,7 @@ Zobacz repozytoria społeczności Publiczna lista Wszystkie napisy wielką literą - Pobrać wszystkie rozszerzenia z tego repozytorium\? + Uwaga: CloudStream 3 nie ponosi żadnej odpowiedzialności za korzystanie z rozszerzeń innych dostawców i nie zapewnia dla nich żadnego wsparcia! %s (Wyłączone) Ścieżki Ścieżki audio @@ -597,7 +597,7 @@ Ten test jest przeznaczony wyłącznie dla programistów i nie weryfikuje ani nie zaprzecza działaniu żadnego rozszerzenia. Zablokuj za pomocą biometrii Uwierzytelnianie hasłem/kodem PIN - Ten ekran został zamknięty z powodu wielu nieudanych prób. Uruchom ponownie aplikację. + Po kilku nieudanych próbach monit zostanie zamknięty. Aby spróbować ponownie, po prostu uruchom ponownie aplikację. Odblokuj CloudStream To urządzenie nie obsługuje uwierzytelniania biometrycznego Odblokuj aplikację za pomocą odcisku palca, identyfikatora twarzy, kodu PIN, wzoru i hasła. @@ -618,4 +618,21 @@ Multimedia Użycie akumulatora przez aplikację jest już ustawione na nieograniczone Aby zapewnić nieprzerwane pobieranie i powiadomienia o subskrybowanych programach telewizyjnych, CloudStream potrzebuje pozwolenia na działanie w tle. Naciskając OK, zostaniesz przekierowany do informacji o aplikacji. Tam przewiń do użycia akumulatora przez aplikację i ustaw je na nieograniczone. Pamiętaj, że to pozwolenie nie oznacza, że CS3 będzie zużywać akumulator. Będzie działać w tle tylko wtedy, gdy będzie to konieczne, na przykład podczas odbierania powiadomień lub pobierania filmów z oficjalnych rozszerzeń. Jeśli zdecydujesz się anulować, możesz dostosować to ustawienie później w ustawieniach głównych. - + Resetuj + Nadchodzące w %s + Odcinek %2$d sezonu %1$d wyjdzie za + Fcast + Wybierz urządzenie do transmisji + Mirror transmisji + Wiki CloudStream + Bezpieczeństwo + Konta + Uwierzytelniaj lokalnie + Obraz kodu QR + Nie można uzyskać kodu PIN urządzenia. Spróbuj uwierzytelnienia lokalnego + Kod PIN stracił ważność! + Kod wygasa za %1$dm %2$ds + Odrzuć + Otwórz repozytorium + Odwiedź %s na swoim smartfonie lub komputerze i wprowadź powyższy kod + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 06e2352c..999ebefb 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -379,7 +379,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) Instalador APK %d min @@ -615,4 +615,10 @@ Multimédia Desativar a otimização da bateria Para garantir descarregamentos ininterruptos e notificações de programas de TV subscritos, o CloudStream precisa de permissão para ser executado em segundo plano. Ao premir OK, será direcionado para informações da aplicação. Aí, desloque-se para utilização da bateria da aplicação e defina a utilização da bateria para sem restrições. Tenha em atenção que esta permissão não significa que o CS3 irá esgotar a sua bateria. Este só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se optar por cancelar, pode ajustar esta definição mais tarde em definições gerais. - + Reiniciar + Episódio %1$d Episódio %2$d vai ser lançado em + Por vir em + Fcast + Escolha o dispositivo + Transmitir + \ 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 d7da44b4..30804c4d 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -59,7 +59,7 @@ Descărcare eșuată Descărcare anulată Descărcare finalizată - Stream + Stream de rețea Eroare la încărcarea linkurilor Stocare internă Dub @@ -142,7 +142,7 @@ Permisiunea de arhivare lipșe, vă rugăm să încercați din nou. Eroare de backup %s Căutare - Conturi și credite + Conturi și Securitate Actualizări și copii de rezervă Informații Căutare avansată @@ -255,8 +255,8 @@ Lungimea buffer-ului video Dimensiunea cache-ului video pe disc Ștergeți memoria cache de imagine și video - Provoacă blocaje dacă este setată la un nivel prea ridicat pe dispozitive cu memorie redusă, cum ar fi Android TV. - Cauzează probleme dacă este setat la un nivel prea ridicat pe dispozitive cu spațiu de stocare redus, cum ar fi Android TV. + Cauzează blocări dacă este setat prea mare pe dispozitive cu memorie redusă, cum ar fi Android TV. + Cauzează probleme dacă este setat prea mare pe dispozitive cu spațiu de stocare redus, cum ar fi Android TV. DNS peste HTTPS Folositor pentru evitarea blocajelor ISP Adaugați site-ul @@ -272,8 +272,8 @@ 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 și în bibliotecă - Limba furnizorului + Afișează butonul pentru aleatoriu pe Pagina Principală și în Bibliotecă + Limbi ale extensiei Aplicație de prezentare Media preferată Codificarea subtitrărilor @@ -309,7 +309,7 @@ /\?\? /%d %s autentificat/ă - Nu s-a putut autentifica la %s + Nu am putut să mă autentific la %s Nu există Normal @@ -332,7 +332,7 @@ https://en.wikipedia.org/w/index.php?title=Pangram&oldid=225849300 https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog --> - Vând muzică de jazz și haine de bun-gust în New-York și Quebec la preț fix. + Vulpea maro iute sare peste câinele leneș Recomandări A fost încărcat %s Încărcați din fișier @@ -343,7 +343,7 @@ Secundar Sursa Aleatoriu - În curând + În curând… Cam Cam Cam @@ -365,7 +365,7 @@ Titlu și rezoluție Titlu Rezoluție - ID invalid + ID-ul invalid Date invalide Eroare @@ -394,14 +394,14 @@ %1$d %2$s NSFW %1$d-%2$d - Player Afișat - Căutați Suma - Player Ascuns/ă - Căutați Suma + Jucătorul afișat - Cantitatea de căutare + Jucător ascuns - Sumă de căutare Livestream-uri NSFW Eșuat - Suma căutată și utilizată atunci când player-ul este vizibil/ă + Suma de căutare utilizată atunci când jucătorul este vizibil Livestream - Cantitatea de căutare utilizată atunci când playerul este ascuns + Cantitatea de căutare folosită când jucătorul este ascuns Calitatea preferată (Date Mobile) Video Instalator APK @@ -426,11 +426,11 @@ Dezabonat de la %s Nu s-a descărcat: %d Vezi depozite din comunitate - PackageInstaller (Instalare a pachetelor) + Instalator de pachete Stare Nu se poate încărca %s Piste audio - Referent + Referer (opțional) Deschidere Extensii Layout @@ -440,7 +440,8 @@ Autori Raportarea accidentelor Adaugă depozit - Se pare că biblioteca ta este goală :( Conectează-te la un cont de bibliotecă sau adaugă emisiuni în biblioteca ta locală. + Biblioteca ta este goală :( +\nConectați-vă într-un cont de bibliotecă sau adăugați emisiuni la biblioteca locală. Eliminați subtitrările închise din subtitrări Descărcați lista de site-uri pe care doriți să le utilizați Evaluare (Ridicat la Scăzut) @@ -453,7 +454,7 @@ Vezi informații despre accident Deschideți cu Eliminați bloat din subtitrări - Actualizat %d plugin-uri + S-au actualizat %d plugin-uri Evaluare (Scăzut la Ridicat) Terminat Versiune @@ -472,11 +473,11 @@ Sortează Selectați Biblioteca Filtrați în funcție de limba media preferată - Episodul %d lansat! + Episodul %d a fost lansat! Android TV VLC Urmăriți videoclipuri în aceste limbi - Reveniți + Revenire Acțiuni Alfabetic (Z la A) URL invalid @@ -492,7 +493,7 @@ \nNu încarcă nicio extensie la pornire până când fișierul nu este eliminat. Scoateți de la urmărit Actualizat (Vechi la Nou) - Aplică la repornire + Reporniți aplicația pentru a vedea schimbările. Descriere Plugin Descărcat Sunteți sigur că vreți să ieșiți\? @@ -508,16 +509,16 @@ Nu s-a putut instala noua versiune a aplicației Piste Repornește - Activează NSFW la furnizori suportate + Activează conținutul pentru adulți pe extensiile suportate Nu s-a putut ajunge la GitHub. Se activează proxy-ul jsDelivr… Proxy GitHub - Depășește blocarea GitHub folosind jsDelivr. Poate cauza întârzieri de câteva zile la actualizări. + Ocolește blocarea URL-urilor brute de pe GitHub folosind jsDelivr. Poate cauza întârzieri în actualizări cu câteva zile. Următorul Toate %s deja descărcate - S-a descărcat: %d + Descărcat: %d Dezactivat: %d Toate subtitrările cu majuscule - Descărcați toate plugin-urile din acest depozit\? + Descărcați toate plugin-urile din acest depozit? Se actualizează emisiunile abonate Abonat Lista publică @@ -532,7 +533,7 @@ Suportat Playlist HLS Piste video - Arată Afișați pop-up-uri de săritură pentru deschidere/încheiere + Afișează opțiunea de omitere a ferestrelor pop-up pentru început/sfârșit Toate limbile Deschidere mixat Credite @@ -593,4 +594,51 @@ Adaugă o opțiune de viteză la player Favoriți/te Frecvența de backup - + Numele și URL-ul depozitului + Copiat! + Eroare la accesarea Clipboard-ului. Te rog să încerci din nou. + Eroare la copiere. Te rog să copiezi logcat-ul și să contactezi suportul aplicației. + PIN incorect. Te rog să încerci din nou. + PIN + Selectați un cont + Administrați conturile + Editare cont + Conectat ca %s + Rotire + Nefavorite + Deblocați CloudStream + Omiteți selecția contului la pornire + Linkuri reîncărcate + Utilizați contul implicit + Această testare este destinată doar dezvoltatorilor și nu verifică sau respinge funcționarea oricărei extensii. + Pentru a asigura descărcările neîntrerupte și notificările pentru serialele TV la care ești abonat, CloudStream are nevoie de permisiunea de a rula în fundal. Apăsând pe OK, vei fi direcționat către informațiile aplicației. Acolo, derulează la \"App battery usage\" și setează utilizarea bateriei la \"Unrestricted\". Te rog să reții, această permisiune nu înseamnă că CS3 îți va consuma bateria. Va opera în fundal doar când este necesar, cum ar fi atunci când primește notificări sau descarcă videoclipuri din extensiile oficiale. Dacă alegi să anulezi, poți ajusta această setare mai târziu în \"General Settings\". + PIN-ul trebuie să fie format din 4 caractere + Afișează un buton de comutare pentru orientarea ecranului + Autentificare parolă/PIN + Autentificarea biometrică nu este acceptată pe acest dispozitiv + Deblocați aplicația cu amprentă digitală, ID facial, PIN, model și parolă. + Acest ecran a fost închis din cauza mai multor încercări eșuate. Vă rugăm să reporniți aplicația. + Datele dvs. CloudStream au fost salvate acum. Deși posibilitatea acestui lucru este foarte mică, toate dispozitivele se pot comporta diferit. În cazul rar, în care nu aveți acces la aplicație, ștergeți complet datele aplicației și restaurați dintr-o copie de rezervă. Ne pare foarte rău pentru orice neplăcere care decurge din aceasta. + Ok + Dezactivează optimizarea bateriei + Utilizarea bateriei pentru aplicație este deja setată ca fiind nelimitată + Imposibil de deschis informațiile aplicației CloudStream. + Favorite + Muzică + Carte audio + Media + Caută în alte extensii + Testează toate extensiile + Rotire automată + Resetați + Activați comutarea automată a orientării ecranului pe baza orientării video + Blocare cu biometrie + %s +\nrămase + Următorul în %s + CloudStream Wiki + Sezonul %1$d Episod %2$d va fi lansat în + Selectați divece-ul pe care doriți să faceți cast + Cast mirror + Fcast + \ 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 cf456f56..79aa66e1 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -422,6 +422,7 @@ %s (отключено) Далее В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев. +\n \nПрисоединяйтесь к нашему Discord-серверу или найдите в интернете. Недопустимые данные Разрешение и название @@ -450,7 +451,7 @@ Все %s уже скачаны Начата загрузка %1$d %2$s… Не скачано: %d - Скачать все плагины из этого репозитория\? + Скачать все плагины из этого репозитория? Включен безопасный режим Скачано: %d Обновлено %d плагинов @@ -616,4 +617,9 @@ Этот экран был закрыт из-за нескольких неудачных попыток. Пожалуйста, перезапустите приложение. Ваши данные в CloudStream были скопированы. Хотя вероятность этого очень мала, все устройства могут вести себя по-разному. В редких случаях, когда доступ к приложению заблокирован, полностью удалите данные приложения и восстановите их из резервной копии. Мы приносим свои извинения за любые неудобства, связанные с этим. Чтобы обеспечить бесперебойную загрузку и получение уведомлений о телепередачах, на которые вы подписаны, CloudStream необходимо разрешение на запуск в фоновом режиме. Нажав OK, вы перейдете к информации о приложении. Там перейдите к разделу 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и установите значение \"Использование батареи\" 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Пожалуйста, обратите внимание, что это разрешение не означает, что CS3 разрядит вашу батарею. Он будет работать в фоновом режиме только при необходимости, например, при получении уведомлений или загрузке видео с официальных расширений. Если вы решите отменить, вы можете изменить эту настройку позже в 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. - + Сброс + Сезон %1$d Эпизод %2$d выйдет + Выйдет %s + Fcast + Выберите девайс для трансляции + \ 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 index ebaaa2ae..5ba29a00 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -112,7 +112,7 @@ Sledujem Popis sa nenašiel Ďalší náhodný - Stream + Sieťový stream Podržané Zobraziť Logcat 🐈 Protokol @@ -355,4 +355,26 @@ Maximálny počet znakov v názve prehrávača Spôsobuje problémy, ak je nastavená príliš vysoko v zariadeniach s malým ukladacím priestorom, ako je napríklad Android TV. Frekvencia zálohovania - + Toto tiež odstráni všetky doplnky repozitára + Sezóna %1$d Epizóda %2$d bude vydaná za + V repozitári neboli nájdené žiadne doplnky + Sťahuje sa aktualizácia aplikácie… + Inštaluje sa aktualizácia aplikácie… + skopírované! + Názov a URL repozitára + Aktualizujú sa odoberané relácie + Upozornenie na novú epizódu + Repozitár nebol nájdený, skontrolujte URL adresu a skúste VPN + Odkazy sa znovu načítali + Zmazať repozitár + URL adresa repozitára + Verejný zoznam + CloudStream nemá nainštalované žiadne stránky v predvolenom nastavení. Musíte nainštalovať stránky z repozitára. +\n +\nPripojte sa k nášmu Discord alebo vyhľadajte online. + Nepodarilo sa nainštalovať novú verziu aplikácie + Stiahnuť všetky doplnky z tohto repozitára? + Pridať repozitár + Názov repozitára + Zobraziť komunitné repozitáre + \ 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 index 7b0d2870..90198dd5 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -440,7 +440,7 @@ \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\? + Soo deji dhamaan sidkanayaasha reboositarkan? Boodhka Boodhka xalqadda Boodhka weyn @@ -485,4 +485,4 @@ 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 76508c43..695cbd31 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -33,7 +33,7 @@ Försök ansluta igen… Gå tillbaka @string/result_poster_img_des - Spela Avsnitt + Spela avsnitt Ladda ner Intern lagring Dub @@ -71,7 +71,7 @@ Håll inne för att återställa till standard Fortsätt titta Ta bort - Mer information + Mer info En VPN kan behövas för att den här leverantören ska fungera korrekt Denna leverantör är en torrent, en VPN rekommenderas Beskrivning @@ -162,12 +162,12 @@ Visa inte igen Uppdatera Nerladdningar startad - Nerladdning Misslyckades + Nerladdning misslyckades Nerladdad Laddar ner - Nerladdning Pausad - Nerladdning Avbryten - Nerladdning Färdig + Nerladdning pausad + Nerladdning avbryten + Nerladdning färdig Återupta nerladdning Pausa nerladdning Pausa @@ -204,7 +204,7 @@ Logga ut konto Nerladdningsplats - Tittar på nytt + Ser om Automatisk DNS över HTTPS " " @@ -217,7 +217,7 @@ Dubbeltryck i mitten för att pausa Återställ data från backup Konton och säkerhet - Uppdateringar och backup + Uppdateringar och säkerhetskopiering Automatiska pluginuppdateringar %1$dd %2$dh %3$dm Sök %s… @@ -230,7 +230,7 @@ Autospela nästa episod Spela Trailer Starta nästa episod när nuvarande slutar - Episod %d kommer släppas om + Episod %d kommer att släppas om %d min Visa trailers @string/home_play @@ -366,7 +366,7 @@ Titta på videor på dessa språk Föregående Spår - Uppdatering påbörjad + Uppdatering startad Logg Videospelarens hoppsträcka (Sekunder) Ändra status @@ -436,7 +436,7 @@ All %s har redan laddats ner Ladda ner alla tillägg från den här databasen? Felsäkert läge på - Applicera vid omstart + Starta om appen för att se ändringar. Intern spelare Kamera HD Tillägg nedladdad @@ -460,7 +460,7 @@ Avsnitt %d släppt! Den här listan är tom. Försök byta till en annan. Tillägg borttagen - Tillägg laddade + Tillägg laddad Tillägg Säkerhetskopierings antal Uppdatera visnings förlopp @@ -490,7 +490,7 @@ Web Affischbild Vad vill du se - Lägg till databas + Lägg till tillägg Uppdaterade %d tillägg Nedladdat %1$d %2$s Inaktiverad: %d @@ -524,7 +524,7 @@ Förbikoppla ISP Kamera Kamera - Alla tillägg stängdes av på grund av en krasch för att hjälpa dig hitta den som orsakar problem. + Alla tillägg stängdes av på grund av en krasch för att hjälpa dig hitta det tillägget som orsakar problem. Storlek Författarna Stödd @@ -599,9 +599,31 @@ Lås upp appen med Fingerprint, Face ID, PIN, mönster eller lösenord. Lås upp CloudStream Biometrisk autentisering stöds inte på den här enheten - Detta fönster stängs efter några misslyckade försök. Du måste starta om appen. + Skärmen stängdes av på grund av flera misslyckade försök. Starta om applikationen. Favorit Ta bort från favoriter %s \nkvarstår - + kopierad! + Tilläggs namn och URL + För att säkerställa oavbrutna nedladdningar och aviseringar för prenumererade tv-program behöver CloudStream tillstånd att köras i bakgrunden. Genom att trycka på OK kommer du till App info. Där bläddrar du till appens batterianvändning och ställer in batterianvändningen på obegränsad. Observera att denna behörighet inte betyder att CS3 kommer att tömma ditt batteri. Den fungerar bara i bakgrunden när det behövs, till exempel när du tar emot aviseringar eller laddar ner videor från officiella tillägg. Om du väljer att avbryta kan du ändra denna inställning senare i allmänna inställningar. + Din CloudStream-data har säkerhetskopierats nu. Även om möjligheten till detta är mycket liten, kan alla enheter bete sig olika. I det sällsynta fallet att du blir utelåst från att komma åt appen, rensa appdata helt och återställ från en säkerhetskopia. Vi ber om ursäkt för eventuella besvär som detta uppstår. + Ljudbok + Det gick inte att komma åt urklipp. Försök igen. + OK + Inaktivera batterioptimering + Appens batterianvändning är redan inställd på obegränsad + Det gick inte att öppna CloudStreams appinformation. + Musik + Återställ + Kommer ut om %s + Fel vid kopiering, kopiera logcat och kontakta appsupport. + Media + Fcast + Cast mirror + Säsong %1$d Avsnitt %2$d kommer att släppas om + Välj cast-enhet + CloudStream Wiki + Konton + Säkerhet + \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index e981d05a..4b000304 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -10,17 +10,17 @@ மேலும் விருப்பங்கள் அடுத்த அத்தியாயம் வகைகள் - பகிர் + பங்கு உலாவியில் திற - ஏற்றுவதைத் தவிர் + ஏற்றுவதைத் தவிர்க்கவும் பார்த்து கொண்டிருப்பது - நிறுத்தி வைக்கப்பட்டுள்ளது - நிறைவடைந்தது + ஆன்-ஓல்ட் + முடிந்தது பார்க்கத் திட்டமிடப்பட்டுள்ளது மீண்டும் பார்க்கத் தொடங்கியது - ஸ்ட்ரீம் டோரண்ட் + ச்ட்ரீம் டொரண்ட் வசன வரிகள் - பின் செல் + திரும்பிச் செல்லுங்கள் அத்தியாயத்தை இயக்கு எபிசோட் பதிவிற்கான அனுமதி கொடுக்கவும் பதிவிறக்கப்பட்டது @@ -28,82 +28,82 @@ பதிவிறக்கம் இடைநிறுத்தப்பட்டது பதிவிறக்கம் தொடங்கியது பதிவிறக்கம் தோல்வியடைந்தது - ஸ்ட்ரீம் + பிணையம் ச்ட்ரீம் உள் சேமிப்பு மொழிபெயர்க்கப்பட்டது - கோப்பை நீக்கு - கோப்பை இயக்கவும் - பதிவிறக்கத்தை நிறுத்து + கோப்பை அழி + கோப்பு + இடைநிறுத்தம் பதிவிறக்கம் தானியங்கி பிழை அறிக்கையை முடக்கு - மேலும் தகவல்கள் + மேலும் செய்தி மறை - நீக்கு - நீக்கு - சேமிக்கவும் - உரை வண்ணம் + அகற்று + தெளிவான + சேமி + உரை நிறம் வெளிப்புற நிறம் பின்னணி நிறம் - வசன உயர்வு + வசன உயரம் எழுத்துரு வழங்குபவர்கள் பயன்படுத்தி தேடுங்கள் - வகைகளைப் பயன்படுத்தி தேடவும் + வகைகளைப் பயன்படுத்தி தேடுங்கள் மொழிகளைப் பதிவிறக்கவும் வசன மொழி - தொடர்ந்து பார்க்கவும் + தொடர்ந்து பார்த்துக் கொள்ளுங்கள் முறையாக இயங்க vpn பயன்படுத்தவும் - பிளேயர் அளவை மாற்றும் பொத்தான் - Chromecast வசனங்கள் - அமைப்புகளை மாற்ற ஸ்வைப் செய்யவும் - அடுத்த எபிசோடை தானாக இயக்கவும் - தற்போதைய அத்தியாயம் முடிந்ததும் அடுத்த அத்தியாயத்தைத் தொடங்கவும் - தேடுவதற்கு இருமுறை தட்டவும் - பிளேயரில் தேடுதல் வேகம் - இடைநிறுத்துவதற்கு நடுவில் தட்டவும் + பிளேயர் மறுஅளவிடுதல் பொத்தானை + Chromecast வசன வரிகள் + அமைப்புகளை மாற்ற ச்வைப் செய்யவும் + தன்னியக்க அடுத்த அத்தியாயம் + தற்போதைய ஒன்று முடிவடையும் போது அடுத்த அத்தியாயத்தைத் தொடங்கவும் + தேட இரட்டை தட்டு + வீரர் தொகை (விநாடிகள்) + இடைநிறுத்த நடுவில் இரண்டு முறை தட்டவும் நடிகர்கள்: %s - பின் செல் + திரும்பிச் செல்லுங்கள் அமைப்புகள் ஏற்றுகிறது… கைவிடப்பட்டது பதிவிறக்கம் முடிந்தது இணைப்பை மீண்டும் முயலவும்… - திரைப்படத்தை இயக்கு - லைவ்ஸ்ட்ரீம் இயக்கு + திரைப்படம் திரைப்படம் + லைவ்ச்ட்ரீம் விளையாடுங்கள் டிரெய்லரை இயக்கு - மூலம் + மூலங்கள் இணைப்புகளை ஏற்றுவதில் பிழை - இயக்கு + விளையாடுங்கள் பதிவிறக்கம் ரத்து செய்யப்பட்டது வசன அமைப்புகள் - பதிவிறக்கத்தை மீண்டும் தொடங்கவும் + பதிவிறக்கத்தை மீண்டும் தொடங்குங்கள் புக்மார்க்குகளை வடிகட்டவும் தகவல் - பிளேயர் வேகம் - புக்மார்க்கு - பயன்படுத்து - நகலெடுக்கவும் + பிளேயர் விரைவு + புக்மார்க்குகள் + இடு + நகலெடு மூடு எழுத்துரு அளவு - நீக்கு - மேலும் தகவல்கள் - தானாக மொழியை தேர்ந்தெடு - முன்னோக்கி அல்லது பின்னோக்கி தேட வலது அல்லது இடது பக்கத்தில் இருமுறை தட்டவும் + அகற்று + மேலும் செய்தி + தானாக தேர்ந்தெடுக்கப்பட்ட மொழி + முன்னோக்கி அல்லது பின்னோக்கி தேட வலது அல்லது இடது பக்கத்தில் இரண்டு முறை தட்டவும் மொபைலில் பிரகாசத்தை பயன்படுத்த - இயல்புநிலைக்கு மீட்டமைக்க அழுத்திப் பிடிக்கவும் + இயல்புநிலைக்கு மீட்டமைக்க பிடிக்கவும் முறையாக இயங்க vpn பரிந்துரைக்கப்பட்டது கருப்பு எல்லைகளை அகற்றவும் - விளக்கம் + விவரம் கதை எதுவும் காணப்படவில்லை - விளக்கம் ஏதும் காணப்படவில்லை - படத்தில்-படம் - பிளேயர் வசனங்கள் அமைப்புகள் - Logcat 🐈 காட்டு - பிற பயன்பாடுகளுக்கு மேல் மினியேச்சர் பிளேயரில் பிளேபேக் தொடர்கிறது + எந்த விளக்கமும் கிடைக்கவில்லை + படம்-படம் + பிளேயர் வசன வரிகள் அமைப்புகள் + LOGCAT ஐக் காட்டு + மற்ற பயன்பாடுகளின் மேல் ஒரு மினியேச்சர் பிளேயரில் பிளேபேக்கைத் தொடர்கிறது வசன வரிகள் - வீடியோ பிளேயரில் நேரத்தைக் கட்டுப்படுத்த இடது அல்லது வலதுபுறம் ஸ்வைப் செய்யவும் - பிரகாசம் அல்லது ஒலியளவை மாற்ற இடது அல்லது வலது பக்கத்தில் ஸ்வைப் செய்யவும் - இடைநிறுத்துவதற்கு இருமுறை தட்டவும் - Chromecast வசன அமைப்புகள் - இருண்ட மேலடுக்குக்குப் பதிலாக ஆப் பிளேயரில் சிஸ்டம் பிரகாசத்தைப் பயன்படுத்தவும் + ஒரு வீடியோவில் உங்கள் நிலையைக் கட்டுப்படுத்த பக்கத்திலிருந்து பக்கமாக ச்வைப் செய்யவும் + ஒளி அல்லது அளவை மாற்ற இடது அல்லது வலது பக்கத்தில் மேலே அல்லது கீழே சறுக்கி விடுங்கள் + இடைநிறுத்த இரட்டை தட்டு + Chromecast வசன வரிகள் அமைப்புகள் + இருண்ட மேலடுக்கு பதிலாக ஆப் பிளேயரில் கணினி பிரகாசத்தைப் பயன்படுத்தவும் அத்தியாயம் %d-இன் வெளியீட்டு நேரம் %1$dம %2$dநி %dநி @@ -118,5 +118,501 @@ எபிசோட்டின் போஸ்டர் போஸ்டர் பிரதான போஸ்டர் - %1$s Ep %2$d - + %1$s ep %2$d + %S ஏற்ற முடியவில்லை + %1$dd %2$dh %3$dm + வசன வரிகள் + முடிவு + முடிந்தது + சுயவிவரம் %d + வைஃபை + மொழி குறியீடு (en) + பதிவு + டப் சிட்டை + வாட்ச் முன்னேற்றத்தைப் புதுப்பிக்கவும் + ஏற்றப்பட்ட காப்புப்பிரதி கோப்பு + விரிவாக்க மொழிகள் + கணக்குகள் மற்றும் பாதுகாப்பு + எச்.எல்.எச் பிளேலிச்ட் + கலப்பு முடிவு + கிளவுட்ச்ட்ரீம் + பெனின்கள் எதுவும் கொடுக்கப்படவில்லை + பிளேபேக் விரைவு + தேட ச்வைப் + தரவை காப்புப் பிரதி எடுக்கவும் + மேம்பட்ட தேடல் + செருகுநிரல்களை தானாக பதிவிறக்கவும் + %1$d-%2$d + +30 + இது %s நிரந்தரமாக நீக்கும +\n நீ சொல்வது உறுதியா? + ஆண்டு + எதிர்பாராத பிளேயர் பிழை + பயன்பாட்டில் விளையாடுங்கள் + Chromecast அத்தியாயம் + ஆண்ட்ராய்டு டிவி போன்ற குறைந்த சேமிப்பு இடங்களைக் கொண்ட சாதனங்களில் மிக அதிகமாக அமைக்கப்பட்டால் சிக்கல்களை ஏற்படுத்துகிறது. + செயல்கள் + தலைப்பை சுவரொட்டியின் கீழ் வைக்கவும் + விரைவில் வருகிறது… + வீடியோ தடங்கள் + சிக்கலை ஏற்படுத்தும் ஒரு விபத்து காரணமாக அனைத்து நீட்டிப்புகளும் அணைக்கப்பட்டன. + சொருகி பதிவிறக்கம் செய்யப்பட்டது + பயன்பாடு கிடைக்கவில்லை + பிளேயர் காட்டப்பட்டுள்ளது - தொகையைத் தேடுங்கள் + பயன்பாட்டு புதுப்பிப்பைப் பதிவிறக்குகிறது… + கிட்அப்பை அடைய முடியவில்லை. Jsdelivr ப்ராக்சியை இயக்குதல்… + நிறுத்து + Chromecast கண்ணாடி + Hello@world.com + தோல்வி + /%d + லைவ்ச்ட்ரீம் + களஞ்சிய பெயர் மற்றும் முகவரி + நகலெடுக்கப்பட்டது! + நகலெடுப்பதில் பிழை, தயவுசெய்து LogCat ஐ நகலெடுத்து பயன்பாட்டு ஆதரவை தொடர்பு கொள்ளவும். + கிளிப்போர்டை அணுகுவதில் பிழை, மீண்டும் முயற்சிக்கவும். + அகரவரிசை (A முதல் சட் வரை) + %S க்கு முள் உள்ளிடவும் + தற்போதைய முள் உள்ளிடவும் + முள் + தவறான முள். தயவு செய்து மீண்டும் முயற்சிக்கவும். + தலைப்பு + சொருகி நீக்கப்பட்டது + தளம் + பாதுகாப்பான பயன்முறை + கொடுக்கப்பட்ட பெனீன் + திரைப்படங்கள் + இல்லை + முரண்பாட்டில் சேரவும் + ஆசிய நாடகங்கள் + மதிப்பிடப்பட்டது: %.1 எஃப் + \@string/home_play + செயல்வரம்பு + தேடல் + டிரெய்லர்களைக் காட்டு + இணைப்புகள் எதுவும் கிடைக்கவில்லை + கிளிப்போர்டில் இணைப்பு நகலெடுக்கப்பட்டது + கள் + அனைத்தும் + வசன நேரந்தவறுகை இல்லை + அதிக உரை. கிளிப்போர்டில் சேமிக்க முடியவில்லை. + பார்த்தபடி குறி + மற்றவைகள் + இணைப்புகள் மீண்டும் ஏற்றப்பட்டன + துணை + சாளரம் நிறம் + எழுத்துருக்களை %s இல் வைப்பதன் மூலம் இறக்குமதி செய்யுங்கள் + மேனிலை தரவு தளத்தால் வழங்கப்படவில்லை, தளத்தில் இல்லாவிட்டால் வீடியோ ஏற்றுதல் தோல்வியடையும். + காப்பு அதிர்வெண் + தரவு சேமிக்கப்பட்டது + சேமிப்பக அனுமதிகள் இல்லை. தயவு செய்து மீண்டும் முயற்சிக்கவும். + செயலிழப்புகள் குறித்த தரவை மட்டுமே அனுப்புகிறது + சுவரொட்டியில் இடைமுகம் கூறுகளை மாற்றவும் + மேம்படுத்தல் சோதிக்க + பூட்டு + அனிமேசன் டப்பிங்/துணை + சைகைகள் + விரைவான பழுப்பு நரி சோம்பேறி நாய் மீது குதிக்கிறது + மூலம் + கேம் + சுவரொட்டி படம் + செயலிழப்பு அறிக்கை + பொது பட்டியல் + பதிப்பு + நூலகத்தைத் தேர்ந்தெடுக்கவும் + பதிவு + கணக்கைத் திருத்தவும் + %1$s %2$d %3$s + ரத்துசெய் + இயல்புநிலை + ஓவா + டொரண்ட் + ஆவணப்படம் + ஆசிய நாடகம் + NSFW + மூலம் + விருப்பமான கண்காணிப்பு தகுதி (மொபைல் தரவு) + பெரிதாக்கு + ISP பைபாச் + நீட்டிப்புகள் + பிளேயர் நற்பொருத்தங்கள் + நற்பொருத்தங்கள் + பொது + ஆதரிக்கப்பட்ட நீட்டிப்புகளில் NSFW ஐ இயக்கவும் + முதன்மை நிறம் + பயன்பாட்டு கருப்பொருள் + சுவரொட்டி தலைப்பு இடம் + %கள் அங்கீகரிக்கப்பட்டவை + வசன நேரந்தவறுகை + வசன வரிகள் %d ms மிக விரைவாக காட்டப்பட்டால் இதைப் பயன்படுத்தவும் + பரிந்துரைக்கப்படுகிறது + ஏற்றப்பட்ட %s + கோப்பிலிருந்து ஏற்றவும் + இணையத்திலிருந்து ஏற்றவும் + ஆட்டக்காரர் + தீர்மானம் மற்றும் தலைப்பு + தலைப்பு + வசன வரிகளிலிருந்து மூடிய தலைப்புகளை அகற்றவும் + வசனங்களிலிருந்து வீக்கத்தை அகற்று + முந்தைய + நீட்டிப்புகள் + களஞ்சிய முகவரி + சொருகி ஏற்றப்பட்டது + %1$d %2$s ஐ பதிவிறக்கத் தொடங்கியது… + பதிவிறக்கம் %1$d %2$s + பதிவிறக்கம்: %d + அனைத்து %கள் ஏற்கனவே பதிவிறக்கம் செய்யப்பட்டுள்ளன + முடக்கப்பட்டது: %d + அனைத்து வசன வரிகள் + ஆடியோ தடங்கள் + மறுதொடக்கம் + விவரம் + ஆசிரியர்கள் + வலை வீடியோ நடிகர்கள் + இணைய உலாவி + %S ஐத் தவிர்க்கவும் + மறுபரிசீலனை செய்யுங்கள் + அறிமுகம் + வரலாற்றை அழிக்கவும் + அகரவரிசை (z முதல் A வரை) + உடன் திறந்திருக்கும் + குணங்கள் + கூட்டு + மாற்றவும் + அனைத்தையும் மாற்று + உங்கள் நூலகத்தில் ஏற்கனவே ஒரு நகல் உருப்படி இருப்பதாகத் தெரிகிறது: \'%கள்.\' +\n இந்த உருப்படியை எப்படியும் சேர்க்க விரும்புகிறீர்களா, இருக்கும் ஒன்றை மாற்ற விரும்புகிறீர்களா அல்லது செயலை ரத்து செய்ய விரும்புகிறீர்களா? + கடிகார நிலையை அமைக்கவும் + விளிம்பு வகை + பருவம் இல்லை + அத்தியாயம் + தற்குறிப்பு + -30 + ஒளிதோற்றம் + வீடியோ பிளேயர் தீர்மானம் + வீடியோ இடையக அளவு + நகலி தளம் + அறிவிலிமையம் பதிலாள் + JSdelivr ஐப் பயன்படுத்தி மூல அறிவிலிமையம் முகவரி களின் பைபாச். புதுப்பிப்புகள் சில நாட்களுக்கு தாமதமாகிவிடும். + களஞ்சியம் கிடைக்கவில்லை, முகவரி ஐ சரிபார்த்து VPN ஐ முயற்சிக்கவும் + தொகுதி பதிவிறக்கம் + சொருகு + இந்த களஞ்சியத்திலிருந்து அனைத்து செருகுநிரல்களையும் பதிவிறக்கவா? + மொழி + திரும்பவும் + %S இலிருந்து குழுவிலகப்பட்டது + இது அனைத்து களஞ்சிய செருகுநிரல்களையும் நீக்கிவிடும் + நீங்கள் பயன்படுத்த விரும்பும் தளங்களின் பட்டியலைப் பதிவிறக்கவும் + விருப்பமான வீடியோ பிளேயர் + திறப்பு/முடிவுக்கு ச்கிப் பாப்அப்களைக் காட்டு + பயன்படுத்தவும் + தொகு + NSFW + பிளேயர் மறைக்கப்பட்டுள்ளது - தொகையைத் தேடுங்கள் + Nginx சேவையக முகவரி + பின்னணி + மதிப்பீடு: %கள் + நிச்சயமாக நீங்கள் வெளியேற வேண்டுமா? + வீடியோ மற்றும் பட தற்காலிக சேமிப்பை அழிக்கவும் + பார்த்ததிலிருந்து அகற்று + APK நிறுவி + ஆண்ட்ராய்டு டிவி + அதிகபட்சம் + கேம் + சாதாரண + தடங்கள் + கிதப் + + வேறு முகவரி உடன் ஏற்கனவே இருக்கும் தளத்தின் குளோனைச் சேர்க்கவும் + Https க்கு மேல் dns + உங்கள் தற்போதைய அத்தியாயம் முன்னேற்றத்தை தானாக ஒத்திசைக்கவும் + சுருக்கம் + இந்த வழங்குநருக்கு Chromecast உதவி இல்லை + அடுத்தது + பிழை + பயன்பாட்டு தளவமைப்பு + இயல்புநிலை + டி.எச் + கணக்கு + வசன குறியீட்டு + பயன்பாட்டு மொழி + டிவி தளவமைப்பு + காப்புப்பிரதியிலிருந்து தரவை மீட்டெடுக்கவும் + பிழை %s + புதுப்பிப்புகள் மற்றும் காப்புப்பிரதி + PackactionInstaller + வெளியேறியதும் பயன்பாடு புதுப்பிக்கப்படும் + வரிசைப்படுத்தவும் + புதுப்பிக்கப்பட்டது (பழையது முதல் புதியது) + சுயவிவர பின்னணி + நீங்கள் ஏற்கனவே வாக்களித்து விட்டீர்கள் + பிடித்தவை + பிடித்தவைகளிலிருந்து அகற்று + சாத்தியமான நகல் காணப்படுகிறது + மாறாத + கிளவுட்ச்ட்ரீமைத் திறக்கவும் + பயோமெட்ரிக்சுடன் பூட்டு + ஆடியோ நூல் + புதிய அத்தியாயம் அறிவிப்பு + பிற நீட்டிப்புகளில் தேடுங்கள் + கிட்சுவிலிருந்து சுவரொட்டிகளைக் காட்டு + அத்தியாயம் விளையாடுங்கள் + இயல்புநிலை மதிப்புக்கு மீட்டமைக்கவும் + பருவம் + நிலை + இலவசம் + தொலைக்காட்சி தொடர் + அனிம் + மீண்டும் காட்ட வேண்டாம் + நீட்சி + அனைத்து நீட்டிப்புகளையும் சோதிக்கவும் + இந்த சோதனை டெவலப்பர்களுக்கு மட்டுமே, எந்தவொரு நீட்டிப்பையும் சரிபார்க்கவோ மறுக்கவோ இல்லை. + முன்மாதிரி தளவமைப்பு + பதிவிறக்கம் செய்யப்பட்ட கோப்பு + பகுத்தல் + எம்.பி.வி. + உங்கள் நூலகம் காலியாக உள்ளது :( +\n நூலகக் கணக்கில் உள்நுழைக அல்லது உங்கள் உள்ளக நூலகத்தில் காட்சிகளைச் சேர்க்கவும். + குழுவிலகவும் + சுயவிவரங்கள் + முள் 4 எழுத்துகளாக இருக்க வேண்டும் + ஒரு கணக்கைத் தேர்ந்தெடுக்கவும் + கணக்குகளை நிர்வகிக்கவும் + செருகுநிரல்கள் + தவறான வலைதள முகவரி + தானியங்கி சொருகி புதுப்பிப்புகள் + பதிவிறக்கத்தை வடிகட்ட பயன்முறையைத் தேர்ந்தெடுக்கவும் + மீட்டமை + களஞ்சியத்தில் செருகுநிரல்கள் எதுவும் காணப்படவில்லை + சீசன் %1$d எபிசோட் %2$d வெளியிடப்படும் + புதுப்பிப்பு தொடங்கியது + பரிந்துரைகளைக் காட்டு + டெவ்சுக்கு வழங்கப்பட்ட %d பெனின்கள் + பிளேயரில் வேக விருப்பத்தை சேர்க்கிறது + கோப்பு %s இலிருந்து தரவை மீட்டெடுப்பதில் தோல்வி + நூலகம் + தகவல் + வழங்குநரால் பிரிக்கப்பட்ட தேடல் முடிவுகளை உங்களுக்கு வழங்குகிறது + தேடல் முடிவுகளில் தேர்ந்தெடுக்கப்பட்ட வீடியோ தரத்தை மறைக்கவும் + பயன்பாட்டு புதுப்பிப்புகளைக் காட்டு + பயன்பாட்டைத் தொடங்கிய பின் புதிய புதுப்பிப்புகளைத் தானாகவே தேடுங்கள். + அமைவு செயல்முறை மீண்டும் + முன்நிபந்தனைகளுக்கு புதுப்பிக்கவும் + அதே தேவ்சின் ஒளி நாவல் பயன்பாடு + தேவ்சுக்கு ஒரு பெனீன் கொடுங்கள் + %1$d %2$s + அத்தியாயங்கள் எதுவும் கிடைக்கவில்லை + அழி + கோப்பை அழி + இடைநிறுத்தம் + தொடங்கு + கடந்து சென்றது + %டி.எம +\n மீதமுள்ள + %கள +\n மீதமுள்ள + நடந்து கொண்டிருக்கிறது + காலம் + வரிசையில் + பயன்படுத்தப்பட்டது + பயன்பாடு + ஆவணப்படங்கள் + லைவ்ச்ட்ரீம்கள் + தொடர் + கார்ட்டூன் + அனிம் + பிழையைப் பதிவிறக்குங்கள், சேமிப்பக அனுமதிகளை சரிபார்க்கவும் + வழங்குநரை மாற்றவும் + முன்னோட்டம் பின்னணி + எந்த தரவை அனுப்பவில்லை + அனிமேசுக்கு நிரப்பு அத்தியாயத்தைக் காட்டு + கூடுதல் களஞ்சியங்களிலிருந்து இன்னும் நிறுவப்படாத அனைத்து செருகுநிரல்களையும் தானாக நிறுவவும். + முழு வெளியீடுகளுக்கு பதிலாக மட்டுமே புதுப்பிப்புகளைத் தேடுங்கள் + அதே தேவ்சின் அனிம் பயன்பாடு + அத்தியாயங்கள் + %S இல் வரவிருக்கும் + மன்னிக்கவும், விண்ணப்பம் செயலிழந்தது. ஒரு அநாமதேய பிழை அறிக்கை டெவலப்பர்களுக்கு அனுப்பப்படும் + முடிந்தது + வசன வரிகள் இல்லை + கார்ட்டூன்கள் + டொரண்ட்ச் + படம் + %S இல் விளையாடுங்கள் + மூல பிழை + தொலை பிழை + ரெண்டரர் பிழை + உலாவியில் விளையாடுங்கள் + இணைப்பை நகலெடுக்கவும் + ஆட்டோ பதிவிறக்கம் + கண்ணாடியைப் பதிவிறக்கவும் + இணைப்புகளை மீண்டும் ஏற்றவும் + OP ஐத் தவிர்க்கவும் + இந்த புதுப்பிப்பைத் தவிர்க்கவும் + வசன வரிகள் பதிவிறக்கவும் + துணை சிட்டை + மறுஅளவிடுங்கள் + விருப்பமான கடிகார தகுதி (வைஃபை) + வீடியோ பிளேயர் தலைப்பு மேக்ச் சார்ச் + தளத்தை அகற்று + பாதை பதிவிறக்க + வீடியோ இடையக நீளம் + வட்டில் வீடியோ கேச் + பிளேயர் தெரியும் போது பயன்படுத்தப்படும் தேடல் தொகை + பிளேயர் மறைக்கப்படும்போது பயன்படுத்தப்படும் தேடல் தொகை + ஆண்ட்ராய்டு டிவி போன்ற குறைந்த நினைவகம் கொண்ட சாதனங்களில் மிக அதிகமாக அமைக்கப்பட்டால் செயலிழப்புகளை ஏற்படுத்துகிறது. + ISP தொகுதிகளைத் தவிர்ப்பதற்கு பயனுள்ளதாக இருக்கும் + திரைக்கு பொருந்தும் + மறுப்பு + இணைப்புகள் + பயன்பாட்டு புதுப்பிப்புகள் + காப்புப்பிரதி + கேச் + மனையமைவு + தெரிகிறது + சீரற்ற பொத்தான் + முகப்புப்பக்கம் மற்றும் நூலகத்தில் சீரற்ற பொத்தானைக் காட்டு + வழங்குநர் சோதனை + மனையமைவு + விருப்பமான மீடியா + வழங்குநர்கள் + தானி + தொலைபேசி தளவமைப்பு + கடவுச்சொல் 123 + பயனர்பெயர் + 127.0.0.1 + %1$s %2$s + புகுபதிகை + கணக்கு சேர்க்க + உங்கள் கணக்கை துவங்குங்கள் + கண்காணிப்பைச் சேர்க்கவும் + சேர்க்கப்பட்டது %கள் + ஒத்திசைவு + மதிப்பிடப்பட்டது + %d / 10 + /?? + %S இல் உள்நுழைய முடியவில்லை + முடக்கு + எதுவுமில்லை + மணித்துளி + அவுட்லைன் + மனச்சோர்வு + நிழல் + எழுப்பப்பட்ட + ஒத்திசைவு துணை + வசன வரிகள் காட்டப்பட்டால் இதைப் பயன்படுத்தவும் %d ms மிகவும் தாமதமானது + முக்கிய + துணை + கேம் + டி.சி. + ப்ளூ-ரே + Wp + டிவிடி + 4 கே + எச்.டி. + யுஎச்.டி + எச்.டி.ஆர் + எச்டி + எச்.டி.ஆர் + தவறான ஐடி + தவறான தரவு + விருப்பமான ஊடக மொழியால் வடிகட்டவும் + கூடுதல் + டிரெய்லர் + https://example.com/example.mp4 + குறிப்பாளர் (விரும்பினால்) + இந்த மொழிகளில் வீடியோக்களைப் பாருங்கள் + சமூக களஞ்சியங்களைக் காண்க + %கள் (முடக்கப்பட்டவை) + மாற்றங்களைக் காண பயன்பாட்டை மறுதொடக்கம் செய்யுங்கள். + செயலிழப்பு தகவலைக் காண்க + களஞ்சியத்தை நீக்கு + கிளவுட்ச்ட்ரீமில் இயல்புநிலையாக எந்த தளங்களும் நிறுவப்படவில்லை. நீங்கள் களஞ்சியங்களிலிருந்து தளங்களை நிறுவ வேண்டும். +\n எங்கள் முரண்பாட்டில் சேரவும் அல்லது ஆன்லைனில் தேடுங்கள். + நிலை + அளவு + ஆதரிக்கப்பட்டது + முதலில் நீட்டிப்பை நிறுவவும் + Fcast + காச்ட் சாதனத்தைத் தேர்ந்தெடுக்கவும் + அனைத்து மொழிகளும் + ஆம் + பேட்டரி தேர்வுமுறை முடக்கு + உங்கள் நூலகத்தில் சாத்தியமான நகல் உருப்படிகள் கண்டறியப்பட்டுள்ளன: +\n %கள் +\n இந்த உருப்படியை எப்படியும் சேர்க்க விரும்புகிறீர்களா, ஏற்கனவே உள்ளவற்றை மாற்ற விரும்புகிறீர்களா அல்லது செயலை ரத்து செய்ய விரும்புகிறீர்களா? + முள் உள்ளிடவும் + பூட்டு சுயவிவரம் + %S ஆக உள்நுழைந்துள்ளது + தொடக்கத்தில் கணக்கு தேர்வைத் தவிர்க்கவும் + இயல்புநிலை கணக்கைப் பயன்படுத்தவும் + கடவுச்சொல்/முள் ஏற்பு + இந்த சாதனத்தில் பயோமெட்ரிக் ஏற்பு ஆதரிக்கப்படவில்லை + கைரேகை, முகம் ஐடி, முள், முறை மற்றும் கடவுச்சொல் மூலம் பயன்பாட்டைத் திறக்கவும். + பல தோல்வியுற்ற முயற்சிகள் காரணமாக இந்த திரை மூடப்பட்டது. விண்ணப்பத்தை மறுதொடக்கம் செய்யுங்கள். + காச்ட் மிரர் + புதுப்பிப்பு எதுவும் கிடைக்கவில்லை + செய்தித் பெயர் + https://example.com + கணக்கை மாற்றவும் + 1000 எம்.எச் + சீரற்ற + Hq + அமைப்பைத் தவிர்க்கவும் + உங்கள் சாதனத்திற்கு ஏற்றவாறு பயன்பாட்டின் தோற்றத்தை மாற்றவும் + உனக்கு என்ன பார்க்க வேண்டும் + களஞ்சியத்தைச் சேர்க்கவும் + களஞ்சிய பெயர் + 18+ + பதிவிறக்கம் செய்யப்படவில்லை: %d + புதுப்பிக்கப்பட்டது %d செருகுநிரல்கள் + உள் வீரர் + வி.எல்.சி. + திறப்பு + கலப்பு திறப்பு + வரவு + வரலாறு + சரி + கிளவுட்ச்ட்ரீமின் பயன்பாட்டுத் தகவலைத் திறக்க முடியவில்லை. + %S க்கு குழுசேர்ந்தது + உதவி + %கள் பிடித்தவைகளில் சேர்க்கப்படுகின்றன + பிடித்தவையில் சேர் + சுழற்றுங்கள் + திரை நோக்குநிலைக்கு மாற்று பொத்தானைக் காண்பி + வீடியோ நோக்குநிலையின் அடிப்படையில் திரை நோக்குநிலையின் தானியங்கி மாறுவதை இயக்கவும் + ஆட்டோ சுழலும் + பிடித்த + இசை + ஓவா + தரமான சிட்டை + புதுப்பிப்பு + விடுபதிகை + விரலிடைத் தோல் + சில தொலைபேசிகள் புதிய தொகுப்பு நிறுவியை ஆதரிக்கவில்லை. புதுப்பிப்புகள் நிறுவப்படாவிட்டால் மரபு விருப்பத்தை முயற்சிக்கவும். + சந்தா தொலைக்காட்சி நிகழ்ச்சிகளுக்கான தடையற்ற பதிவிறக்கங்கள் மற்றும் அறிவிப்புகளை உறுதிப்படுத்த, கிளவுட்ச்ட்ரீம் பின்னணியில் இயங்க இசைவு தேவை. சரி என்பதை அழுத்துவதன் மூலம், நீங்கள் பயன்பாட்டுத் தகவலுக்கு அனுப்பப்படுவீர்கள். அங்கு, 𝘼𝙥𝙥 𝘼𝙥𝙥 பெறுநர் க்கு உருட்டி, பேட்டரி பயன்பாட்டை 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙 என அமைக்கவும். தயவுசெய்து கவனிக்கவும், இந்த இசைவு CS3 உங்கள் பேட்டரியை வெளியேற்றும் என்று அர்த்தமல்ல. அறிவிப்புகளைப் பெறும்போது அல்லது உத்தியோகபூர்வ நீட்டிப்புகளிலிருந்து வீடியோக்களைப் பதிவிறக்குவது போன்ற பின்னணியில் மட்டுமே இது செயல்படும். நீங்கள் ரத்து செய்ய தேர்வுசெய்தால், இந்த அமைப்பை பின்னர் 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 in இல் சரிசெய்யலாம். + பயன்பாட்டு பேட்டரி பயன்பாடு ஏற்கனவே கட்டுப்பாடற்றதாக அமைக்கப்பட்டுள்ளது + பயன்பாட்டு புதுப்பிப்பை நிறுவுகிறது… + பயன்பாட்டின் புதிய பதிப்பை நிறுவ முடியவில்லை + மரபு + வரிசைப்படுத்து + மதிப்பீடு (உயர் முதல் குறைந்த வரை) + மதிப்பீடு (குறைந்த முதல் உயர் வரை) + புதுப்பிக்கப்பட்டது (பழையது புதியது) + இந்த பட்டியல் காலியாக உள்ளது. இன்னொரு இடத்திற்கு மாற முயற்சிக்கவும். + பாதுகாப்பான பயன்முறை கோப்பு கிடைத்தது! +\n கோப்பு அகற்றப்படும் வரை தொடக்கத்தில் எந்த நீட்டிப்புகளையும் ஏற்றவில்லை. + சந்தா காட்சிகளைப் புதுப்பித்தல் + சந்தா + எபிசோட் %d வெளியானது! + மொபைல் தரவு + இயல்புநிலையை அமைக்கவும் + ஆதாரங்கள் எவ்வாறு உத்தரவிடப்படுகின்றன என்பதை இங்கே மாற்றலாம். ஒரு வீடியோவுக்கு அதிக முன்னுரிமை இருந்தால், அது மூல தேர்வில் அதிகமாகத் தோன்றும். மூல முன்னுரிமையின் தொகை மற்றும் தரமான முன்னுரிமை ஆகியவை வீடியோ முன்னுரிமை. +\n சான்று A: 3 +\n தகுதி பி: 7 +\n 10 இன் ஒருங்கிணைந்த வீடியோ முன்னுரிமை இருக்கும். +\n குறிப்பு: தொகை 10 அல்லது அதற்கு மேற்பட்டதாக இருந்தால், அந்த இணைப்பு ஏற்றப்படும்போது பிளேயர் தானாகவே ஏற்றுவதைத் தவிர்க்கும்! + இடைமுகம் ஐ சரியாக உருவாக்க முடியவில்லை, இது ஒரு பெரிய பிழை மற்றும் உடனடியாக %கள் தெரிவிக்க வேண்டும் + %கள் பிடித்தவைகளிலிருந்து அகற்றப்பட்டன + உங்கள் கிளவுட்ச்ட்ரீம் தரவு இப்போது காப்புப் பிரதி எடுக்கப்பட்டுள்ளது. இதன் சாத்தியம் மிகக் குறைவு என்றாலும், எல்லா சாதனங்களும் வித்தியாசமாக நடந்து கொள்ளலாம். அரிய விசயத்தில், பயன்பாட்டை அணுகுவதிலிருந்து நீங்கள் பூட்டப்படுகிறீர்கள், பயன்பாட்டு தரவை முழுவதுமாக அழித்து, காப்புப்பிரதியிலிருந்து மீட்டெடுக்கவும். இதிலிருந்து எழும் ஏதேனும் சிரமத்திற்கு நாங்கள் மிகவும் வருந்துகிறோம். + ஊடகம் + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index c3e5959a..bd74194e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -18,10 +18,10 @@ %1$ds %2$dd %dm - Poster + Afiş Afiş - Bölüm Posteri - Ana Poster + Bölüm Afişi + Ana Afiş Sonraki Rastgele @string/play_episode Geri git @@ -62,7 +62,7 @@ Canlı yayını oynat Torrent oynat Kaynaklar - Alt yazılar + Altyazılar Yeniden bağlan… Geri dön Bölümü oynat @@ -80,7 +80,7 @@ Bağlantılar yüklenirken hata oluştu Dahili depolama Dublajlı - Alt yazılı + Altyazılı Dosyayı sil Dosyayı oynat İndirmeyi sürdür @@ -100,13 +100,13 @@ Temizle Kaydet Oynatıcı hızı - Alt yazı ayarları + Altyazı ayarları Yazı rengi Dış hat rengi Arka plan rengi Pencere rengi Kenar tipi - Alt yazı yüksekliği + Altyazı yüksekliği Yazı tipi Yazı boyutu Sağlayıcıları kullanarak ara @@ -116,7 +116,7 @@ Otomatik seçilecek dil İndirilecek diller Altyazı dili - Sıfırlamak için basılı tut + Varsayılana sıfırlamak için basılı tutun Fontları içe aktarmak için %s konumuna yerleştirin İzlemeye devam et Kaldır @@ -133,10 +133,10 @@ İçerik diğer uygulamaların üzerinde küçük bir pencerede oynatılmaya devam eder Oynatıcı yeniden boyutlandırma butonu Siyah sınır çizgilerini kaldır - Alt yazılar - Oynatıcı alt yazı ayarları - Chromecast alt yazıları - Chromecast alt yazı ayarları + Altyazılar + Oynatıcı altyazı ayarları + Chromecast altyazıları + Chromecast altyazı ayarları Oynatma hızı Atlamak için kaydır Zamanı ayarlamak için yanlardan kaydır @@ -220,7 +220,7 @@ Site Özet Sıraya alındı - Alt yazı yok + Altyazı yok Varsayılan Boş Kullanılan @@ -263,16 +263,16 @@ Otomatik indir Şu kaynaktan indir Bağlantıları yenile - Alt yazıları indir + Altyazıları indir Kalite etiketi Dublaj etiketi - Alt yazı etiketi + Altyazı etiketi Başlık show_hd_key show_dub_key show_sub_key show_title_key - Poster üzerindeki öğeler + Afiş üzerindeki öğeleri değiştir Güncelleme bulunamadı Güncellemeleri denetle Kilitle @@ -298,7 +298,7 @@ Farklı bir URL ile mevcut bir sitenin klonunu ekleyin İndirme konumu NGINX sunucu URL\'si - Dublajlı/Alt yazılı animeleri göster + Dublajlı/Altyazılı Anime Gösterimi Ekrana sığdır Uzat Yakınlaştır @@ -307,12 +307,12 @@ 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 İçerik - Anasayfada ve Kütüphanede rastgele düğmesini göster + Ana sayfada ve Kütüphanede rastgele düğmesini göster Uzantı dilleri Uygulama düzeni Tercih edilen medya Desteklenen Uzantılarda NSFW\'yi etkinleştirin - Alt yazı kodlaması + Altyazı kodlaması Sağlayıcılar Düzen Otomatik @@ -321,8 +321,8 @@ Emülatör düzeni Birincil renk Uygulama teması - Poster başlık konumu - Başlığı posterin altına yerleştir + Afiş başlık konumu + Başlığı afişin altına yerleştir anilist_key mal_key @@ -344,7 +344,7 @@ Trakt --> %1$s %2$s - hesabı + hesap Çıkış yap Giriş yap Hesap değiştir @@ -360,7 +360,7 @@ %s başarıyla doğrulandı %s ile giriş yapılamadı - Hiçbiri + Yok Normal Hepsi Maksimum @@ -370,12 +370,12 @@ Çökmüş Gölge Yükseltilmiş - Alt yazı senkronu + Altyazı senkronu 1000 ms - Alt yazı gecikmesi - Alt yazılar %d ms erken görüntüleniyorsa bunu kullanın - Alt yazılar %d ms geç gözüküyorsa bunu kullanın - Alt yazı gecikmesi yok + Altyazı gecikmesi + Altyazılar %d ms erken görüntüleniyorsa bunu kullanın + Altyazılar %d ms geç gözüküyorsa bunu kullanın + Altyazı gecikmesi yok Movie Series From 02b956940a626daaed1ba2af70954ac152324237 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 4 Jul 2024 18:07:01 +0000 Subject: [PATCH 520/570] Port large parts of the API to crossplatform (#1163) --- app/build.gradle.kts | 2 + .../lagradost/cloudstream3/AcraApplication.kt | 4 +- .../lagradost/cloudstream3/CommonActivity.kt | 2 +- .../lagradost/cloudstream3/MainActivity.kt | 24 +- .../cloudstream3/network/CloudflareKiller.kt | 1 + .../cloudstream3/plugins/PluginManager.kt | 2 +- .../services/BackupWorkManager.kt | 2 +- .../services/SubscriptionWorkManager.kt | 4 +- .../syncproviders/AccountManager.kt | 13 +- .../syncproviders/{SyncAPI.kt => SyncApi.kt} | 10 - .../syncproviders/providers/AniListApi.kt | 2 +- .../syncproviders/providers/MALApi.kt | 2 +- .../providers/OpenSubtitlesApi.kt | 1 + .../syncproviders/providers/SimklApi.kt | 55 +--- .../cloudstream3/ui/ControllerActivity.kt | 2 +- .../cloudstream3/ui/WebviewFragment.kt | 2 +- .../cloudstream3/ui/account/AccountHelper.kt | 2 +- .../ui/download/DownloadAdapter.kt | 3 +- .../ui/download/DownloadButtonSetup.kt | 6 +- .../ui/download/DownloadFragment.kt | 3 +- .../cloudstream3/ui/home/HomeFragment.kt | 14 +- .../ui/home/HomeParentItemAdapter.kt | 2 +- .../ui/home/HomeParentItemAdapterPreview.kt | 2 +- .../cloudstream3/ui/home/HomeViewModel.kt | 10 +- .../ui/library/LibraryFragment.kt | 6 +- .../cloudstream3/ui/library/PageAdapter.kt | 4 +- .../ui/player/AbstractPlayerFragment.kt | 6 +- .../cloudstream3/ui/player/CS3IPlayer.kt | 3 +- .../ui/player/DownloadFileGenerator.kt | 1 - .../ui/player/DownloadedPlayerActivity.kt | 4 + .../ui/player/ExtractorLinkGenerator.kt | 1 - .../ui/player/FullScreenPlayer.kt | 2 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 1 + .../cloudstream3/ui/player/IGenerator.kt | 1 - .../cloudstream3/ui/player/IPlayer.kt | 1 - .../cloudstream3/ui/player/LinkGenerator.kt | 20 +- .../ui/player/OfflinePlaybackHelper.kt | 2 +- .../ui/player/PlayerGeneratorViewModel.kt | 1 - .../ui/player/PreviewGenerator.kt | 3 - .../ui/player/RepoLinkGenerator.kt | 1 - .../player/source_priority/PriorityAdapter.kt | 4 +- .../player/source_priority/ProfilesAdapter.kt | 4 +- .../ui/quicksearch/QuickSearchFragment.kt | 7 +- .../cloudstream3/ui/result/ActorAdaptor.kt | 2 +- .../cloudstream3/ui/result/EpisodeAdapter.kt | 2 +- .../cloudstream3/ui/result/ResultFragment.kt | 2 +- .../ui/result/ResultFragmentPhone.kt | 10 +- .../ui/result/ResultFragmentTv.kt | 9 +- .../ui/result/ResultViewModel2.kt | 108 +++++-- .../cloudstream3/ui/result/UiText.kt | 2 +- .../cloudstream3/ui/search/SearchFragment.kt | 14 +- .../cloudstream3/ui/search/SearchHelper.kt | 2 +- .../ui/search/SearchResultBuilder.kt | 2 +- .../ui/settings/SettingsAccount.kt | 6 +- .../ui/settings/SettingsProviders.kt | 6 +- .../settings/extensions/ExtensionsFragment.kt | 4 +- .../ui/settings/extensions/PluginAdapter.kt | 2 +- .../ui/settings/extensions/PluginsFragment.kt | 2 +- .../ui/settings/testing/TestResultAdapter.kt | 4 +- .../ui/settings/testing/TestView.kt | 2 +- .../ui/setup/SetupFragmentProviderLanguage.kt | 2 +- .../utils/{AppUtils.kt => AppContextUtils.kt} | 187 +++++++++-- .../cloudstream3/utils/DataStoreHelper.kt | 2 +- .../cloudstream3/utils/InAppUpdater.kt | 2 +- .../utils/PackageInstallerService.kt | 2 +- .../utils/VideoDownloadManager.kt | 11 +- library/build.gradle.kts | 9 + .../lagradost/api/ContextHelper.android.kt | 20 ++ .../network/WebViewResolver.android.kt | 43 +-- .../kotlin/com/lagradost/api/ContextHelper.kt | 16 + .../com/lagradost/cloudstream3/MainAPI.kt | 290 ++++-------------- .../cloudstream3/extractors/AStreamHub.kt | 2 +- .../cloudstream3/extractors/Acefile.kt | 0 .../cloudstream3/extractors/AsianLoad.kt | 0 .../cloudstream3/extractors/Blogger.kt | 0 .../cloudstream3/extractors/BullStream.kt | 0 .../cloudstream3/extractors/ByteShare.kt | 0 .../lagradost/cloudstream3/extractors/Cda.kt | 0 .../cloudstream3/extractors/Chillx.kt | 0 .../extractors/ContentXExtractor.kt | 2 +- .../cloudstream3/extractors/Dailymotion.kt | 0 .../cloudstream3/extractors/DoodExtractor.kt | 0 .../cloudstream3/extractors/EPlay.kt | 0 .../cloudstream3/extractors/Embedgram.kt | 0 .../extractors/EmturbovidExtractor.kt | 0 .../cloudstream3/extractors/Evolaod.kt | 0 .../cloudstream3/extractors/Fastream.kt | 0 .../cloudstream3/extractors/Filesim.kt | 0 .../cloudstream3/extractors/GMPlayer.kt | 0 .../cloudstream3/extractors/Gdriveplayer.kt | 0 .../cloudstream3/extractors/GenericM3U8.kt | 0 .../cloudstream3/extractors/Gofile.kt | 0 .../extractors/GoodstreamExtractor.kt | 0 .../cloudstream3/extractors/GuardareStream.kt | 0 .../extractors/HDMomPlayerExtractor.kt | 2 +- .../extractors/HDPlayerSystemExtractor.kt | 2 +- .../extractors/HDStreamAbleExtractor.kt | 0 .../extractors/HotlingerExtractor.kt | 0 .../cloudstream3/extractors/Hxfile.kt | 0 .../cloudstream3/extractors/JWPlayer.kt | 0 .../cloudstream3/extractors/Jawcloud.kt | 0 .../cloudstream3/extractors/Jeniusplay.kt | 0 .../cloudstream3/extractors/Krakenfiles.kt | 0 .../cloudstream3/extractors/Linkbox.kt | 0 .../cloudstream3/extractors/M3u8Manifest.kt | 0 .../extractors/MailRuExtractor.kt | 2 +- .../cloudstream3/extractors/Maxstream.kt | 0 .../cloudstream3/extractors/Mediafire.kt | 0 .../cloudstream3/extractors/Minoplres.kt | 0 .../cloudstream3/extractors/MixDrop.kt | 0 .../cloudstream3/extractors/Moviehab.kt | 0 .../cloudstream3/extractors/Mp4Upload.kt | 0 .../cloudstream3/extractors/MultiQuality.kt | 0 .../cloudstream3/extractors/Mvidoo.kt | 0 .../extractors/OdnoklassnikiExtractor.kt | 2 +- .../cloudstream3/extractors/OkRuExtractor.kt | 0 .../cloudstream3/extractors/Okrulink.kt | 0 .../extractors/PeaceMakerstExtractor.kt | 2 +- .../cloudstream3/extractors/Pelisplus.kt | 0 .../extractors/PixelDrainExtractor.kt | 0 .../cloudstream3/extractors/PlayLtXyz.kt | 2 +- .../cloudstream3/extractors/PlayerVoxzer.kt | 0 .../cloudstream3/extractors/Rabbitstream.kt | 0 .../extractors/RapidVidExtractor.kt | 2 +- .../cloudstream3/extractors/SBPlay.kt | 0 .../cloudstream3/extractors/Sendvid.kt | 0 .../extractors/SibNetExtractor.kt | 2 +- .../cloudstream3/extractors/Solidfiles.kt | 0 .../cloudstream3/extractors/StreamSB.kt | 0 .../cloudstream3/extractors/StreamTape.kt | 0 .../extractors/StreamWishExtractor.kt | 0 .../cloudstream3/extractors/Streamhub.kt | 0 .../cloudstream3/extractors/Streamlare.kt | 0 .../cloudstream3/extractors/StreamoUpload.kt | 0 .../cloudstream3/extractors/Streamplay.kt | 0 .../cloudstream3/extractors/Supervideo.kt | 0 .../cloudstream3/extractors/TRsTXExtractor.kt | 2 +- .../cloudstream3/extractors/Tantifilm.kt | 0 .../extractors/TauVideoExtractor.kt | 2 +- .../cloudstream3/extractors/Tomatomatela.kt | 0 .../extractors/UpstreamExtractor.kt | 0 .../cloudstream3/extractors/Uqload.kt | 0 .../cloudstream3/extractors/Userload.kt | 0 .../cloudstream3/extractors/Userscloud.kt | 0 .../cloudstream3/extractors/Uservideo.kt | 0 .../cloudstream3/extractors/Vicloud.kt | 0 .../extractors/VidMoxyExtractor.kt | 2 +- .../extractors/VidSrcExtractor.kt | 0 .../cloudstream3/extractors/VidSrcTo.kt | 142 ++++----- .../extractors/VideoSeyredExtractor.kt | 2 +- .../cloudstream3/extractors/VideoVard.kt | 0 .../cloudstream3/extractors/Vidguard.kt | 4 +- .../extractors/VidhideExtractor.kt | 0 .../cloudstream3/extractors/Vidmoly.kt | 0 .../lagradost/cloudstream3/extractors/Vido.kt | 0 .../cloudstream3/extractors/Vidplay.kt | 0 .../cloudstream3/extractors/Vidstream.kt | 0 .../lagradost/cloudstream3/extractors/Voe.kt | 6 +- .../lagradost/cloudstream3/extractors/Vtbe.kt | 0 .../cloudstream3/extractors/WatchSB.kt | 0 .../cloudstream3/extractors/WcoStream.kt | 0 .../cloudstream3/extractors/Wibufile.kt | 0 .../cloudstream3/extractors/XStreamCdn.kt | 0 .../cloudstream3/extractors/YourUpload.kt | 0 .../extractors/YoutubeExtractor.kt | 0 .../cloudstream3/extractors/Zorofile.kt | 0 .../cloudstream3/extractors/Zplayer.kt | 0 .../extractors/helper/AesHelper.kt | 0 .../extractors/helper/AsianEmbedHelper.kt | 2 +- .../extractors/helper/GogoHelper.kt | 0 .../extractors/helper/NineAnimeHelper.kt | 0 .../extractors/helper/VstreamhubHelper.kt | 0 .../extractors/helper/WcoHelper.kt | 10 +- .../cloudstream3/network/WebViewResolver.kt | 28 ++ .../cloudstream3/syncproviders/SyncAPI.kt | 10 + .../lagradost/cloudstream3/utils/AppUtils.kt | 24 ++ .../cloudstream3/utils/ExtractorApi.kt | 28 +- .../cloudstream3/utils/M3u8Helper.kt | 0 .../cloudstream3/utils/UnshortenUrl.kt | 6 +- .../com/lagradost/api/ContextHelper.jvm.kt | 10 + .../network/WebViewResolver.jvm.kt | 35 +++ 181 files changed, 730 insertions(+), 608 deletions(-) rename app/src/main/java/com/lagradost/cloudstream3/syncproviders/{SyncAPI.kt => SyncApi.kt} (97%) rename app/src/main/java/com/lagradost/cloudstream3/utils/{AppUtils.kt => AppContextUtils.kt} (82%) create mode 100644 library/src/androidMain/kotlin/com/lagradost/api/ContextHelper.android.kt rename app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt => library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt (90%) create mode 100644 library/src/commonMain/kotlin/com/lagradost/api/ContextHelper.kt rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/MainAPI.kt (86%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/AStreamHub.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Acefile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/AsianLoad.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Blogger.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/BullStream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/ByteShare.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Cda.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Chillx.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Dailymotion.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/DoodExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/EPlay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Embedgram.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Evolaod.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Fastream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Filesim.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/GMPlayer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/GenericM3U8.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Gofile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/GuardareStream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Hxfile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/JWPlayer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Jawcloud.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Jeniusplay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Krakenfiles.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Linkbox.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Maxstream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Mediafire.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Minoplres.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/MixDrop.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Moviehab.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Mp4Upload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/MultiQuality.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Mvidoo.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Okrulink.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt (99%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Pelisplus.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt (99%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Rabbitstream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/SBPlay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Sendvid.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Solidfiles.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/StreamSB.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/StreamTape.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Streamhub.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Streamlare.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/StreamoUpload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Streamplay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Supervideo.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Tantifilm.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Tomatomatela.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Uqload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Userload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Userscloud.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Uservideo.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vicloud.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VidSrcTo.kt (88%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VideoVard.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vidguard.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vidmoly.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vido.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vidplay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vidstream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Voe.kt (93%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vtbe.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/WatchSB.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/WcoStream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Wibufile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/XStreamCdn.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/YourUpload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Zorofile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Zplayer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt (76%) create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/ExtractorApi.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/M3u8Helper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/UnshortenUrl.kt (96%) create mode 100644 library/src/jvmMain/kotlin/com/lagradost/api/ContextHelper.jvm.kt create mode 100644 library/src/jvmMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.jvm.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9c75a90d..ebefa0ea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -263,6 +263,8 @@ tasks.register("copyJar") { // Merge the app classes and the library classes into classes.jar tasks.register("makeJar") { + // Duplicates cause hard to catch errors, better to fail at compile time. + duplicatesStrategy = DuplicatesStrategy.FAIL dependsOn(tasks.getByName("copyJar")) from( zipTree("build/app-classes/classes.jar"), diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 1680d698..598ff540 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -8,13 +8,14 @@ import android.content.Intent import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import com.lagradost.api.setContext import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.openBrowser +import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys @@ -151,6 +152,7 @@ class AcraApplication : Application() { get() = _context?.get() private set(value) { _context = WeakReference(value) + setContext(WeakReference(value)) } fun getKeyClass(path: String, valueType: Class): T? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 82e985db..ba303fef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -36,7 +36,7 @@ import com.lagradost.cloudstream3.ui.player.PlayerEventType import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.settings.Globals.updateTv -import com.lagradost.cloudstream3.utils.AppUtils.isRtl +import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.UIHelper diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 21567e4d..a47e7685 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -56,9 +56,7 @@ import com.google.common.collect.Comparators.min import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.initAll -import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -121,16 +119,18 @@ 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.ApkInstaller -import com.lagradost.cloudstream3.utils.AppUtils.html -import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable -import com.lagradost.cloudstream3.utils.AppUtils.isLtr -import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable -import com.lagradost.cloudstream3.utils.AppUtils.isRtl -import com.lagradost.cloudstream3.utils.AppUtils.loadCache -import com.lagradost.cloudstream3.utils.AppUtils.loadRepository -import com.lagradost.cloudstream3.utils.AppUtils.loadResult -import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.isCastApiAvailable +import com.lagradost.cloudstream3.utils.AppContextUtils.isLtr +import com.lagradost.cloudstream3.utils.AppContextUtils.isNetworkAvailable +import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl +import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache +import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository +import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult +import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback 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 c8c385cf..ce2fb3a2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.network +import android.util.Base64 import android.util.Log import android.webkit.CookieManager import androidx.annotation.AnyThread 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 a5631500..6b2b75f2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -14,7 +14,6 @@ import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.google.gson.Gson import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.removePluginMapping import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -33,6 +32,7 @@ import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt index 6ed7a447..4ef841f5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt @@ -10,7 +10,7 @@ import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager import androidx.work.WorkerParameters import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import java.util.concurrent.TimeUnit diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt index e2bcd6e1..00c74dff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt @@ -10,13 +10,13 @@ import androidx.core.app.NotificationCompat import androidx.core.net.toUri import androidx.work.* import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions 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 a14f8438..e86d73aa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -3,15 +3,22 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.syncproviders.providers.* import java.util.concurrent.TimeUnit abstract class AccountManager(private val defIndex: Int) : AuthAPI { companion object { - val malApi = MALApi(0) - val aniListApi = AniListApi(0) + val malApi = MALApi(0).also { api -> + LoadResponse.Companion.malIdPrefix = api.idPrefix + } + val aniListApi = AniListApi(0).also { api -> + LoadResponse.Companion.aniListIdPrefix = api.idPrefix + } + val simklApi = SimklApi(0).also { api -> + LoadResponse.Companion.simklIdPrefix = api.idPrefix + } val openSubtitlesApi = OpenSubtitlesApi(0) - val simklApi = SimklApi(0) val addic7ed = Addic7ed() val subDlApi = SubDlApi(0) val localListApi = LocalList() diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt rename to app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt index 045fdc94..878e0cb3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt @@ -2,20 +2,10 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.ui.SyncWatchType -import com.lagradost.cloudstream3.ui.WatchType 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, - Simkl, - LocalList, -} - interface SyncAPI : OAuth2API { /** * Set this to true if the user updates something on the list like watch status or score 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 0551fe6c..8a82cf94 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 @@ -17,7 +17,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType 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.AppContextUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.Coroutines.ioSafe 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 4249f949..24ef7136 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 @@ -20,7 +20,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType 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.AppContextUtils.splitQuery import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import java.net.URL import java.security.SecureRandom 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 7d0514d1..6412ff1b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager +import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.AppUtils import okhttp3.Interceptor import okhttp3.Response diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 4385fa5e..27975d19 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -12,7 +12,9 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.LoadResponse.Companion.readIdFromString import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SimklSyncServices import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mapper @@ -29,7 +31,6 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import okhttp3.Interceptor import okhttp3.Response import java.math.BigInteger @@ -184,32 +185,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } } - /** - * Set of sync services simkl is compatible with. - * Add more as required: https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id - */ - enum class SyncServices(val originalName: String) { - Simkl("simkl"), - Imdb("imdb"), - Tmdb("tmdb"), - AniList("anilist"), - Mal("mal"), - } - - /** - * The ID string is a way to keep a collection of services in one single ID using a map - * This adds a database service (like imdb) to the string and returns the new string. - */ - fun addIdToString(idString: String?, database: SyncServices, id: String?): String? { - if (id == null) return idString - return (readIdFromString(idString) + mapOf(database to id)).toJson() - } - - /** Read the id string to get all other ids */ - fun readIdFromString(idString: String?): Map { - return tryParseJson(idString) ?: return emptyMap() - } - fun getPosterUrl(poster: String): String { return "https://wsrv.nl/?url=https://simkl.in/posters/${poster}_m.webp" } @@ -361,13 +336,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("anilist") val anilist: String? = null, ) { companion object { - fun fromMap(map: Map): Ids { + fun fromMap(map: Map): Ids { return Ids( - simkl = map[SyncServices.Simkl]?.toIntOrNull(), - imdb = map[SyncServices.Imdb], - tmdb = map[SyncServices.Tmdb], - mal = map[SyncServices.Mal], - anilist = map[SyncServices.AniList] + simkl = map[SimklSyncServices.Simkl]?.toIntOrNull(), + imdb = map[SimklSyncServices.Imdb], + tmdb = map[SimklSyncServices.Tmdb], + mal = map[SimklSyncServices.Mal], + anilist = map[SimklSyncServices.AniList] ) } } @@ -749,13 +724,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("anilist") val anilist: String?, @JsonProperty("traktslug") val traktslug: String? ) { - fun matchesId(database: SyncServices, id: String): Boolean { + fun matchesId(database: SimklSyncServices, id: String): Boolean { return when (database) { - SyncServices.Simkl -> this.simkl == id.toIntOrNull() - SyncServices.AniList -> this.anilist == id - SyncServices.Mal -> this.mal == id - SyncServices.Tmdb -> this.tmdb == id - SyncServices.Imdb -> this.imdb == id + SimklSyncServices.Simkl -> this.simkl == id.toIntOrNull() + SimklSyncServices.AniList -> this.anilist == id + SimklSyncServices.Mal -> this.mal == id + SimklSyncServices.Tmdb -> this.tmdb == id + SimklSyncServices.Imdb -> this.imdb == id } } } @@ -916,7 +891,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ - suspend fun searchByIds(serviceMap: Map): Array? { + suspend fun searchByIds(serviceMap: Map): Array? { if (serviceMap.isEmpty()) return emptyArray() return app.get( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 688363e9..6bafa975 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -23,13 +23,13 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall -import com.lagradost.cloudstream3.sortSubs import com.lagradost.cloudstream3.sortUrls import com.lagradost.cloudstream3.ui.player.LoadType import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment +import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt index 9ed58e2c..15e66b38 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.databinding.FragmentWebviewBinding import com.lagradost.cloudstream3.network.WebViewResolver -import com.lagradost.cloudstream3.utils.AppUtils.loadRepository +import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository class WebviewFragment : Fragment() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt index 1db49e27..d2aca862 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt @@ -27,7 +27,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setLinearListLayout -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt index 1132416a..b4a16a66 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -13,8 +13,9 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.ui.download.button.DownloadStatusTell -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.UIHelper.setImage diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index 880d5f6c..c8c40e29 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -9,11 +9,11 @@ import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator +import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.ui.player.GeneratorPlayer -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE -import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index d5427cd3..82c5ffb8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -39,7 +39,8 @@ import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.loadResult +import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult +import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE import com.lagradost.cloudstream3.utils.DataStore import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe 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 12185cbf..82a92d80 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 @@ -25,8 +25,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.chip.Chip import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.HomeEpisodesExpandedBinding @@ -46,11 +44,13 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable -import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult -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.AppContextUtils.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.isRecyclerScrollable +import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppContextUtils.ownHide +import com.lagradost.cloudstream3.utils.AppContextUtils.ownShow +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event 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 4b0360d7..916cb9ae 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 @@ -22,7 +22,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable +import com.lagradost.cloudstream3.utils.AppContextUtils.isRecyclerScrollable class LoadClickCallback( val action: Int = 0, 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 52ec06db..2e98dd1f 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 @@ -16,7 +16,6 @@ import androidx.viewbinding.ViewBinding import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup -import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList @@ -36,6 +35,7 @@ import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST +import com.lagradost.cloudstream3.ui.result.getId import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index a2c7583f..9e70d088 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -6,9 +6,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality -import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia -import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -36,8 +33,11 @@ import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching -import com.lagradost.cloudstream3.utils.AppUtils.loadResult +import com.lagradost.cloudstream3.utils.AppContextUtils.addProgramsToContinueWatching +import com.lagradost.cloudstream3.utils.AppContextUtils.filterHomePageListByFilmQuality +import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.utils.AppContextUtils.filterSearchResultByFilmQuality +import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStoreHelper 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 index 90e57ef4..7144de09 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -53,9 +53,9 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -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.AppContextUtils.loadResult +import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppContextUtils.reduceDragSensitivity import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar 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 index b8feb656..b2de307f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt @@ -16,7 +16,7 @@ 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.AppContextUtils import com.lagradost.cloudstream3.utils.UIHelper.toPx import kotlin.math.roundToInt @@ -26,7 +26,7 @@ class PageAdapter( private val resView: AutofitRecyclerView, val clickCallback: (SearchClickCallback) -> Unit ) : - AppUtils.DiffAdapter(items) { + AppContextUtils.DiffAdapter(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return LibraryItemViewHolder( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 0865b220..9d838c97 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -44,8 +44,8 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.unixTimeMs import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment -import com.lagradost.cloudstream3.utils.AppUtils -import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus +import com.lagradost.cloudstream3.utils.AppContextUtils +import com.lagradost.cloudstream3.utils.AppContextUtils.requestLocalAudioFocus import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.UIHelper @@ -258,7 +258,7 @@ abstract class AbstractPlayerFragment( private fun requestAudioFocus() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - activity?.requestLocalAudioFocus(AppUtils.getFocusRequest()) + activity?.requestLocalAudioFocus(AppContextUtils.getFocusRequest()) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 31adbc87..8e322f73 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -57,14 +57,13 @@ import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle -import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.DrmExtractorLink import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File import java.lang.IllegalArgumentException diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index 5585924e..3b242172 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -4,7 +4,6 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.VideoDownloadManager import kotlin.math.max import kotlin.math.min diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 4279b542..92ef279d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -8,10 +8,14 @@ import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.safefile.SafeFile import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri +const val DTAG = "PlayerActivity" + class DownloadedPlayerActivity : AppCompatActivity() { private val dTAG = "DownloadedPlayerAct" diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt index d8d2d537..8255360c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt @@ -1,7 +1,6 @@ package com.lagradost.cloudstream3.ui.player import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri class ExtractorLinkGenerator( private val links: List, 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 aa25157b..75a861c0 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 @@ -45,7 +45,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute 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 c77f9404..d827d31e 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 @@ -49,6 +49,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1 import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index c5de1a1c..1e2cf4f5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.ui.player import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.ExtractorUri enum class LoadType { Unknown, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index 0e54e2cb..4bd5c769 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -6,7 +6,6 @@ import android.util.Rational import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri enum class PlayerEventType(val value: Int) { //Stop(-1), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index 02f44eb9..89e3c8de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -1,13 +1,31 @@ package com.lagradost.cloudstream3.ui.player +import android.net.Uri +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri +import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.unshortenLinkSafe +data class ExtractorUri( + val uri: Uri, + val name: String, + + val basePath: String? = null, + val relativePath: String? = null, + val displayName: String? = null, + + val id: Int? = null, + val parentId: Int? = null, + val episode: Int? = null, + val season: Int? = null, + val headerName: String? = null, + val tvType: TvType? = null, +) + /** * Used to open the player more easily with the LinkGenerator **/ diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt index a52ce160..e6de1266 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt @@ -5,7 +5,7 @@ import android.content.ContentUris import android.net.Uri import androidx.core.content.ContextCompat.getString import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.ExtractorUri +import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.safefile.SafeFile diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index ee44567f..1ba5a29f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri import kotlinx.coroutines.Job import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index fb600ef1..7c78ce63 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -9,14 +9,11 @@ import android.util.Log import androidx.annotation.WorkerThread import androidx.core.graphics.scale import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper2 import kotlinx.coroutines.CoroutineScope diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt index 0a194785..90bd1ca7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt @@ -7,7 +7,6 @@ import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri import kotlin.math.max import kotlin.math.min diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt index fb60ccce..1e2c9f67 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt @@ -4,7 +4,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.databinding.PlayerPrioritizeItemBinding -import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppContextUtils data class SourcePriority( val data: T, @@ -13,7 +13,7 @@ data class SourcePriority( ) class PriorityAdapter(override val items: MutableList>) : - AppUtils.DiffAdapter>(items) { + AppContextUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return PriorityViewHolder( PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt index 8153d7a1..b587276f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt @@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.PlayerQualityProfileItemBinding import com.lagradost.cloudstream3.ui.result.UiImage -import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.UIHelper.setImage class ProfilesAdapter( @@ -21,7 +21,7 @@ class ProfilesAdapter( val usedProfile: Int, val clickCallback: (oldIndex: Int?, newIndex: Int) -> Unit, ) : - AppUtils.DiffAdapter( + AppContextUtils.DiffAdapter( items, comparison = { first: QualityDataHelper.QualityProfile, second: QualityDataHelper.QualityProfile -> first.id == second.id 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 85e20d1c..12adc040 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 @@ -17,8 +17,6 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.bottomsheet.BottomSheetDialog -import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia -import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList @@ -34,12 +32,13 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchViewModel -import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.ownShow +import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.utils.AppContextUtils.filterSearchResultByFilmQuality +import com.lagradost.cloudstream3.utils.AppContextUtils.ownShow import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 7b743388..61188905 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -138,7 +138,7 @@ class ActorAdaptor( voiceActorImageHolder.isVisible = false voiceActorName.isVisible = false } else { - voiceActorName.text = actor.voiceActor.name + voiceActorName.text = actor.voiceActor?.name voiceActorImageHolder.isVisible = voiceActorImage.setImage(vaImage) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 62b1fdd1..0a1b777d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -21,7 +21,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper 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 1d3f5a08..c687eaa0 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 @@ -3,12 +3,12 @@ package com.lagradost.cloudstream3.ui.result import android.os.Bundle import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager -import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos 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 e185e75d..2f297098 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 @@ -29,7 +29,6 @@ import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse @@ -57,10 +56,11 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull -import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable -import com.lagradost.cloudstream3.utils.AppUtils.loadCache -import com.lagradost.cloudstream3.utils.AppUtils.openBrowser +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.isCastApiAvailable +import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache +import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser +import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.openBatteryOptimizationSettings import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog 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 13621cda..a0207060 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 @@ -17,7 +17,6 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialog -import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse @@ -40,13 +39,13 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper -import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.html -import com.lagradost.cloudstream3.utils.AppUtils.isRtl -import com.lagradost.cloudstream3.utils.AppUtils.loadCache +import com.lagradost.cloudstream3.utils.AppContextUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl +import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache +import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant import com.lagradost.cloudstream3.utils.UIHelper 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 ac6527de..8e8dfe30 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 @@ -18,7 +18,7 @@ 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.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -27,9 +27,9 @@ import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId -import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie +import com.lagradost.cloudstream3.LoadResponse.Companion.readIdFromString import com.lagradost.cloudstream3.MainActivity.Companion.MPV import com.lagradost.cloudstream3.MainActivity.Companion.MPV_COMPONENT import com.lagradost.cloudstream3.MainActivity.Companion.MPV_PACKAGE @@ -56,10 +56,11 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull -import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled -import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.isAppInstalled +import com.lagradost.cloudstream3.utils.AppContextUtils.isConnectedToChromecast +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWork @@ -301,6 +302,23 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { ) } +data class ExtractorSubtitleLink( + val name: String, + override val url: String, + override val referer: String, + override val headers: Map = mapOf() +) : IDownloadableMinimum + +fun LoadResponse.getId(): Int { + // this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked + return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) + ?: getLoadResponseIdFromUrl(url, apiName) +} + +private fun getLoadResponseIdFromUrl(url: String, apiName: String): Int { + return url.replace(getApiFromNameNull(apiName)?.mainUrl ?: "", "").replace("/", "") + .hashCode() +} data class LinkProgress( val linksLoaded: Int, @@ -856,7 +874,7 @@ class ResultViewModel2 : ViewModel() { loadResponse: LoadResponse? = null, statusChangedCallback: ((statusChanged: Boolean) -> Unit)? = null ) { - val (response,currentId) = loadResponse?.let { load -> + val (response, currentId) = loadResponse?.let { load -> (load to load.getId()) } ?: ((currentResponse ?: return) to (currentId ?: return)) @@ -1140,12 +1158,16 @@ class ResultViewModel2 : ViewModel() { val message = if (duplicateEntries.size == 1) { val list = when (listType) { - LibraryListType.BOOKMARKS -> getResultWatchState(duplicateEntries[0].id ?: 0).stringRes + LibraryListType.BOOKMARKS -> getResultWatchState( + duplicateEntries[0].id ?: 0 + ).stringRes + LibraryListType.FAVORITES -> R.string.favorites_list_name LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name } - context.getString(R.string.duplicate_message_single, + context.getString( + R.string.duplicate_message_single, "${normalizeString(duplicateEntries[0].name)} (${context.getString(list)}) — ${duplicateEntries[0].apiName}" ) } else { @@ -1170,9 +1192,11 @@ class ResultViewModel2 : ViewModel() { DialogInterface.BUTTON_POSITIVE -> { checkDuplicatesCallback.invoke(true, emptyList()) } + DialogInterface.BUTTON_NEGATIVE -> { checkDuplicatesCallback.invoke(false, emptyList()) } + DialogInterface.BUTTON_NEUTRAL -> { checkDuplicatesCallback.invoke(true, duplicateEntries.map { it.id }) } @@ -1189,17 +1213,17 @@ class ResultViewModel2 : ViewModel() { private fun getImdbIdFromSyncData(syncData: Map?): String? { return normalSafeApiCall { - SimklApi.readIdFromString( + readIdFromString( syncData?.get(AccountManager.simklApi.idPrefix) - )[SimklApi.Companion.SyncServices.Imdb] + )[SimklSyncServices.Imdb] } } private fun getTMDbIdFromSyncData(syncData: Map?): String? { return normalSafeApiCall { - SimklApi.readIdFromString( + readIdFromString( syncData?.get(AccountManager.simklApi.idPrefix) - )[SimklApi.Companion.SyncServices.Tmdb] + )[SimklSyncServices.Tmdb] } } @@ -1303,7 +1327,8 @@ class ResultViewModel2 : ViewModel() { postPopup( text, links.links.apmap { - val size = it.getVideoSize()?.let { size -> " " + formatFileSize(context, size) } ?: "" + val size = + it.getVideoSize()?.let { size -> " " + formatFileSize(context, size) } ?: "" txt("${it.name} ${Qualities.getStringByInt(it.quality)}$size") }) { callback.invoke(links to (it ?: return@postPopup)) @@ -1928,7 +1953,8 @@ class ResultViewModel2 : ViewModel() { .distinct().map { // this actually would be nice if we improved a bit as 3rd season == season 3 == III ect // right now it just removes the dubbed status - it.lowercase().replace(Regex("""\(?[ds]ub(bed)?\)?(\s|$)""") , "").trim() + it.lowercase().replace(Regex("""\(?[ds]ub(bed)?\)?(\s|$)"""), "") + .trim() }, TrackerType.getTypes(this.type), this.year @@ -2276,7 +2302,7 @@ class ResultViewModel2 : ViewModel() { private suspend fun postSuccessful( loadResponse: LoadResponse, - mainId : Int, + mainId: Int, apiRepository: APIRepository, updateEpisodes: Boolean, updateFillers: Boolean, @@ -2292,7 +2318,11 @@ class ResultViewModel2 : ViewModel() { postEpisodes(loadResponse, mainId, updateFillers) } - private suspend fun postEpisodes(loadResponse: LoadResponse, mainId : Int, updateFillers: Boolean) { + private suspend fun postEpisodes( + loadResponse: LoadResponse, + mainId: Int, + updateFillers: Boolean + ) { _episodes.postValue(Resource.Loading()) if (updateFillers && loadResponse is AnimeLoadResponse) { @@ -2313,7 +2343,12 @@ class ResultViewModel2 : ViewModel() { ?: 0) val totalIndex = - i.season?.let { season -> loadResponse.getTotalEpisodeIndex(episode, season) } + i.season?.let { season -> + loadResponse.getTotalEpisodeIndex( + episode, + season + ) + } if (!existingEpisodes.contains(id)) { existingEpisodes.add(id) @@ -2366,7 +2401,12 @@ class ResultViewModel2 : ViewModel() { loadResponse.seasonNames.getSeason(episode.season) val totalIndex = - episode.season?.let { season -> loadResponse.getTotalEpisodeIndex(episodeIndex, season) } + episode.season?.let { season -> + loadResponse.getTotalEpisodeIndex( + episodeIndex, + season + ) + } val ep = buildResultEpisode( @@ -2546,7 +2586,13 @@ class ResultViewModel2 : ViewModel() { ResumeProgress( progress = (viewPos.position / 1000).toInt(), maxProgress = (viewPos.duration / 1000).toInt(), - txt(R.string.resume_remaining, secondsToReadable(((viewPos.duration - viewPos.position) / 1_000).toInt(), "0 mins")) + txt( + R.string.resume_remaining, + secondsToReadable( + ((viewPos.duration - viewPos.position) / 1_000).toInt(), + "0 mins" + ) + ) ) } @@ -2672,17 +2718,26 @@ class ResultViewModel2 : ViewModel() { override var posterHeaders: Map? = null, override var backgroundPosterUrl: String? = null, override var contentRating: String? = null, - val id : Int?, + val id: Int?, ) : LoadResponse - fun loadSmall(activity: Activity?, searchResponse : SearchResponse) = ioSafe { + fun loadSmall(activity: Activity?, searchResponse: SearchResponse) = ioSafe { val url = searchResponse.url _page.postValue(Resource.Loading(url)) _episodes.postValue(Resource.Loading()) - val api = APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull(searchResponse.url) ?: APIRepository.noneApi + val api = + APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull( + searchResponse.url + ) ?: APIRepository.noneApi val repo = APIRepository(api) - val response = LoadResponseFromSearch(name = searchResponse.name, url = searchResponse.url, apiName = api.name, type = searchResponse.type ?: TvType.Others, - posterUrl = searchResponse.posterUrl, id = searchResponse.id).apply { + val response = LoadResponseFromSearch( + name = searchResponse.name, + url = searchResponse.url, + apiName = api.name, + type = searchResponse.type ?: TvType.Others, + posterUrl = searchResponse.posterUrl, + id = searchResponse.id + ).apply { if (searchResponse is SyncAPI.LibraryItem) { this.plot = searchResponse.plot this.rating = searchResponse.personalRating?.times(100) ?: searchResponse.rating @@ -2701,7 +2756,8 @@ class ResultViewModel2 : ViewModel() { mainId = mainId, apiRepository = repo, updateEpisodes = false, - updateFillers = false) + updateFillers = false + ) } fun load( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index e0762cc5..70919943 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -10,7 +10,7 @@ import androidx.annotation.StringRes import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage sealed class UiText { 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 24e87d30..ef10fcee 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 @@ -24,11 +24,7 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton -import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia -import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings -import com.lagradost.cloudstream3.APIHolder.getApiSettings import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AllLanguagesName @@ -58,9 +54,13 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -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.AppContextUtils.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.utils.AppContextUtils.filterSearchResultByFilmQuality +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.ownHide +import com.lagradost.cloudstream3.utils.AppContextUtils.ownShow +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount 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 66423982..ef1b8719 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 @@ -11,7 +11,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.ui.result.START_ACTION_LOAD_EP import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper 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 d18c0197..f597132b 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 @@ -19,7 +19,7 @@ import com.lagradost.cloudstream3.isMovieType import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.SubtitleHelper diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 67a2a15b..15f8735f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -9,8 +9,6 @@ import android.view.inputmethod.EditorInfo import android.widget.TextView import androidx.annotation.UiThread import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmapOrNull import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity @@ -49,7 +47,7 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback @@ -64,9 +62,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage -import com.lagradost.cloudstream3.utils.UIHelper.toPx import qrcode.QRCode -import java.io.ByteArrayOutputStream class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback { companion object { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 7dc73a46..cfb46c39 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -7,19 +7,17 @@ import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings -import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard -import com.lagradost.cloudstream3.utils.UIHelper.navigate class SettingsProviders : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 1364c376..1b487629 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -33,8 +33,8 @@ import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.AppUtils.addRepositoryDialog -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.addRepositoryDialog +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index cab029bb..909c30be 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -20,7 +20,7 @@ import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage 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 3bdcb251..c5319c37 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -8,7 +8,6 @@ import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R @@ -24,6 +23,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.ui.settings.appLanguages +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.toPx diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt index 023ecb4c..bad58a0e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.databinding.ProviderTestItemBinding import com.lagradost.cloudstream3.mvvm.getAllMessages import com.lagradost.cloudstream3.mvvm.getStackTracePretty import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso @@ -23,7 +23,7 @@ import com.lagradost.cloudstream3.utils.TestingUtils import java.io.File class TestResultAdapter(override val items: MutableList>) : - AppUtils.DiffAdapter>(items) { + AppContextUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProviderTestViewHolder( ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt index 26513f4a..eea495a2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt @@ -13,7 +13,7 @@ import androidx.core.view.isVisible import androidx.core.widget.ContentLoadingProgressBar import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.AppUtils.animateProgressTo +import com.lagradost.cloudstream3.utils.AppContextUtils.animateProgressTo class TestView @JvmOverloads constructor( context: Context, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index 59dcc402..c12e9eb8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -11,11 +11,11 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentSetupProviderLanguagesBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt similarity index 82% rename from app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt rename to app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index 626eca12..f0aae7bc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -51,6 +51,7 @@ import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.wrappers.Wrappers import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent @@ -60,9 +61,9 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WebviewFragment +import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.settings.Globals -import com.lagradost.cloudstream3.ui.settings.extensions.ExtensionsFragment import com.lagradost.cloudstream3.ui.settings.extensions.PluginsFragment import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -79,7 +80,7 @@ import java.io.* import java.net.URL import java.net.URLDecoder -object AppUtils { +object AppContextUtils { fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) { for (i in 0..maxViewTypeId) recycledViewPool.setMaxRecycledViews(i, maxPoolSize) @@ -371,6 +372,168 @@ object AppUtils { } } + fun sortSubs(subs: Set): List { + return subs.sortedBy { it.name } + } + + fun Context.getApiSettings(): HashSet { + //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + + val hashSet = HashSet() + val activeLangs = getApiProviderLangSettings() + val hasUniversal = activeLangs.contains(AllLanguagesName) + hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } + .map { it.name }) + + /*val set = settingsManager.getStringSet( + this.getString(R.string.search_providers_list_key), + hashSet + )?.toHashSet() ?: hashSet + + val list = HashSet() + for (name in set) { + val api = getApiFromNameNull(name) ?: continue + if (activeLangs.contains(api.lang)) { + list.add(name) + } + }*/ + //if (list.isEmpty()) return hashSet + //return list + return hashSet + } + + fun Context.getApiDubstatusSettings(): HashSet { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = HashSet() + hashSet.addAll(DubStatus.values()) + val list = settingsManager.getStringSet( + this.getString(R.string.display_sub_key), + hashSet.map { it.name }.toMutableSet() + ) ?: return hashSet + + val names = DubStatus.values().map { it.name }.toHashSet() + //if(realSet.isEmpty()) return hashSet + + return list.filter { names.contains(it) }.map { DubStatus.valueOf(it) }.toHashSet() + } + + fun Context.getApiProviderLangSettings(): HashSet { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = hashSetOf(AllLanguagesName) // def is all languages +// hashSet.add("en") // def is only en + val list = settingsManager.getStringSet( + this.getString(R.string.provider_lang_key), + hashSet + ) + + if (list.isNullOrEmpty()) return hashSet + return list.toHashSet() + } + + fun Context.getApiTypeSettings(): HashSet { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = HashSet() + hashSet.addAll(TvType.values()) + val list = settingsManager.getStringSet( + this.getString(R.string.search_types_list_key), + hashSet.map { it.name }.toMutableSet() + ) + + if (list.isNullOrEmpty()) return hashSet + + val names = TvType.values().map { it.name }.toHashSet() + val realSet = list.filter { names.contains(it) }.map { TvType.valueOf(it) }.toHashSet() + if (realSet.isEmpty()) return hashSet + + return realSet + } + + fun Context.updateHasTrailers() { + LoadResponse.isTrailersEnabled = getHasTrailers() + } + + private fun Context.getHasTrailers(): Boolean { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true) + } + + fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List { + // We are getting the weirdest crash ever done: + // java.lang.ClassCastException: com.lagradost.cloudstream3.TvType cannot be cast to com.lagradost.cloudstream3.TvType + // Trying fixing using classloader fuckery + val oldLoader = Thread.currentThread().contextClassLoader + Thread.currentThread().contextClassLoader = TvType::class.java.classLoader + + val default = TvType.values() + .sorted() + .filter { it != TvType.NSFW } + .map { it.ordinal } + + Thread.currentThread().contextClassLoader = oldLoader + + val defaultSet = default.map { it.toString() }.toSet() + val currentPrefMedia = try { + PreferenceManager.getDefaultSharedPreferences(this) + .getStringSet(this.getString(R.string.prefer_media_type_key), defaultSet) + ?.mapNotNull { it.toIntOrNull() ?: return@mapNotNull null } + } catch (e: Throwable) { + null + } ?: default + val langs = this.getApiProviderLangSettings() + val hasUniversal = langs.contains(AllLanguagesName) + val allApis = synchronized(apis) { + apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } + } + return if (currentPrefMedia.isEmpty()) { + allApis + } else { + // Filter API depending on preferred media type + allApis.filter { api -> api.supportedTypes.any { currentPrefMedia.contains(it.ordinal) } } + } + } + + fun Context.filterSearchResultByFilmQuality(data: List): List { + // Filter results omitting entries with certain quality + if (data.isNotEmpty()) { + val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) + ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) + ?.mapNotNull { entry -> + entry.toIntOrNull() ?: return@mapNotNull null + } ?: listOf() + if (filteredSearchQuality.isNotEmpty()) { + return data.filter { item -> + val searchQualVal = item.quality?.ordinal ?: -1 + //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") + !filteredSearchQuality.contains(searchQualVal) + } + } + } + return data + } + + fun Context.filterHomePageListByFilmQuality(data: HomePageList): HomePageList { + // Filter results omitting entries with certain quality + if (data.list.isNotEmpty()) { + val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) + ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) + ?.mapNotNull { entry -> + entry.toIntOrNull() ?: return@mapNotNull null + } ?: listOf() + if (filteredSearchQuality.isNotEmpty()) { + return HomePageList( + name = data.name, + isHorizontalImages = data.isHorizontalImages, + list = data.list.filter { item -> + val searchQualVal = item.quality?.ordinal ?: -1 + //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") + !filteredSearchQuality.contains(searchQualVal) + } + ) + } + } + return data + } + fun Activity.loadRepository(url: String) { ioSafe { val repo = RepositoryManager.parseRepository(url) ?: return@ioSafe @@ -532,24 +695,6 @@ object AppUtils { return queryPairs } - /** Any object as json string */ - fun Any.toJson(): String { - if (this is String) return this - return mapper.writeValueAsString(this) - } - - inline fun parseJson(value: String): T { - return mapper.readValue(value) - } - - inline fun tryParseJson(value: String?): T? { - return try { - parseJson(value ?: return null) - } catch (_: Exception) { - null - } - } - /**| S1:E2 Hello World * | Episode 2. Hello world * | Hello World @@ -619,7 +764,7 @@ object AppUtils { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) Kitsu.isEnabled = settingsManager.getBoolean(this.getString(R.string.show_kitsu_posters_key), true) - }catch (t : Throwable) { + } catch (t: Throwable) { logError(t) } 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 04387d80..43124a53 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.utils import android.content.Context import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -18,6 +17,7 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState +import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia import kotlin.reflect.KClass import kotlin.reflect.KProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index d9a31b4e..89bb0031 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -24,7 +24,7 @@ import okio.sink import java.io.File import android.text.TextUtils import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import java.io.BufferedReader import java.io.IOException import java.io.InputStreamReader diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt index 322547f4..57b98dc2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import kotlinx.coroutines.delay diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 421b09e2..f3cbdaf1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -24,6 +24,7 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.IDownloadableMinimum import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType @@ -107,16 +108,6 @@ object VideoDownloadManager { Stop, } - interface IDownloadableMinimum { - val url: String - val referer: String - val headers: Map - } - - fun IDownloadableMinimum.getId(): Int { - return url.hashCode() - } - data class DownloadEpisodeMetadata( @JsonProperty("id") val id: Int, @JsonProperty("mainName") val mainName: String, diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 46da8e84..516e1ee9 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -1,4 +1,5 @@ import com.codingfeline.buildkonfig.compiler.FieldSpec +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { kotlin("multiplatform") @@ -12,6 +13,11 @@ kotlin { androidTarget() jvm() + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + sourceSets { commonMain.dependencies { implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib @@ -19,6 +25,9 @@ kotlin { ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API Level 25 or Less. */ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + implementation("me.xdrop:fuzzywuzzy:1.4.0") // Match extractors + implementation("org.mozilla:rhino:1.7.15") // run JavaScript + implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") } } } diff --git a/library/src/androidMain/kotlin/com/lagradost/api/ContextHelper.android.kt b/library/src/androidMain/kotlin/com/lagradost/api/ContextHelper.android.kt new file mode 100644 index 00000000..a8472fea --- /dev/null +++ b/library/src/androidMain/kotlin/com/lagradost/api/ContextHelper.android.kt @@ -0,0 +1,20 @@ +package com.lagradost.api + +import android.content.Context +import java.lang.ref.WeakReference + +var ctx: WeakReference? = null + +/** + * Helper function for Android specific context. Not usable in JVM. + * Do not use this unless absolutely necessary. + */ +actual fun getContext(): Any? { + return ctx?.get() +} + +actual fun setContext(context: WeakReference) { + if (context.get() is Context) { + ctx = context as? WeakReference + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt similarity index 90% rename from app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt rename to library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt index 90872d94..0fbc5749 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt +++ b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt @@ -1,13 +1,12 @@ package com.lagradost.cloudstream3.network import android.annotation.SuppressLint +import android.content.Context import android.net.http.SslError import android.os.Handler import android.os.Looper import android.webkit.* -import com.lagradost.cloudstream3.AcraApplication -import com.lagradost.cloudstream3.AcraApplication.Companion.context -import com.lagradost.cloudstream3.USER_AGENT +import com.lagradost.api.getContext import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.logError @@ -33,40 +32,24 @@ import java.net.URI * @param scriptCallback will be called with the result from custom js * @param timeout close webview after timeout * */ -class WebViewResolver( +actual class WebViewResolver actual constructor( val interceptUrl: Regex, - val additionalUrls: List = emptyList(), - val userAgent: String? = USER_AGENT, - val useOkhttp: Boolean = true, - val script: String? = null, - val scriptCallback: ((String) -> Unit)? = null, - val timeout: Long = DEFAULT_TIMEOUT + val additionalUrls: List, + val userAgent: String?, + val useOkhttp: Boolean, + val script: String?, + val scriptCallback: ((String) -> Unit)?, + val timeout: Long ) : Interceptor { - constructor( - interceptUrl: Regex, - additionalUrls: List = emptyList(), - userAgent: String? = USER_AGENT, - useOkhttp: Boolean = true, - script: String? = null, - scriptCallback: ((String) -> Unit)? = null, - ) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, script, scriptCallback, DEFAULT_TIMEOUT) - - constructor( - interceptUrl: Regex, - additionalUrls: List = emptyList(), - userAgent: String? = USER_AGENT, - useOkhttp: Boolean = true - ) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null, DEFAULT_TIMEOUT) - - companion object { - private const val DEFAULT_TIMEOUT = 60_000L + actual companion object { var webViewUserAgent: String? = null + actual val DEFAULT_TIMEOUT = 60_000L @JvmName("getWebViewUserAgent1") fun getWebViewUserAgent(): String? { - return webViewUserAgent ?: context?.let { ctx -> + return webViewUserAgent ?: (getContext() as? Context)?.let { ctx -> runBlocking { mainWork { WebView(ctx).settings.userAgentString.also { userAgent -> @@ -137,7 +120,7 @@ class WebViewResolver( WebView.setWebContentsDebuggingEnabled(true) try { webView = WebView( - AcraApplication.context + (getContext() as? Context) ?: throw RuntimeException("No base context in WebViewResolver") ).apply { // Bare minimum to bypass captcha diff --git a/library/src/commonMain/kotlin/com/lagradost/api/ContextHelper.kt b/library/src/commonMain/kotlin/com/lagradost/api/ContextHelper.kt new file mode 100644 index 00000000..fb54e3ca --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/api/ContextHelper.kt @@ -0,0 +1,16 @@ +package com.lagradost.api + +import java.lang.ref.WeakReference + +/** + * Set context for android specific code such as webview. + * Does nothing on JVM. + */ +expect fun setContext(context: WeakReference) +/** + * Helper function for Android specific context. + * Do not use this unless absolutely necessary. + * setContext() must be called before this is called. + * @return Context if on android, null if not. + */ +expect fun getContext(): Any? diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt similarity index 86% rename from app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index 91da2ed0..47ef5382 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -1,34 +1,26 @@ package com.lagradost.cloudstream3 -import android.annotation.SuppressLint -import android.content.Context -import android.net.Uri -import android.util.Base64.encodeToString -import androidx.annotation.WorkerThread -import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi import com.lagradost.cloudstream3.syncproviders.SyncIdName -import com.lagradost.cloudstream3.syncproviders.providers.SimklApi -import com.lagradost.cloudstream3.ui.player.SubtitleData -import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.nicehttp.RequestBodyTypes import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.toRequestBody +import java.net.URI import java.text.SimpleDateFormat import java.util.* +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.math.absoluteValue /** @@ -111,17 +103,6 @@ object APIHolder { return null } - private fun getLoadResponseIdFromUrl(url: String, apiName: String): Int { - return url.replace(getApiFromNameNull(apiName)?.mainUrl ?: "", "").replace("/", "") - .hashCode() - } - - fun LoadResponse.getId(): Int { - // this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked - return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) - ?: getLoadResponseIdFromUrl(url, apiName) - } - /** * Gets the website captcha token * discovered originally by https://github.com/ahmedgamal17 @@ -137,10 +118,9 @@ object APIHolder { // To get the key suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? { try { - val uri = Uri.parse(url) - val domain = encodeToString( + val uri = URI.create(url) + val domain = base64Encode( (uri.scheme + "://" + uri.host + ":443").encodeToByteArray(), - 0 ).replace("\n", "").replace("=", ".") val vToken = @@ -275,165 +255,6 @@ object APIHolder { return app.post("https://graphql.anilist.co", requestBody = data) .parsedSafe() } - - - fun Context.getApiSettings(): HashSet { - //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - - val hashSet = HashSet() - val activeLangs = getApiProviderLangSettings() - val hasUniversal = activeLangs.contains(AllLanguagesName) - hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } - .map { it.name }) - - /*val set = settingsManager.getStringSet( - this.getString(R.string.search_providers_list_key), - hashSet - )?.toHashSet() ?: hashSet - - val list = HashSet() - for (name in set) { - val api = getApiFromNameNull(name) ?: continue - if (activeLangs.contains(api.lang)) { - list.add(name) - } - }*/ - //if (list.isEmpty()) return hashSet - //return list - return hashSet - } - - fun Context.getApiDubstatusSettings(): HashSet { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val hashSet = HashSet() - hashSet.addAll(DubStatus.values()) - val list = settingsManager.getStringSet( - this.getString(R.string.display_sub_key), - hashSet.map { it.name }.toMutableSet() - ) ?: return hashSet - - val names = DubStatus.values().map { it.name }.toHashSet() - //if(realSet.isEmpty()) return hashSet - - return list.filter { names.contains(it) }.map { DubStatus.valueOf(it) }.toHashSet() - } - - fun Context.getApiProviderLangSettings(): HashSet { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val hashSet = hashSetOf(AllLanguagesName) // def is all languages -// hashSet.add("en") // def is only en - val list = settingsManager.getStringSet( - this.getString(R.string.provider_lang_key), - hashSet - ) - - if (list.isNullOrEmpty()) return hashSet - return list.toHashSet() - } - - fun Context.getApiTypeSettings(): HashSet { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val hashSet = HashSet() - hashSet.addAll(TvType.values()) - val list = settingsManager.getStringSet( - this.getString(R.string.search_types_list_key), - hashSet.map { it.name }.toMutableSet() - ) - - if (list.isNullOrEmpty()) return hashSet - - val names = TvType.values().map { it.name }.toHashSet() - val realSet = list.filter { names.contains(it) }.map { TvType.valueOf(it) }.toHashSet() - if (realSet.isEmpty()) return hashSet - - return realSet - } - - fun Context.updateHasTrailers() { - LoadResponse.isTrailersEnabled = getHasTrailers() - } - - private fun Context.getHasTrailers(): Boolean { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true) - } - - fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List { - // We are getting the weirdest crash ever done: - // java.lang.ClassCastException: com.lagradost.cloudstream3.TvType cannot be cast to com.lagradost.cloudstream3.TvType - // Trying fixing using classloader fuckery - val oldLoader = Thread.currentThread().contextClassLoader - Thread.currentThread().contextClassLoader = TvType::class.java.classLoader - - val default = TvType.values() - .sorted() - .filter { it != TvType.NSFW } - .map { it.ordinal } - - Thread.currentThread().contextClassLoader = oldLoader - - val defaultSet = default.map { it.toString() }.toSet() - val currentPrefMedia = try { - PreferenceManager.getDefaultSharedPreferences(this) - .getStringSet(this.getString(R.string.prefer_media_type_key), defaultSet) - ?.mapNotNull { it.toIntOrNull() ?: return@mapNotNull null } - } catch (e: Throwable) { - null - } ?: default - val langs = this.getApiProviderLangSettings() - val hasUniversal = langs.contains(AllLanguagesName) - val allApis = synchronized(apis) { - apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } - } - return if (currentPrefMedia.isEmpty()) { - allApis - } else { - // Filter API depending on preferred media type - allApis.filter { api -> api.supportedTypes.any { currentPrefMedia.contains(it.ordinal) } } - } - } - - fun Context.filterSearchResultByFilmQuality(data: List): List { - // Filter results omitting entries with certain quality - if (data.isNotEmpty()) { - val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) - ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) - ?.mapNotNull { entry -> - entry.toIntOrNull() ?: return@mapNotNull null - } ?: listOf() - if (filteredSearchQuality.isNotEmpty()) { - return data.filter { item -> - val searchQualVal = item.quality?.ordinal ?: -1 - //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") - !filteredSearchQuality.contains(searchQualVal) - } - } - } - return data - } - - fun Context.filterHomePageListByFilmQuality(data: HomePageList): HomePageList { - // Filter results omitting entries with certain quality - if (data.list.isNotEmpty()) { - val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) - ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) - ?.mapNotNull { entry -> - entry.toIntOrNull() ?: return@mapNotNull null - } ?: listOf() - if (filteredSearchQuality.isNotEmpty()) { - return HomePageList( - name = data.name, - isHorizontalImages = data.isHorizontalImages, - list = data.list.filter { item -> - val searchQualVal = item.quality?.ordinal ?: -1 - //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") - !filteredSearchQuality.contains(searchQualVal) - } - ) - } - } - return data - } } /* @@ -656,7 +477,7 @@ abstract class MainAPI { //emptyList() // open val mainPage = listOf(MainPageData("", "", false)) - @WorkerThread + // @WorkerThread open suspend fun getMainPage( page: Int, request: MainPageRequest, @@ -664,17 +485,17 @@ abstract class MainAPI { throw NotImplementedError() } - @WorkerThread + // @WorkerThread open suspend fun search(query: String): List? { throw NotImplementedError() } - @WorkerThread + // @WorkerThread open suspend fun quickSearch(query: String): List? { throw NotImplementedError() } - @WorkerThread + // @WorkerThread /** * Based on data from search() or getMainPage() it generates a LoadResponse, * basically opening the info page from a link. @@ -692,13 +513,13 @@ abstract class MainAPI { * This function might be updated to include exoplayer timestamps etc in the future * if the need arises. * */ - @WorkerThread + // @WorkerThread open suspend fun extractorVerifierJob(extractorData: String?) { throw NotImplementedError() } /**Callback is fired once a link is found, will return true if method is executed successfully*/ - @WorkerThread + // @WorkerThread open suspend fun loadLinks( data: String, isCasting: Boolean, @@ -723,27 +544,16 @@ abstract class MainAPI { } /** Might need a different implementation for desktop*/ -@SuppressLint("NewApi") fun base64Decode(string: String): String { return String(base64DecodeArray(string), Charsets.ISO_8859_1) } - -@SuppressLint("NewApi") +@OptIn(ExperimentalEncodingApi::class) fun base64DecodeArray(string: String): ByteArray { - return try { - android.util.Base64.decode(string, android.util.Base64.DEFAULT) - } catch (e: Exception) { - Base64.getDecoder().decode(string) - } + return Base64.decode(string) } - -@SuppressLint("NewApi") +@OptIn(ExperimentalEncodingApi::class) fun base64Encode(array: ByteArray): String { - return try { - String(android.util.Base64.encode(array, android.util.Base64.NO_WRAP), Charsets.ISO_8859_1) - } catch (e: Exception) { - String(Base64.getEncoder().encode(array)) - } + return Base64.encode(array) } fun MainAPI.fixUrlNull(url: String?): String? { @@ -779,10 +589,6 @@ fun sortUrls(urls: Set): List { return urls.sortedBy { t -> -t.quality } } -fun sortSubs(subs: Set): List { - return subs.sortedBy { it.name } -} - fun capitalizeString(str: String): String { return capitalizeStringNullable(str) ?: str } @@ -1204,11 +1010,25 @@ interface LoadResponse { var contentRating: String? companion object { - private val malIdPrefix = malApi.idPrefix - private val aniListIdPrefix = aniListApi.idPrefix - private val simklIdPrefix = simklApi.idPrefix + var malIdPrefix = "" //malApi.idPrefix + var aniListIdPrefix = "" //aniListApi.idPrefix + var simklIdPrefix = "" //simklApi.idPrefix var isTrailersEnabled = true + /** + * The ID string is a way to keep a collection of services in one single ID using a map + * This adds a database service (like imdb) to the string and returns the new string. + */ + fun addIdToString(idString: String?, database: SimklSyncServices, id: String?): String? { + if (id == null) return idString + return (readIdFromString(idString) + mapOf(database to id)).toJson() + } + + /** Read the id string to get all other ids */ + fun readIdFromString(idString: String?): Map { + return tryParseJson(idString) ?: return emptyMap() + } + fun LoadResponse.isMovie(): Boolean { return this.type.isMovieType() || this is MovieLoadResponse } @@ -1232,12 +1052,12 @@ interface LoadResponse { * Internal helper function to add simkl ids from other databases. */ private fun LoadResponse.addSimklId( - database: SimklApi.Companion.SyncServices, + database: SimklSyncServices, id: String? ) { normalSafeApiCall { this.syncData[simklIdPrefix] = - SimklApi.addIdToString(this.syncData[simklIdPrefix], database, id.toString()) + addIdToString(this.syncData[simklIdPrefix], database, id.toString()) ?: return@normalSafeApiCall } } @@ -1257,30 +1077,28 @@ interface LoadResponse { fun LoadResponse.getImdbId(): String? { return normalSafeApiCall { - SimklApi.readIdFromString(this.syncData[simklIdPrefix]) - ?.get(SimklApi.Companion.SyncServices.Imdb) + readIdFromString(this.syncData[simklIdPrefix])[SimklSyncServices.Imdb] } } fun LoadResponse.getTMDbId(): String? { return normalSafeApiCall { - SimklApi.readIdFromString(this.syncData[simklIdPrefix]) - ?.get(SimklApi.Companion.SyncServices.Tmdb) + readIdFromString(this.syncData[simklIdPrefix])[SimklSyncServices.Tmdb] } } fun LoadResponse.addMalId(id: Int?) { this.syncData[malIdPrefix] = (id ?: return).toString() - this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString()) + this.addSimklId(SimklSyncServices.Mal, id.toString()) } fun LoadResponse.addAniListId(id: Int?) { this.syncData[aniListIdPrefix] = (id ?: return).toString() - this.addSimklId(SimklApi.Companion.SyncServices.AniList, id.toString()) + this.addSimklId(SimklSyncServices.AniList, id.toString()) } fun LoadResponse.addSimklId(id: Int?) { - this.addSimklId(SimklApi.Companion.SyncServices.Simkl, id.toString()) + this.addSimklId(SimklSyncServices.Simkl, id.toString()) } fun LoadResponse.addImdbUrl(url: String?) { @@ -1362,7 +1180,7 @@ interface LoadResponse { fun LoadResponse.addImdbId(id: String?) { // TODO add imdb sync - this.addSimklId(SimklApi.Companion.SyncServices.Imdb, id) + this.addSimklId(SimklSyncServices.Imdb, id) } fun LoadResponse.addTrackId(id: String?) { @@ -1375,7 +1193,7 @@ interface LoadResponse { fun LoadResponse.addTMDbId(id: String?) { // TODO add TMDb sync - this.addSimklId(SimklApi.Companion.SyncServices.Tmdb, id) + this.addSimklId(SimklSyncServices.Tmdb, id) } fun LoadResponse.addRating(text: String?) { @@ -1466,7 +1284,7 @@ data class NextAiring( constructor( episode: Int, unixTime: Long, - ) : this ( + ) : this( episode, unixTime, null @@ -1929,6 +1747,28 @@ fun MainAPI.newEpisode( return builder } +interface IDownloadableMinimum { + val url: String + val referer: String + val headers: Map +} + +fun IDownloadableMinimum.getId(): Int { + return url.hashCode() +} + +/** + * Set of sync services simkl is compatible with. + * Add more as required: https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id + */ +enum class SimklSyncServices(val originalName: String) { + Simkl("simkl"), + Imdb("imdb"), + Tmdb("tmdb"), + AniList("anilist"), + Mal("mal"), +} + data class TvSeriesLoadResponse( override var name: String, override var url: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt index b0051ba7..23f8dcf4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Acefile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Acefile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/BullStream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/BullStream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByteShare.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByteShare.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt index b7f84af1..27a5c52a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/EPlay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/EPlay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Embedgram.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Embedgram.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Embedgram.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Embedgram.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Fastream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Fastream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filesim.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filesim.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GMPlayer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GMPlayer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GenericM3U8.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GenericM3U8.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/GenericM3U8.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GenericM3U8.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GuardareStream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GuardareStream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt index 03586386..1f70ce61 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.extractors.helper.AesHelper diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt index 14333d35..8318c3fb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/JWPlayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/JWPlayer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Jawcloud.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jawcloud.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Jawcloud.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jawcloud.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Krakenfiles.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Krakenfiles.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt index 766c7762..ce742e97 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Maxstream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Maxstream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Maxstream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Maxstream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mediafire.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mediafire.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Mediafire.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mediafire.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/MixDrop.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MixDrop.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/MixDrop.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MixDrop.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Moviehab.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Moviehab.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mp4Upload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mp4Upload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mvidoo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Mvidoo.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt index 46f6ad0f..6db0830c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Okrulink.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Okrulink.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Okrulink.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Okrulink.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt similarity index 99% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt index b57449bf..0a005036 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt similarity index 99% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt index 2b286abb..a4dc694e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt index a0d830cf..607d2d78 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SBPlay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SBPlay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Sendvid.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Sendvid.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Sendvid.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Sendvid.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt index a8bcee31..ebd57f9c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Solidfiles.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Solidfiles.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamlare.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Streamlare.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamoUpload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamoUpload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/StreamoUpload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamoUpload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Streamplay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt index 645d7c0e..de5ca9a2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt index 2478edc1..157374a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tomatomatela.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tomatomatela.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uqload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uqload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Userload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Userload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Userscloud.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userscloud.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Userscloud.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userscloud.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uservideo.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uservideo.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt index b963fe56..e57772ce 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt similarity index 88% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt index 2655670d..73857fb3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -1,70 +1,72 @@ -package com.lagradost.cloudstream3.extractors - -import android.util.Base64 -import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.amap -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.ExtractorApi -import com.lagradost.cloudstream3.utils.ExtractorLink -import java.net.URLDecoder -import javax.crypto.Cipher -import javax.crypto.spec.SecretKeySpec - -class VidSrcTo : ExtractorApi() { - override val name = "VidSrcTo" - override val mainUrl = "https://vidsrc.to" - override val requiresReferer = true - - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return - val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return - if (res.status != 200) return - res.result?.amap { source -> - try { - val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap - val finalUrl = DecryptUrl(embedRes.result.encUrl) - if(finalUrl.equals(embedRes.result.encUrl)) return@amap - when (source.title) { - "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) - "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) - } - } catch (e: Exception) { - logError(e) - } - } - } - - private fun DecryptUrl(encUrl: String): String { - var data = encUrl.toByteArray() - data = Base64.decode(data, Base64.URL_SAFE) - val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4") - val cipher = Cipher.getInstance("RC4") - cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters) - data = cipher.doFinal(data) - return URLDecoder.decode(data.toString(Charsets.UTF_8), "utf-8") - } - - data class VidsrctoEpisodeSources( - @JsonProperty("status") val status: Int, - @JsonProperty("result") val result: List? - ) - - data class VidsrctoResult( - @JsonProperty("id") val id: String, - @JsonProperty("title") val title: String - ) - - data class VidsrctoEmbedSource( - @JsonProperty("status") val status: Int, - @JsonProperty("result") val result: VidsrctoUrl - ) - - data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) -} +package com.lagradost.cloudstream3.extractors + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64Decode +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import java.net.URLDecoder +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +class VidSrcTo : ExtractorApi() { + override val name = "VidSrcTo" + override val mainUrl = "https://vidsrc.to" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return + val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return + if (res.status != 200) return + res.result?.amap { source -> + try { + val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap + val finalUrl = DecryptUrl(embedRes.result.encUrl) + if(finalUrl.equals(embedRes.result.encUrl)) return@amap + when (source.title) { + "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + } + } catch (e: Exception) { + logError(e) + } + } + } + + @OptIn(ExperimentalEncodingApi::class) + private fun DecryptUrl(encUrl: String): String { + val data = Base64.UrlSafe.decode(encUrl) + val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters) + val finalData = cipher.doFinal(data) + return URLDecoder.decode(finalData.toString(Charsets.UTF_8), "utf-8") + } + + data class VidsrctoEpisodeSources( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: List? + ) + + data class VidsrctoResult( + @JsonProperty("id") val id: String, + @JsonProperty("title") val title: String + ) + + data class VidsrctoEmbedSource( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: VidsrctoUrl + ) + + data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt index 2439b8ad..1161ff66 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoVard.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoVard.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VideoVard.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoVard.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidguard.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidguard.kt index 230a9e1a..c48b683c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidguard.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.AppUtils @@ -87,7 +87,7 @@ open class Vidguardto : ExtractorApi() { } Log.d("runJS", "Result: $result") } catch (e: Exception) { - Log.e("runJS", "Error executing JavaScript", e) + Log.e("runJS", "Error executing JavaScript: ${e.message}") } finally { Context.exit() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vido.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vido.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vido.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vido.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Voe.kt similarity index 93% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Voe.kt index 67fd7eea..1d7dee7c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Voe.kt @@ -1,9 +1,9 @@ package com.lagradost.cloudstream3.extractors -import android.util.Base64 import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64Decode import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink @@ -58,12 +58,12 @@ open class Voe : ExtractorApi() { videoLinks.add( when { linkRegex.matches(link) -> link - else -> String(Base64.decode(link, Base64.DEFAULT)) + else -> base64Decode(link) } ) } else { val link2 = base64Regex.find(script)?.value ?: return - val decoded = Base64.decode(link2, Base64.DEFAULT).toString() + val decoded = base64Decode(link2) val videoLinkDTO = AppUtils.parseJson(decoded) videoLinkDTO.let { videoLinks.add(it.toString()) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vtbe.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vtbe.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/WatchSB.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WatchSB.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/WatchSB.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WatchSB.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WcoStream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WcoStream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Wibufile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Wibufile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/YourUpload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YourUpload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/YourUpload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YourUpload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Zorofile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zorofile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Zorofile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zorofile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Zplayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zplayer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Zplayer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zplayer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt index 0b401c06..bd42424f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors.helper -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt similarity index 76% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt index 768fa1f6..35aec2b1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt @@ -1,8 +1,6 @@ package com.lagradost.cloudstream3.extractors.helper import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.app class WcoHelper { @@ -30,9 +28,7 @@ class WcoHelper { private suspend fun getKeys() { keys = keys ?: app.get("https://raw.githubusercontent.com/reduplicated/Cloudstream/master/docs/keys.json") - .parsedSafe()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey( - BACKUP_KEY_DATA - ) + .parsedSafe() } suspend fun getWcoKey(): ExternalKeys? { @@ -43,9 +39,7 @@ class WcoHelper { private suspend fun getNewKeys() { newKeys = newKeys ?: app.get("https://raw.githubusercontent.com/chekaslowakiya/BruhFlow/main/keys.json") - .parsedSafe()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey( - BACKUP_KEY_DATA - ) + .parsedSafe() } suspend fun getNewWcoKey(): NewExternalKeys? { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.kt new file mode 100644 index 00000000..8baf2f31 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.kt @@ -0,0 +1,28 @@ +package com.lagradost.cloudstream3.network + +import com.lagradost.cloudstream3.USER_AGENT +import okhttp3.Interceptor + +/** + * When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...) + * @param interceptUrl will stop the WebView when reaching this url. + * @param additionalUrls this will make resolveUsingWebView also return all other requests matching the list of Regex. + * @param userAgent if null then will use the default user agent + * @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare. + * @param script pass custom js to execute + * @param scriptCallback will be called with the result from custom js + * @param timeout close webview after timeout + * */ +expect class WebViewResolver( + interceptUrl: Regex, + additionalUrls: List = emptyList(), + userAgent: String? = USER_AGENT, + useOkhttp: Boolean = true, + script: String? = null, + scriptCallback: ((String) -> Unit)? = null, + timeout: Long = DEFAULT_TIMEOUT +) : Interceptor { + companion object { + val DEFAULT_TIMEOUT: Long + } +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt new file mode 100644 index 00000000..676ac6fe --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -0,0 +1,10 @@ +package com.lagradost.cloudstream3.syncproviders + +enum class SyncIdName { + Anilist, + MyAnimeList, + Trakt, + Imdb, + Simkl, + LocalList, +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt new file mode 100644 index 00000000..374751a8 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -0,0 +1,24 @@ +package com.lagradost.cloudstream3.utils + +import com.fasterxml.jackson.module.kotlin.readValue +import com.lagradost.cloudstream3.mapper + +object AppUtils { + /** Any object as json string */ + fun Any.toJson(): String { + if (this is String) return this + return mapper.writeValueAsString(this) + } + + inline fun parseJson(value: String): T { + return mapper.readValue(value) + } + + inline fun tryParseJson(value: String?): T? { + return try { + parseJson(value ?: return null) + } catch (_: Exception) { + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt index ce6e5ecc..566e29f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -1,9 +1,8 @@ package com.lagradost.cloudstream3.utils -import android.net.Uri import com.fasterxml.jackson.annotation.JsonIgnore +import com.lagradost.cloudstream3.IDownloadableMinimum import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.extractors.AStreamHub @@ -431,7 +430,7 @@ open class ExtractorLink constructor( /** Used for getExtractorVerifierJob() */ open val extractorData: String? = null, open val type: ExtractorLinkType, -) : VideoDownloadManager.IDownloadableMinimum { +) : IDownloadableMinimum { val isM3u8: Boolean get() = type == ExtractorLinkType.M3U8 val isDash: Boolean get() = type == ExtractorLinkType.DASH @@ -530,29 +529,6 @@ open class ExtractorLink constructor( } } -data class ExtractorUri( - val uri: Uri, - val name: String, - - val basePath: String? = null, - val relativePath: String? = null, - val displayName: String? = null, - - val id: Int? = null, - val parentId: Int? = null, - val episode: Int? = null, - val season: Int? = null, - val headerName: String? = null, - val tvType: TvType? = null, -) - -data class ExtractorSubtitleLink( - val name: String, - override val url: String, - override val referer: String, - override val headers: Map = mapOf() -) : VideoDownloadManager.IDownloadableMinimum - /** * Removes https:// and www. * To match urls regardless of schema, perhaps Uri() can be used? diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UnshortenUrl.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt similarity index 96% rename from app/src/main/java/com/lagradost/cloudstream3/utils/UnshortenUrl.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt index 46b232f6..b13e88e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UnshortenUrl.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.utils -import android.util.Base64 import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.base64Decode import com.lagradost.nicehttp.NiceResponse @@ -91,13 +90,12 @@ object ShortLink { } val encodedbytearray = encodedUri.map { it.code.toByte() }.toByteArray() var decodedUri = - Base64.decode(encodedbytearray, Base64.DEFAULT).decodeToString().dropLast(16) + base64Decode(encodedbytearray.toString()).dropLast(16) .drop(16) if (Regex("""go\.php\?u=""").find(decodedUri) != null) { decodedUri = - Base64.decode(decodedUri.replace(Regex("""(.*?)u="""), ""), Base64.DEFAULT) - .decodeToString() + base64Decode(decodedUri.replace(Regex("""(.*?)u="""), "")) } return decodedUri diff --git a/library/src/jvmMain/kotlin/com/lagradost/api/ContextHelper.jvm.kt b/library/src/jvmMain/kotlin/com/lagradost/api/ContextHelper.jvm.kt new file mode 100644 index 00000000..a30810b8 --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/api/ContextHelper.jvm.kt @@ -0,0 +1,10 @@ +package com.lagradost.api + +import java.lang.ref.WeakReference + +actual fun getContext(): Any? { + return null +} + +actual fun setContext(context: WeakReference) { +} \ No newline at end of file diff --git a/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.jvm.kt b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.jvm.kt new file mode 100644 index 00000000..6b99ef3b --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.jvm.kt @@ -0,0 +1,35 @@ +package com.lagradost.cloudstream3.network + +import okhttp3.Interceptor +import okhttp3.Response + +/** + * When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...) + * @param interceptUrl will stop the WebView when reaching this url. + * @param additionalUrls this will make resolveUsingWebView also return all other requests matching the list of Regex. + * @param userAgent if null then will use the default user agent + * @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare. + * @param script pass custom js to execute + * @param scriptCallback will be called with the result from custom js + * @param timeout close webview after timeout + * */ +actual class WebViewResolver actual constructor( + interceptUrl: Regex, + additionalUrls: List, + userAgent: String?, + useOkhttp: Boolean, + script: String?, + scriptCallback: ((String) -> Unit)?, + timeout: Long +) : + Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + return chain.proceed(request) + } + + actual companion object { + actual val DEFAULT_TIMEOUT = 60_000L + } +} From e5c9e96c8347cf31cc7ee25e2490cc70046167c3 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:33:21 +0200 Subject: [PATCH 521/570] fix filesystem --- .../commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt | 5 +++++ .../commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt | 6 ------ 2 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index 47ef5382..aa08cb59 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -29,6 +29,11 @@ import kotlin.math.absoluteValue **/ const val AllLanguagesName = "universal" +const val USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" + +class ErrorLoadingException(message: String? = null) : Exception(message) + //val baseHeader = mapOf("User-Agent" to USER_AGENT) val mapper = JsonMapper.builder().addModule(kotlinModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt deleted file mode 100644 index 160ff098..00000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.lagradost.cloudstream3 - -const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" - -class ErrorLoadingException(message: String? = null) : Exception(message) \ No newline at end of file From c1b5f5c128859158a5ef022d0f16eb129b963651 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 4 Jul 2024 15:51:07 -0600 Subject: [PATCH 522/570] Fix download button display bug in adapter (#1175) --- .../ui/download/DownloadAdapter.kt | 22 ++++++-- .../ui/download/DownloadFragment.kt | 3 +- .../ui/download/button/PieFetchButton.kt | 50 +++++++++---------- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt index b4a16a66..9a026334 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.ViewGroup +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter @@ -13,9 +14,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull -import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.ui.download.button.DownloadStatusTell +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.UIHelper.setImage @@ -135,8 +135,15 @@ class DownloadAdapter( downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes) // We will let the view model handle this downloadButton.doSetProgress = false + downloadButton.progressBar.progressDrawable = + downloadButton.getDrawableFromStatus(status) + ?.let { ContextCompat.getDrawable(downloadButton.context, it) } downloadHeaderInfo.text = formattedSizeString - } else downloadButton.doSetProgress = true + } else { + downloadButton.doSetProgress = true + downloadButton.progressBar.progressDrawable = + ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) + } downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback) downloadButton.isVisible = true @@ -197,8 +204,15 @@ class DownloadAdapter( downloadButton.applyMetaData(d.id, card.currentBytes, card.totalBytes) // We will let the view model handle this downloadButton.doSetProgress = false + downloadButton.progressBar.progressDrawable = + downloadButton.getDrawableFromStatus(status) + ?.let { ContextCompat.getDrawable(downloadButton.context, it) } downloadChildEpisodeTextExtra.text = formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes) - } else downloadButton.doSetProgress = true + } else { + downloadButton.doSetProgress = true + downloadButton.progressBar.progressDrawable = + ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) + } downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, mediaClickCallback) downloadButton.isVisible = true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 82c5ffb8..23d546e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -40,7 +40,6 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult -import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE import com.lagradost.cloudstream3.utils.DataStore import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -273,4 +272,4 @@ class DownloadFragment : Fragment() { val selectedVideoUri = result?.data?.data ?: return@registerForActivityResult playUri(activity ?: return@registerForActivityResult, selectedVideoUri) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index a6dc5c56..abc159d0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -1,7 +1,6 @@ package com.lagradost.cloudstream3.ui.download.button import android.content.Context -import android.graphics.drawable.Drawable import android.os.Looper import android.util.AttributeSet import android.util.Log @@ -45,6 +44,8 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : private var iconPaused: Int = 0 private var hideWhenIcon: Boolean = true + var progressDrawable: Int = 0 + var overrideLayout: Int? = null companion object { @@ -115,10 +116,10 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : R.styleable.PieFetchButton_download_icon_complete, R.drawable.download_icon_done ) iconPaused = getResourceId( - R.styleable.PieFetchButton_download_icon_paused, 0//R.drawable.download_icon_pause + R.styleable.PieFetchButton_download_icon_paused, 0 // R.drawable.download_icon_pause ) iconActive = getResourceId( - R.styleable.PieFetchButton_download_icon_active, 0 //R.drawable.download_icon_load + R.styleable.PieFetchButton_download_icon_active, 0 // R.drawable.download_icon_load ) iconWaiting = getResourceId( R.styleable.PieFetchButton_download_icon_waiting, 0 @@ -129,7 +130,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : val fillIndex = getInt(R.styleable.PieFetchButton_download_fill, 0) - val progressDrawable = getResourceId( + progressDrawable = getResourceId( R.styleable.PieFetchButton_download_fill_override, fillArray[fillIndex] ) @@ -170,7 +171,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : if (isZeroBytes) { removeKey(KEY_RESUME_PACKAGES, card.id.toString()) callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card)) - //callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) + // callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) } else { val list = arrayListOf( Pair(DOWNLOAD_ACTION_PLAY_FILE, R.string.popup_play_file), @@ -197,7 +198,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : list ) { callback(DownloadClickEvent(itemId, card)) - //callback.invoke(DownloadClickEvent(itemId, data)) + // callback.invoke(DownloadClickEvent(itemId, data)) } } } @@ -205,7 +206,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : view.setOnLongClickListener { callback(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, card)) - //clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data)) + // clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data)) return@setOnLongClickListener true } } @@ -218,7 +219,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : setDefaultClickListener(this, textView, card, callback) } - /*open fun setDefaultClickListener(requestGetter: suspend BaseFetchButton.() -> List) { + /* open fun setDefaultClickListener(requestGetter: suspend BaseFetchButton.() -> List) { this.setOnClickListener { when (this.currentStatus) { null -> { @@ -244,7 +245,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : else -> {} } } - }*/ + } */ @MainThread private fun setStatusInternal(status : DownloadStatusTell?) { @@ -262,7 +263,8 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : progressBarBackground.background = ContextCompat.getDrawable(context, progressDrawable) - val drawable = getDrawableFromStatus(status) + val drawable = + getDrawableFromStatus(status)?.let { ContextCompat.getDrawable(this.context, it) } statusView.setImageDrawable(drawable) val isDrawable = drawable != null @@ -280,12 +282,12 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : override fun setStatus(status: DownloadStatusTell?) { currentStatus = status - // runs on the main thread, but also instant if it already is + // Runs on the main thread, but also instant if it already is if (Looper.myLooper() == Looper.getMainLooper()) { try { setStatusInternal(status) } catch (t : Throwable) { - logError(t) // just in case setStatusInternal throws because thread + logError(t) // Just in case setStatusInternal throws because thread progressBarBackground.post { setStatusInternal(status) } @@ -325,19 +327,13 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } } - open fun getDrawableFromStatus(status: DownloadStatusTell?): Drawable? { - val drawableInt = when (status) { - DownloadStatusTell.IsPaused -> iconPaused - DownloadStatusTell.IsPending -> iconWaiting - DownloadStatusTell.IsDownloading -> iconActive - DownloadStatusTell.IsFailed -> iconError - DownloadStatusTell.IsDone -> iconComplete - DownloadStatusTell.IsStopped -> iconRemoved - null -> iconInit - } - if (drawableInt == 0) { - return null - } - return ContextCompat.getDrawable(this.context, drawableInt) - } + open fun getDrawableFromStatus(status: DownloadStatusTell?): Int? = when (status) { + DownloadStatusTell.IsPaused -> iconPaused + DownloadStatusTell.IsPending -> iconWaiting + DownloadStatusTell.IsDownloading -> iconActive + DownloadStatusTell.IsFailed -> iconError + DownloadStatusTell.IsDone -> iconComplete + DownloadStatusTell.IsStopped -> iconRemoved + else -> iconInit + }.takeIf { it != 0 } } \ No newline at end of file From e1d4a46309f8a979327e5b5486f2f8753f0a0639 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:26:44 +0200 Subject: [PATCH 523/570] bugfix on lib startup --- .../lagradost/cloudstream3/MainActivity.kt | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a47e7685..59f499c5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1112,23 +1112,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa MainAPI.settingsForProvider = settingsForProvider - // Change library icon with logo of current api in sync - libraryViewModel = ViewModelProvider(this)[LibraryViewModel::class.java] - libraryViewModel?.currentApiName?.observe(this) { - val syncAPI = libraryViewModel?.currentSyncApi - Log.i("SYNC_API", "${syncAPI?.name}, ${syncAPI?.idPrefix}") - val icon = if (syncAPI?.idPrefix == localListApi.idPrefix) { - R.drawable.library_icon - } else { - syncAPI?.icon ?: R.drawable.library_icon - } - - binding?.apply { - navRailView.menu.findItem(R.id.navigation_library)?.setIcon(icon) - navView.menu.findItem(R.id.navigation_library)?.setIcon(icon) - } - } - loadThemes(this) updateLocale() super.onCreate(savedInstanceState) @@ -1538,6 +1521,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa logError(e) } } + + // we need to run this after we init all apis, otherwise currentSyncApi will fuck itself + this@MainActivity.runOnUiThread { + // Change library icon with logo of current api in sync + libraryViewModel = ViewModelProvider(this@MainActivity)[LibraryViewModel::class.java] + libraryViewModel?.currentApiName?.observe(this@MainActivity) { + val syncAPI = libraryViewModel?.currentSyncApi + Log.i("SYNC_API", "${syncAPI?.name}, ${syncAPI?.idPrefix}") + val icon = if (syncAPI?.idPrefix == localListApi.idPrefix) { + R.drawable.library_icon + } else { + syncAPI?.icon ?: R.drawable.library_icon + } + + binding?.apply { + navRailView.menu.findItem(R.id.navigation_library)?.setIcon(icon) + navView.menu.findItem(R.id.navigation_library)?.setIcon(icon) + } + } + } } SearchResultBuilder.updateCache(this) From 699a6979a5d6a924859d5dff122de34389a100a7 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 5 Jul 2024 19:04:32 +0300 Subject: [PATCH 524/570] feat(TV UI): Fix clone site focus (#1179) --- app/src/main/res/layout/add_remove_sites.xml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/layout/add_remove_sites.xml b/app/src/main/res/layout/add_remove_sites.xml index 9ef6ad6a..653f607f 100644 --- a/app/src/main/res/layout/add_remove_sites.xml +++ b/app/src/main/res/layout/add_remove_sites.xml @@ -1,19 +1,21 @@ + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + android:id="@+id/add_site" + android:text="@string/add_site_pref" + android:focusable="true" + style="@style/SettingsItem"> + android:id="@+id/remove_site" + android:text="@string/remove_site_pref" + android:focusable="true" + style="@style/SettingsItem" /> \ No newline at end of file From 9b1ac5fc28774585f207acd0a5444cc9d09933b6 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 5 Jul 2024 19:05:32 +0300 Subject: [PATCH 525/570] feat(Trakt): Skip specials season for next airing (#1181) --- .../com/lagradost/cloudstream3/metaproviders/TraktProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 736e05f2..7c375e0a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -238,7 +238,7 @@ open class TraktProvider : MainAPI() { description = episode.overview, ).apply { this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") - if (nextAir == null && this.date != null && this.date!! > unixTimeMS) { + if (nextAir == null && this.date != null && this.date!! > unixTimeMS && this.season != 0) { nextAir = NextAiring( episode = this.episode!!, unixTime = this.date!!.div(1000L), From 145c42f1c8bdbd53a18733786778be6ff9f77d2b Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 5 Jul 2024 19:10:58 +0300 Subject: [PATCH 526/570] feat(UI): Use same Episode holder size (#1180) --- .../cloudstream3/ui/result/EpisodeAdapter.kt | 7 +++---- app/src/main/res/layout/result_episode.xml | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 0a1b777d..ed5e51f1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -57,8 +57,7 @@ const val ACTION_PLAY_EPISODE_IN_MPV = 17 const val ACTION_MARK_AS_WATCHED = 18 const val ACTION_FCAST = 19 -const val TV_EP_SIZE_LARGE = 400 -const val TV_EP_SIZE_SMALL = 300 +const val TV_EP_SIZE = 400 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) class EpisodeAdapter( @@ -181,7 +180,7 @@ class EpisodeAdapter( fun bind(card: ResultEpisode) { localCard = card val setWidth = - if (isLayout(TV or EMULATOR)) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT + if (isLayout(TV or EMULATOR)) TV_EP_SIZE.toPx else ViewGroup.LayoutParams.MATCH_PARENT binding.episodeLinHolder.layoutParams.width = setWidth binding.episodeHolderLarge.layoutParams.width = setWidth @@ -336,7 +335,7 @@ class EpisodeAdapter( fun bind(card: ResultEpisode) { binding.episodeHolder.layoutParams.apply { width = - if (isLayout(TV or EMULATOR)) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT + if (isLayout(TV or EMULATOR)) TV_EP_SIZE.toPx else ViewGroup.LayoutParams.MATCH_PARENT } binding.apply { diff --git a/app/src/main/res/layout/result_episode.xml b/app/src/main/res/layout/result_episode.xml index b56cdb1d..36d60bd6 100644 --- a/app/src/main/res/layout/result_episode.xml +++ b/app/src/main/res/layout/result_episode.xml @@ -90,14 +90,15 @@ android:textColor="?attr/textColor" tools:text="Episode 1" /> - - + + + \ No newline at end of file From e86c926c30d565841d0701adac937d48dc9a8d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Mon, 8 Jul 2024 23:59:02 +0300 Subject: [PATCH 527/570] Extractor: added Pichive & Sobreatsesuyp (#1184) --- .../extractors/HotlingerExtractor.kt | 5 ++ .../extractors/SobreatsesuypExtractor.kt | 56 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 4 ++ 3 files changed, 65 insertions(+) create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt index db721108..11f8ccaf 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt @@ -22,6 +22,11 @@ class FourPlayRu : ContentX() { override var mainUrl = "https://four.playru.net" } +class Pichive : ContentX() { + override var name = "Pichive" + override var mainUrl = "https://pichive.online" +} + class FourPichive : ContentX() { override var name = "FourPichive" override var mainUrl = "https://four.pichive.online" diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt new file mode 100644 index 00000000..91b60dac --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt @@ -0,0 +1,56 @@ +// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır. + +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* +import com.fasterxml.jackson.annotation.JsonProperty + +open class Sobreatsesuyp : ExtractorApi() { + override val name = "Sobreatsesuyp" + override val mainUrl = "https://sobreatsesuyp.com" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { + val extRef = referer ?: "" + + val videoReq = app.get(url, referer = extRef).text + + val file = Regex("""file\":\"([^\"]+)""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val postLink = "${mainUrl}/" + file.replace("\\", "") + val rawList = app.post(postLink, referer = extRef).parsedSafe>() ?: throw ErrorLoadingException("Post link not found") + + val postJson: List = rawList.drop(1).map { item -> + val mapItem = item as Map<*, *> + SobreatsesuypVideoData( + title = mapItem["title"] as? String, + file = mapItem["file"] as? String + ) + } + Log.d("Kekik_${this.name}", "postJson » ${postJson}") + + for (item in postJson) { + if (item.file == null || item.title == null) continue + + val fileUrl = "${mainUrl}/playlist/${item.file.substring(1)}.txt" + val videoData = app.post(fileUrl, referer = extRef).text + + callback.invoke( + ExtractorLink( + source = this.name, + name = "${this.name} - ${item.title}", + url = videoData, + referer = extRef, + quality = Qualities.Unknown.value, + type = INFER_TYPE + ) + ) + } + } + + data class SobreatsesuypVideoData( + @JsonProperty("title") val title: String? = null, + @JsonProperty("file") val file: String? = null + ) +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 566e29f0..0df73a0e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -115,6 +115,7 @@ import com.lagradost.cloudstream3.extractors.Hotlinger import com.lagradost.cloudstream3.extractors.FourCX import com.lagradost.cloudstream3.extractors.PlayRu import com.lagradost.cloudstream3.extractors.FourPlayRu +import com.lagradost.cloudstream3.extractors.Pichive import com.lagradost.cloudstream3.extractors.FourPichive import com.lagradost.cloudstream3.extractors.HDMomPlayer import com.lagradost.cloudstream3.extractors.HDPlayerSystem @@ -124,6 +125,7 @@ import com.lagradost.cloudstream3.extractors.HDStreamAble import com.lagradost.cloudstream3.extractors.RapidVid import com.lagradost.cloudstream3.extractors.TRsTX import com.lagradost.cloudstream3.extractors.VidMoxy +import com.lagradost.cloudstream3.extractors.Sobreatsesuyp import com.lagradost.cloudstream3.extractors.PixelDrain import com.lagradost.cloudstream3.extractors.MailRu import com.lagradost.cloudstream3.extractors.Mediafire @@ -734,6 +736,7 @@ val extractorApis: MutableList = arrayListOf( FourCX(), PlayRu(), FourPlayRu(), + Pichive(), FourPichive(), HDMomPlayer(), HDPlayerSystem(), @@ -743,6 +746,7 @@ val extractorApis: MutableList = arrayListOf( RapidVid(), TRsTX(), VidMoxy(), + Sobreatsesuyp(), PixelDrain(), MailRu(), From 8be8e5474647e5aeb31cb1026fe2e350dbf3c139 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:17:25 +0200 Subject: [PATCH 528/570] Fixed log --- .../cloudstream3/extractors/SobreatsesuypExtractor.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt index 91b60dac..c90b22f4 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty @@ -28,15 +27,13 @@ open class Sobreatsesuyp : ExtractorApi() { file = mapItem["file"] as? String ) } - Log.d("Kekik_${this.name}", "postJson » ${postJson}") for (item in postJson) { if (item.file == null || item.title == null) continue - val fileUrl = "${mainUrl}/playlist/${item.file.substring(1)}.txt" - val videoData = app.post(fileUrl, referer = extRef).text + val videoData = app.post("${mainUrl}/playlist/${item.file.substring(1)}.txt", referer = extRef).text - callback.invoke( + callback.invoke( ExtractorLink( source = this.name, name = "${this.name} - ${item.title}", From febb843424e0331b63b2e26ad796e797f7267ccc Mon Sep 17 00:00:00 2001 From: RowdyRushya Date: Mon, 15 Jul 2024 08:06:20 -0700 Subject: [PATCH 529/570] Fix VidSrcTo extractor (#1198) --- .../cloudstream3/extractors/Chillx.kt | 29 +++++++++---------- .../cloudstream3/extractors/VidSrcTo.kt | 14 ++++++++- .../cloudstream3/extractors/Vidplay.kt | 27 +++++++++++++---- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt index 26567c7a..dd22efb2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -6,6 +6,7 @@ import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +import kotlin.run class Moviesapi : Chillx() { override val name = "Moviesapi" @@ -28,17 +29,22 @@ open class Chillx : ExtractorApi() { override val requiresReferer = true companion object { + private val keySource = "https://rowdy-avocado.github.io/multi-keys/" + private var key: String? = null - suspend fun fetchKey(): String { - return if (key != null) { - key!! - } else { - val fetch = app.get("https://raw.githubusercontent.com/rushi-chavan/multi-keys/keys/keys.json").parsedSafe()?.key?.get(0) ?: throw ErrorLoadingException("Unable to get key") - key = fetch - key!! - } + private suspend fun fetchKey(): String { + return key + ?: run { + val res = + app.get(keySource).parsedSafe() + ?: throw ErrorLoadingException("Unable to get keys") + key = res.keys.get(0) + res.keys.get(0) + } } + + private data class KeysData(@JsonProperty("chillx") val keys: List) } @Suppress("NAME_SHADOWING") @@ -97,11 +103,4 @@ open class Chillx : ExtractorApi() { it.groupValues[1].toInt(16).toChar().toString() } } - - - - data class Keys( - @JsonProperty("chillx") val key: List - ) - } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt index 73857fb3..578f5fb9 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -26,7 +26,13 @@ class VidSrcTo : ExtractorApi() { callback: (ExtractorLink) -> Unit ) { val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return - val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return + val subtitlesLink = "$mainUrl/ajax/embed/episode/$mediaId/subtitles" + val subRes = app.get(subtitlesLink).parsedSafe>() + subRes?.forEach { + if (it.kind.equals("captions")) subtitleCallback.invoke(SubtitleFile(it.label, it.file)) + } + val sourcesLink = "$mainUrl/ajax/embed/episode/$mediaId/sources" + val res = app.get(sourcesLink).parsedSafe() ?: return if (res.status != 200) return res.result?.amap { source -> try { @@ -68,5 +74,11 @@ class VidSrcTo : ExtractorApi() { @JsonProperty("result") val result: VidsrctoUrl ) + data class VidsrctoSubtitles( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String, + @JsonProperty("kind") val kind: String + ) + data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt index cb9eaf1e..6202800f 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.base64Encode @@ -9,6 +10,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec +import kotlin.run // Code found in https://github.com/KillerDogeEmpire/vidplay-keys // special credits to @KillerDogeEmpire for providing key @@ -35,8 +37,25 @@ open class Vidplay : ExtractorApi() { override val name = "Vidplay" override val mainUrl = "https://vidplay.site" override val requiresReferer = true - open val key = - "https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json" + + companion object { + private val keySource = "https://rowdy-avocado.github.io/multi-keys/" + + private var keys: List? = null + + private suspend fun getKeys(): List { + return keys + ?: run { + val res = + app.get(keySource).parsedSafe() + ?: throw ErrorLoadingException("Unable to get keys") + keys = res.keys + res.keys + } + } + + private data class KeysData(@JsonProperty("vidplay") val keys: List) + } override suspend fun getUrl( url: String, @@ -70,10 +89,6 @@ open class Vidplay : ExtractorApi() { } - private suspend fun getKeys(): List { - return app.get(key).parsed() - } - private suspend fun callFutoken(id: String, url: String): String? { val script = app.get("$mainUrl/futoken", referer = url).text val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null From 694193fa3eeada9388d68be521822ecf4f659bd4 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 15 Jul 2024 20:40:41 +0530 Subject: [PATCH 530/570] refactor(fix): result sync, fix slider theme and trailer fix (#1187) --- app/build.gradle.kts | 24 +- app/src/main/res/layout/result_sync.xml | 397 ++++++++++-------------- 2 files changed, 168 insertions(+), 253 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ebefa0ea..6e439d53 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -157,16 +157,16 @@ dependencies { testImplementation("junit:junit:4.13.2") testImplementation("org.json:json:20240303") androidTestImplementation("androidx.test:core") - implementation("androidx.test.ext:junit-ktx:1.1.5") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation("androidx.test.ext:junit-ktx:1.2.1") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") // Android Core & Lifecycle implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.navigation:navigation-ui-ktx:2.7.7") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.2") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.2") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.3") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3") implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") // Design & UI @@ -182,9 +182,9 @@ dependencies { implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0") // For KSP -> Official Annotation Processors are Not Yet Supported for KSP - ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") - implementation("com.google.guava:guava:33.2.0-android") - implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") + ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0") + implementation("com.google.guava:guava:33.2.1-android") + implementation("dev.zacsweers.autoservice:auto-service-ksp:1.2.0") // Media 3 (ExoPlayer) implementation("androidx.media3:media3-ui:1.1.1") @@ -200,9 +200,9 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:592f159") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ - implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding + implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding // Crash Reports (AcraApplication.kt) implementation("ch.acra:acra-core:5.11.3") @@ -215,14 +215,14 @@ dependencies { implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview - implementation("io.github.g0dkar:qrcode-kotlin:4.1.1") // QR code for PIN Auth on TV + implementation("io.github.g0dkar:qrcode-kotlin:4.2.0") // QR code for PIN Auth on TV // Extensions & Other Libs implementation("org.mozilla:rhino:1.7.15") // run JavaScript implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9 - implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0") // TMDB API v3 Wrapper Made with RetroFit + implementation("com.uwetrottmann.tmdb2:tmdb-java:2.11.0") // TMDB API v3 Wrapper Made with RetroFit coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API diff --git a/app/src/main/res/layout/result_sync.xml b/app/src/main/res/layout/result_sync.xml index 9cde195c..8b7b33c0 100644 --- a/app/src/main/res/layout/result_sync.xml +++ b/app/src/main/res/layout/result_sync.xml @@ -1,306 +1,221 @@ + android:layout_width="match_parent" + android:layout_height="match_parent"> + android:id="@+id/result_sync_holder" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:visibility="gone" + tools:visibility="visible"> + + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + android:id="@+id/result_sync_names" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:text="MyAnimeList, AniList" + android:textSize="16sp" + android:textStyle="bold" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:visibility="visible"> + android:id="@+id/result_sync_sub_episode" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end|center_vertical" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:padding="10dp" + android:src="@drawable/baseline_remove_24" + app:tint="?attr/textColor" /> + android:id="@+id/result_sync_current_episodes" + style="@style/AppEditStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:inputType="number" + android:textColorHint="?attr/grayTextColor" + android:textSize="20sp" + tools:hint="20" + tools:ignore="LabelFor" /> + android:id="@+id/result_sync_max_episodes" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:paddingBottom="1dp" + android:textColor="?attr/textColor" + android:textSize="20sp" + tools:text="30" /> + android:id="@+id/result_sync_add_episode" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end|center_vertical" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:padding="10dp" + android:src="@drawable/ic_baseline_add_24" + app:tint="?attr/textColor" /> - - + android:id="@+id/result_sync_episodes" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="20dp" + android:layout_gravity="end|center_vertical" + android:indeterminate="false" + android:max="100" + android:padding="10dp" + android:progress="0" + android:progressBackgroundTint="?attr/colorPrimary" + tools:visibility="visible" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:padding="10dp" + android:text="@string/sync_score" + android:textColor="?attr/textColor" + android:textSize="17sp" /> + style="@style/BlackButton" + android:layout_width="wrap_content" + android:layout_height="30dp" + android:layout_gravity="center_vertical" + android:layout_marginStart="0dp" + android:minWidth="0dp" + android:text="7/10" /> + android:id="@+id/result_sync_rating" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="-5dp" + android:layout_marginEnd="-5dp" + app:thumbHeight="20dp" + android:stepSize="1" + android:value="4" + android:valueFrom="0" + android:valueTo="10" + app:labelStyle="@style/BlackLabel" + app:thumbRadius="10dp" + app:tickVisible="false" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingTop="12dp" + android:paddingBottom="12dp" + android:visibility="gone"> + android:id="@+id/home_parent_item_title" + style="@style/WatchHeaderText" + tools:text="Recommended" /> + android:layout_width="30dp" + android:layout_height="match_parent" + android:layout_gravity="end|center_vertical" + android:layout_marginEnd="5dp" + android:contentDescription="@string/home_more_info" + android:src="@drawable/ic_baseline_arrow_forward_24" + app:tint="?attr/textColor" /> + android:id="@+id/result_sync_check" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_rowWeight="1" + tools:listitem="@layout/sort_bottom_single_choice" /> + style="@style/WhiteButton" + android:layout_width="match_parent" + android:layout_marginTop="10dp" + android:text="@string/type_watching" + android:visibility="gone" /> + android:id="@+id/result_sync_set_score" + style="@style/BlackButton" + android:layout_width="match_parent" + android:layout_marginTop="10dp" + android:text="@string/upload_sync" + app:icon="@drawable/baseline_sync_24" /> + android:id="@+id/result_sync_loading_shimmer" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:orientation="vertical" + android:padding="15dp" + app:shimmer_auto_start="true" + app:shimmer_base_alpha="0.2" + app:shimmer_duration="@integer/loading_time" + app:shimmer_highlight_alpha="0.3" + tools:visibility="gone" + tools:ignore="MissingClass"> + + - - + android:layout_height="30dp" /> + android:layout_width="match_parent" + android:layout_height="30dp" /> + android:layout_width="match_parent" + android:layout_height="30dp" /> @@ -313,8 +228,8 @@ + android:layout_width="match_parent" + android:layout_height="30dp" /> From a157115cfac1ef3f3c532198a931873d6cee9097 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Mon, 15 Jul 2024 18:15:59 +0300 Subject: [PATCH 531/570] feat(Subtitles): SubSource subtitles provider (#1199) --- .../syncproviders/AccountManager.kt | 4 +- .../syncproviders/providers/SubSource.kt | 158 ++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt 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 e86d73aa..0259ccad 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -22,6 +22,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val addic7ed = Addic7ed() val subDlApi = SubDlApi(0) val localListApi = LocalList() + val subSourceApi = SubSourceApi() // used to login via app intent val OAuth2Apis @@ -51,7 +52,8 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { get() = listOf( openSubtitlesApi, addic7ed, - subDlApi + subDlApi, + subSourceApi ) const val appString = "cloudstreamapp" diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt new file mode 100644 index 00000000..0e233ece --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt @@ -0,0 +1,158 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.subtitles.AbstractSubProvider +import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities +import com.lagradost.cloudstream3.subtitles.SubtitleResource +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.SubtitleHelper + +class SubSourceApi : AbstractSubProvider { + override val idPrefix = "subsource" + val name = "SubSource" + + companion object { + const val APIURL = "https://api.subsource.net/api" + const val DOWNLOADENDPOINT = "https://api.subsource.net/api/downloadSub" + } + + override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { + + //Only supports Imdb Id search for now + if (query.imdbId == null) return null + val queryLang = SubtitleHelper.fromTwoLettersToLanguage(query.lang!!) + val type = if ((query.seasonNumber ?: 0) > 0) TvType.TvSeries else TvType.Movie + + val searchRes = app.post( + url = "$APIURL/searchMovie", + data = mapOf( + "query" to query.imdbId!! + ) + ).parsedSafe() ?: return null + + val postData = if (type == TvType.TvSeries) { + mapOf( + "langs" to "[]", + "movieName" to searchRes.found.first().linkName, + "season" to "season-${query.seasonNumber}" + ) + } else { + mapOf( + "langs" to "[]", + "movieName" to searchRes.found.first().linkName, + ) + } + + val getMovieRes = app.post( + url = "$APIURL/getMovie", + data = postData + ).parsedSafe().let { + // api doesn't has episode number or lang filtering + if (type == TvType.Movie) { + it?.subs?.filter { sub -> + sub.lang == queryLang + } + } else { + it?.subs?.filter { sub -> + sub.releaseName!!.contains( + String.format( + "E%02d", + query.epNumber + ) + ) && sub.lang == queryLang + } + } + } ?: return null + + return getMovieRes.map { subtitle -> + AbstractSubtitleEntities.SubtitleEntity( + idPrefix = this.idPrefix, + name = subtitle.releaseName!!, + lang = subtitle.lang!!, + data = SubData( + movie = subtitle.linkName!!, + lang = subtitle.lang, + id = subtitle.subId.toString(), + ).toJson(), + type = type, + source = this.name, + epNumber = query.epNumber, + seasonNumber = query.seasonNumber, + isHearingImpaired = subtitle.hi == 1, + ) + } + } + + override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { + + val parsedSub = parseJson(data.data) + + val subRes = app.post( + url = "$APIURL/getSub", + data = mapOf( + "movie" to parsedSub.movie, + "lang" to data.lang, + "id" to parsedSub.id + ) + ).parsedSafe() ?: return + + this.addZipUrl( + "$DOWNLOADENDPOINT/${subRes.sub.downloadToken}" + ) { name, _ -> + name + } + } + + data class ApiSearch( + @JsonProperty("success") val success: Boolean, + @JsonProperty("found") val found: List, + ) + + data class Found( + @JsonProperty("id") val id: Long, + @JsonProperty("title") val title: String, + @JsonProperty("seasons") val seasons: Long, + @JsonProperty("type") val type: String, + @JsonProperty("releaseYear") val releaseYear: Long, + @JsonProperty("linkName") val linkName: String, + ) + + data class ApiResponse( + @JsonProperty("success") val success: Boolean, + @JsonProperty("movie") val movie: Movie, + @JsonProperty("subs") val subs: List, + ) + + data class Movie( + @JsonProperty("id") val id: Long? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("year") val year: Long? = null, + @JsonProperty("fullName") val fullName: String? = null, + ) + + data class Sub( + @JsonProperty("hi") val hi: Int? = null, + @JsonProperty("fullLink") val fullLink: String? = null, + @JsonProperty("linkName") val linkName: String? = null, + @JsonProperty("lang") val lang: String? = null, + @JsonProperty("releaseName") val releaseName: String? = null, + @JsonProperty("subId") val subId: Long? = null, + ) + + data class SubData( + @JsonProperty("movie") val movie: String, + @JsonProperty("lang") val lang: String, + @JsonProperty("id") val id: String, + ) + + data class SubTitleLink( + @JsonProperty("sub") val sub: SubToken, + ) + + data class SubToken( + @JsonProperty("downloadToken") val downloadToken: String, + ) +} \ No newline at end of file From 627dd453093f3246afbc606d83fa13284ce16e5e Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Thu, 18 Jul 2024 02:02:35 +0200 Subject: [PATCH 532/570] 0bytes downloads fix --- .../utils/VideoDownloadManager.kt | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index f3cbdaf1..197bacc6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -546,7 +546,8 @@ object VideoDownloadManager { tryResume: Boolean, ): StreamData { return setupStream( - context.getBasePath().first ?: getDefaultDir(context) ?: throw IOException("Bad config"), + context.getBasePath().first ?: getDefaultDir(context) + ?: throw IOException("Bad config"), name, folder, extension, @@ -945,7 +946,7 @@ object VideoDownloadManager { bufferSize: Int = DEFAULT_BUFFER_SIZE, /** how many bytes bytes it should require to use the parallel downloader instead, * if we download a very small file we don't want it parallel */ - maximumSmallSize : Long = chuckSize * 2 + maximumSmallSize: Long = chuckSize * 2 ): LazyStreamDownloadData { // we don't want to make a separate connection for every 1kb require(chuckSize > 1000) @@ -1028,7 +1029,10 @@ object VideoDownloadManager { tryResume: Boolean, parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, - parallelConnections: Int = 3 + parallelConnections: Int = 3, + /** how many bytes a valid file must be in bytes, + * this should be different for subtitles and video */ + minimumSize: Long = 100 ): DownloadStatus = withContext(Dispatchers.IO) { if (parallelConnections < 1) { return@withContext DOWNLOAD_INVALID_INPUT @@ -1074,6 +1078,13 @@ object VideoDownloadManager { ) ) + if (items.totalLength != null && items.totalLength < minimumSize) { + fileStream.closeQuietly() + metadata.onDelete() + stream.delete() + return@withContext DOWNLOAD_INVALID_INPUT + } + metadata.totalBytes = items.totalLength metadata.type = DownloadType.IsDownloading metadata.setDownloadFileInfoTemplate( @@ -1223,6 +1234,16 @@ object VideoDownloadManager { return@withContext DOWNLOAD_STOPPED } + // in case the head request lies about content-size, + // then we don't want shit output + if (metadata.bytesDownloaded < minimumSize) { + // we need to close before delete + fileStream.closeQuietly() + metadata.onDelete() + stream.delete() + return@withContext DOWNLOAD_INVALID_INPUT + } + metadata.type = DownloadType.IsDone return@withContext DOWNLOAD_SUCCESS } catch (e: IOException) { @@ -1274,6 +1295,7 @@ object VideoDownloadManager { val displayName = getDisplayName(name, extension) val stream = setupStream(baseFile, name, folder, extension, startAt > 0) + if (!stream.resume) startAt = 0 fileStream = stream.open() @@ -1300,6 +1322,7 @@ object VideoDownloadManager { ) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap() ) ) + val items = M3u8Helper2.hslLazy(listOf(m3u8)) metadata.hlsTotal = items.size @@ -1397,7 +1420,7 @@ object VideoDownloadManager { try { // may cause java.lang.IllegalStateException: Mutex is not locked because of cancelling fileMutex.unlock() - } catch (t : Throwable) { + } catch (t: Throwable) { logError(t) } } @@ -1524,7 +1547,7 @@ object VideoDownloadManager { tryResume: Boolean = false, ): DownloadStatus { // no support for these file formats - if(link.type == ExtractorLinkType.MAGNET || link.type == ExtractorLinkType.TORRENT || link.type == ExtractorLinkType.DASH) { + if (link.type == ExtractorLinkType.MAGNET || link.type == ExtractorLinkType.TORRENT || link.type == ExtractorLinkType.DASH) { return DOWNLOAD_INVALID_INPUT } @@ -1556,7 +1579,7 @@ object VideoDownloadManager { } try { - when(link.type) { + when (link.type) { ExtractorLinkType.M3U8 -> { val startIndex = if (tryResume) { context.getKey( @@ -1576,6 +1599,7 @@ object VideoDownloadManager { callback, parallelConnections = maxConcurrentConnections ) } + ExtractorLinkType.VIDEO -> { return downloadThing( context, @@ -1585,9 +1609,13 @@ object VideoDownloadManager { "mp4", tryResume, ep.id, - callback, parallelConnections = maxConcurrentConnections + callback, + parallelConnections = maxConcurrentConnections, + /** We require at least 10 MB video files */ + minimumSize = (1 shl 20) * 10 ) } + else -> throw IllegalArgumentException("unsuported download type") } } catch (t: Throwable) { From 12de92455960f012bb0298723127061b90932327 Mon Sep 17 00:00:00 2001 From: RowdyRushya Date: Fri, 19 Jul 2024 09:10:34 -0700 Subject: [PATCH 533/570] updating vidplay encryption method (#1202) --- .../cloudstream3/extractors/VidSrcTo.kt | 2 +- .../cloudstream3/extractors/Vidplay.kt | 101 ++++++++---------- 2 files changed, 43 insertions(+), 60 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt index 578f5fb9..e974f23a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -40,7 +40,7 @@ class VidSrcTo : ExtractorApi() { val finalUrl = DecryptUrl(embedRes.result.encUrl) if(finalUrl.equals(embedRes.result.encUrl)) return@amap when (source.title) { - "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "F2Cloud" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) } } catch (e: Exception) { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt index 6202800f..d7e7ce18 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -4,13 +4,14 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +import java.net.URLDecoder import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec -import kotlin.run +import kotlin.io.encoding.Base64 // Code found in https://github.com/KillerDogeEmpire/vidplay-keys // special credits to @KillerDogeEmpire for providing key @@ -33,6 +34,7 @@ class VidplayOnline : Vidplay() { override val mainUrl = "https://vidplay.online" } +@OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class) open class Vidplay : ExtractorApi() { override val name = "Vidplay" override val mainUrl = "https://vidplay.site" @@ -58,83 +60,64 @@ open class Vidplay : ExtractorApi() { } override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit ) { + val myKeys = getKeys() + val domain = url.substringBefore("/e/") val id = url.substringBefore("?").substringAfterLast("/") - val encodeId = encodeId(id, getKeys()) - val mediaUrl = callFutoken(encodeId, url) - val res = app.get( - "$mediaUrl", headers = mapOf( - "Accept" to "application/json, text/javascript, */*; q=0.01", - "X-Requested-With" to "XMLHttpRequest", - ), referer = url - ).parsedSafe()?.result - + val encodedId = encode(id, myKeys.get(0)) + val t = url.substringAfter("t=").substringBefore("&") + val h = encode(id, myKeys.get(1)) + val mediaUrl = "$domain/mediainfo/$encodedId?t=$t&h=$h" + val encodedRes = + app.get("$mediaUrl").parsedSafe()?.result + ?: throw Exception("Unable to fetch link") + val decodedRes = decode(encodedRes, myKeys.get(2)) + val res = tryParseJson(decodedRes) res?.sources?.map { - M3u8Helper.generateM3u8( - this.name, - it.file ?: return@map, - "$mainUrl/" - ).forEach(callback) + M3u8Helper.generateM3u8(this.name, it.file ?: return@map, "$mainUrl/").forEach(callback) } res?.tracks?.filter { it.kind == "captions" }?.map { - subtitleCallback.invoke( - SubtitleFile(it.label ?: return@map, it.file ?: return@map) - ) + subtitleCallback.invoke(SubtitleFile(it.label ?: return@map, it.file ?: return@map)) } - } - private suspend fun callFutoken(id: String, url: String): String? { - val script = app.get("$mainUrl/futoken", referer = url).text - val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null - val a = mutableListOf(k) - for (i in id.indices) { - a.add((k[i % k.length].code + id[i].code).toString()) - } - return "$mainUrl/mediainfo/${a.joinToString(",")}?${url.substringAfter("?")}" + private fun encode(input: String, key: String): String { + val rc4Key = SecretKeySpec(key.toByteArray(Charsets.UTF_8), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.ENCRYPT_MODE, rc4Key) + val encryptedBytes = cipher.doFinal(input.toByteArray(Charsets.UTF_8)) + return Base64.UrlSafe.encode(encryptedBytes) } - private fun encodeId(id: String, keyList: List): String { - val cipher1 = Cipher.getInstance("RC4") - val cipher2 = Cipher.getInstance("RC4") - cipher1.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(keyList[0].toByteArray(), "RC4"), - cipher1.parameters - ) - cipher2.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(keyList[1].toByteArray(), "RC4"), - cipher2.parameters - ) - var input = id.toByteArray() - input = cipher1.doFinal(input) - input = cipher2.doFinal(input) - return base64Encode(input).replace("/", "_") + fun decode(input: String, key: String): String { + val decodedBytes = Base64.UrlSafe.decode(input) + val rc4Key = SecretKeySpec(key.toByteArray(Charsets.UTF_8), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.DECRYPT_MODE, rc4Key) + val decryptedBytes = cipher.doFinal(decodedBytes) + val decodedString = String(decryptedBytes, Charsets.UTF_8) + return URLDecoder.decode(decodedString, "UTF-8") } data class Tracks( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, - @JsonProperty("kind") val kind: String? = null, + @JsonProperty("file") val file: String? = null, + @JsonProperty("label") val label: String? = null, + @JsonProperty("kind") val kind: String? = null, ) data class Sources( - @JsonProperty("file") val file: String? = null, + @JsonProperty("file") val file: String? = null, ) data class Result( - @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), - @JsonProperty("tracks") val tracks: ArrayList? = arrayListOf(), - ) - - data class Response( - @JsonProperty("result") val result: Result? = null, + @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), + @JsonProperty("tracks") val tracks: ArrayList? = arrayListOf(), ) + data class Response(@JsonProperty("result") val result: String? = null) } From 63465ed7a9cfb2121eb91015671872f9a14deefd Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:24:06 +0200 Subject: [PATCH 534/570] fix autohide --- .../lagradost/cloudstream3/ui/player/FullScreenPlayer.kt | 9 +++++++++ .../lagradost/cloudstream3/ui/player/GeneratorPlayer.kt | 1 + 2 files changed, 10 insertions(+) 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 75a861c0..97075136 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 @@ -728,6 +728,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private var currentTapIndex = 0 protected fun autoHide() { currentTapIndex++ + delayHide() + } + + override fun playerStatusChanged() { + super.playerStatusChanged() + delayHide() + } + + private fun delayHide() { val index = currentTapIndex playerBinding?.playerHolder?.postDelayed({ if (!isCurrentTouchValid && isShowing && index == currentTapIndex && player.getIsPlaying()) { 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 d827d31e..f6c78b07 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 @@ -158,6 +158,7 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun playerStatusChanged() { + super.playerStatusChanged() if (player.getIsPlaying()) { viewModel.forceClearCache = false } From 073af50f5f09a3ca6b4bc21d0c84ff1bf2264e8f Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:28:36 +0200 Subject: [PATCH 535/570] fixed html plot in preview --- app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 59f499c5..e8cbc4d8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -107,6 +107,7 @@ import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST import com.lagradost.cloudstream3.ui.result.SyncViewModel import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setText +import com.lagradost.cloudstream3.ui.result.setTextHtml import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.SearchFragment import com.lagradost.cloudstream3.ui.search.SearchResultBuilder @@ -1404,7 +1405,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa resultviewPreviewMetaDuration.setText(d.durationText) resultviewPreviewMetaRating.setText(d.ratingText) - resultviewPreviewDescription.setText(d.plotText) + resultviewPreviewDescription.setTextHtml(d.plotText) resultviewPreviewPoster.setImage( d.posterImage ?: d.posterBackgroundImage ) From bb8144a52ecd4f6518eaca41bebd7cdf2ce31e1d Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 19 Jul 2024 20:35:29 +0300 Subject: [PATCH 536/570] feat(TV UI): Player's Top controls redesign (#1203) --- .../cloudstream3/ui/player/CS3IPlayer.kt | 4 + .../ui/player/FullScreenPlayer.kt | 72 +++++++-- .../cloudstream3/ui/player/GeneratorPlayer.kt | 19 ++- .../cloudstream3/ui/player/IPlayer.kt | 2 + .../res/drawable/ic_baseline_equalizer_24.xml | 9 ++ .../res/drawable/ic_baseline_replay_24.xml | 9 ++ .../res/drawable/ic_baseline_restart_24.xml | 9 ++ .../drawable/ic_baseline_skip_next_24_big.xml | 10 ++ .../ic_baseline_skip_next_rounded_24.xml | 9 ++ .../main/res/layout/fragment_player_tv.xml | 11 +- .../main/res/layout/player_custom_layout.xml | 119 +++++++++++--- .../res/layout/player_custom_layout_tv.xml | 148 ++++++++++++++---- app/src/main/res/layout/subtitle_offset.xml | 31 ++-- .../main/res/layout/trailer_custom_layout.xml | 123 ++++++++++++--- app/src/main/res/values/strings.xml | 1 + 15 files changed, 467 insertions(+), 109 deletions(-) create mode 100644 app/src/main/res/drawable/ic_baseline_equalizer_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_replay_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_restart_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml create mode 100644 app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 8e322f73..735e4095 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -912,7 +912,11 @@ class CS3IPlayer : IPlayer { } CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source) + CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source) + + CSPlayerEvent.Restart -> seekTo(0, source) + CSPlayerEvent.NextEpisode -> event( EpisodeSeekEvent( offset = 1, 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 97075136..a75b9899 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 @@ -14,7 +14,13 @@ import android.os.Bundle import android.provider.Settings import android.text.Editable import android.text.format.DateUtils -import android.view.* +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.Surface +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES import android.view.animation.AlphaAnimation import android.view.animation.Animation @@ -58,7 +64,11 @@ import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UserPreferenceDelegate import com.lagradost.cloudstream3.utils.Vector2 -import kotlin.math.* +import kotlin.math.abs +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min +import kotlin.math.round const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking const val MINIMUM_VERTICAL_SWIPE = 2.0f // in percentage @@ -77,7 +87,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected open var isFullScreenPlayer = true protected var playerBinding: PlayerCustomLayoutBinding? = null - private var durationMode : Boolean by UserPreferenceDelegate("duration_mode", false) + private var durationMode: Boolean by UserPreferenceDelegate("duration_mode", false) + // state of player UI protected var isShowing = false protected var isLocked = false @@ -243,7 +254,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { val playerSourceMove = if (isShowing) 0f else -50.toPx.toFloat() - playerBinding?.apply { playerOpenSource.let { ObjectAnimator.ofFloat(it, "translationY", playerSourceMove).apply { @@ -284,7 +294,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.getCurrentPreferredSubtitle() == null } - private fun restoreOrientationWithSensor(activity: Activity){ + private fun restoreOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation var orientation = 0 when (currentOrientation) { @@ -300,7 +310,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { activity.requestedOrientation = orientation } - private fun toggleOrientationWithSensor(activity: Activity){ + private fun toggleOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation var orientation = 0 when (currentOrientation) { @@ -345,12 +355,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun updateOrientation(ignoreDynamicOrientation: Boolean = false) { activity?.apply { - if(lockRotation) { - if(isLocked) { + if (lockRotation) { + if (isLocked) { lockOrientation(this) - } - else { - if(ignoreDynamicOrientation){ + } else { + if (ignoreDynamicOrientation) { // restore when lock is disabled restoreOrientationWithSensor(this) } else { @@ -958,7 +967,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } else -> { - player.handleEvent(CSPlayerEvent.PlayPauseToggle, PlayerEventSource.UI) + player.handleEvent( + CSPlayerEvent.PlayPauseToggle, + PlayerEventSource.UI + ) } } } else if (doubleTapEnabled && isFullScreenPlayer) { @@ -1234,6 +1246,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { // if nothing has loaded these buttons should not be visible playerBinding?.apply { playerSkipEpisode.isVisible = false + playerGoForward.isVisible = false playerTracksBtt.isVisible = false playerSkipOp.isVisible = false shadowOverlay.isVisible = false @@ -1307,6 +1320,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.handleEvent(CSPlayerEvent.SeekBack) } + PlayerEventType.Restart -> { + player.handleEvent(CSPlayerEvent.Restart) + } + PlayerEventType.ToggleMute -> { player.handleEvent(CSPlayerEvent.ToggleMute) } @@ -1428,6 +1445,25 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } playerBinding?.apply { + + if (isLayout(TV or EMULATOR)) { + mapOf( + playerGoBack to playerGoBackText, + playerRestart to playerRestartText, + playerGoForward to playerGoForwardText + ).forEach { (button, text) -> + button.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + text.isSelected = false + text.isVisible = false + return@setOnFocusChangeListener + } + text.isSelected = true + text.isVisible = true + } + } + } + playerPausePlay.setOnClickListener { autoHide() player.handleEvent(CSPlayerEvent.PlayPauseToggle) @@ -1471,6 +1507,16 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.handleEvent(CSPlayerEvent.NextEpisode) } + playerGoForward.setOnClickListener { + autoHide() + player.handleEvent(CSPlayerEvent.NextEpisode) + } + + playerRestart.setOnClickListener { + autoHide() + player.handleEvent(CSPlayerEvent.Restart) + } + playerLock.setOnClickListener { autoHide() toggleLock() @@ -1564,7 +1610,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun setRemainingTimeCounter(showRemaining: Boolean) { durationMode = showRemaining - playerBinding?.exoDuration?.isInvisible= showRemaining + playerBinding?.exoDuration?.isInvisible = showRemaining playerBinding?.timeLeft?.isVisible = showRemaining } 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 f6c78b07..1f7cc5bd 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 @@ -44,6 +44,7 @@ import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog import com.lagradost.cloudstream3.ui.result.* import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY @@ -1097,8 +1098,15 @@ class GeneratorPlayer : FullScreenPlayer() { } playerBinding?.playerSkipOp?.isVisible = isOpVisible - playerBinding?.playerSkipEpisode?.isVisible = - !isOpVisible && viewModel.hasNextEpisode() == true + + when { + isLayout(PHONE) -> + playerBinding?.playerSkipEpisode?.isVisible = + !isOpVisible && viewModel.hasNextEpisode() == true + + else -> + playerBinding?.playerGoForward?.isVisible = viewModel.hasNextEpisode() == true + } if (percentage >= PRELOAD_NEXT_EPISODE_PERCENTAGE) { viewModel.preLoadNextLinks() @@ -1254,7 +1262,7 @@ class GeneratorPlayer : FullScreenPlayer() { fun setPlayerDimen(widthHeight: Pair?) { val extra = if (widthHeight != null) { val (width, height) = widthHeight - "${width}x${height}" + "- ${width}x${height}" } else { "" } @@ -1265,7 +1273,7 @@ class GeneratorPlayer : FullScreenPlayer() { 0 -> "" 1 -> extra 2 -> source - 3 -> "$source - $extra" + 3 -> "$source $extra" else -> "" } playerBinding?.playerVideoTitleRez?.apply { @@ -1290,7 +1298,8 @@ class GeneratorPlayer : FullScreenPlayer() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // this is used instead of layout-television to follow the settings and some TV devices are not classified as TV for some reason - layout = if (isLayout(TV or EMULATOR)) R.layout.fragment_player_tv else R.layout.fragment_player + layout = + if (isLayout(TV or EMULATOR)) R.layout.fragment_player_tv else R.layout.fragment_player viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java] sync = ViewModelProvider(this)[SyncViewModel::class.java] diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index 4bd5c769..5f7161f7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -26,6 +26,7 @@ enum class PlayerEventType(val value: Int) { Resize(13), SearchSubtitlesOnline(14), SkipOp(15), + Restart(16), } enum class CSPlayerEvent(val value: Int) { @@ -39,6 +40,7 @@ enum class CSPlayerEvent(val value: Int) { PrevEpisode(6), PlayPauseToggle(7), ToggleMute(8), + Restart(9), } enum class CSPlayerLoading { diff --git a/app/src/main/res/drawable/ic_baseline_equalizer_24.xml b/app/src/main/res/drawable/ic_baseline_equalizer_24.xml new file mode 100644 index 00000000..cd20ad15 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_equalizer_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_replay_24.xml b/app/src/main/res/drawable/ic_baseline_replay_24.xml new file mode 100644 index 00000000..e247aa92 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_replay_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_restart_24.xml b/app/src/main/res/drawable/ic_baseline_restart_24.xml new file mode 100644 index 00000000..aed3a562 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_restart_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml b/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml new file mode 100644 index 00000000..a8c43bbd --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml b/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml new file mode 100644 index 00000000..452c4dd9 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_player_tv.xml b/app/src/main/res/layout/fragment_player_tv.xml index 07cbb3c3..3c0ac05e 100644 --- a/app/src/main/res/layout/fragment_player_tv.xml +++ b/app/src/main/res/layout/fragment_player_tv.xml @@ -68,7 +68,9 @@ android:layout_height="wrap_content" android:layout_margin="5dp" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toTopOf="parent" + android:visibility="gone" + tools:visibility="visible"> diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 83be8832..be97b978 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -172,27 +172,108 @@ android:id="@+id/player_go_back_holder" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="5dp" + android:layout_margin="20dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - + + + + + + + + + + + + + + + + + + + + + + + - @@ -626,7 +707,7 @@ android:nextFocusLeft="@id/player_sources_btt" android:nextFocusRight="@id/player_skip_op" android:text="@string/tracks" - app:icon="@drawable/ic_baseline_playlist_play_24" /> + app:icon="@drawable/ic_baseline_equalizer_24" /> + diff --git a/app/src/main/res/layout/player_custom_layout_tv.xml b/app/src/main/res/layout/player_custom_layout_tv.xml index d8406b35..98eb58ac 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -231,7 +231,7 @@ @@ -240,6 +240,7 @@ android:id="@+id/player_video_title" android:layout_width="match_parent" android:layout_height="wrap_content" + android:textAlignment="viewEnd" android:gravity="end" android:textColor="@color/white" android:textSize="16sp" @@ -250,6 +251,7 @@ android:id="@+id/player_video_title_rez" android:layout_width="match_parent" android:layout_height="wrap_content" + android:textAlignment="viewEnd" android:gravity="end" android:textColor="@color/white" android:textSize="16sp" @@ -285,28 +287,116 @@ android:id="@+id/player_go_back_holder" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="5dp" + android:layout_marginStart="17dp" + android:layout_marginTop="20dp" + android:layout_marginEnd="17dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - + + + + + + + + + + + + + + + + + + + + + + + - @@ -509,6 +599,7 @@ android:layout_height="wrap_content" android:layoutDirection="ltr" android:orientation="horizontal"> + + app:layout_constraintStart_toStartOf="parent" + app:tint="@color/player_button_tv" + tools:ignore="ContentDescription" /> + tools:text="-23:20" /> @@ -672,12 +763,13 @@ android:id="@+id/player_skip_episode" style="@style/VideoButtonTV" android:nextFocusLeft="@id/player_skip_op" - android:nextFocusRight="@id/player_resize_btt" android:nextFocusUp="@id/player_pause_play" android:nextFocusDown="@id/player_resize_btt" android:text="@string/next_episode" - app:icon="@drawable/ic_baseline_skip_next_24" /> + app:icon="@drawable/ic_baseline_skip_next_24" + android:visibility="gone" + tools:visibility="visible"/> + app:icon="@drawable/ic_baseline_equalizer_24" /> diff --git a/app/src/main/res/layout/subtitle_offset.xml b/app/src/main/res/layout/subtitle_offset.xml index d5e303b6..82c24e61 100644 --- a/app/src/main/res/layout/subtitle_offset.xml +++ b/app/src/main/res/layout/subtitle_offset.xml @@ -30,28 +30,27 @@ @@ -67,29 +66,29 @@ diff --git a/app/src/main/res/layout/trailer_custom_layout.xml b/app/src/main/res/layout/trailer_custom_layout.xml index 59104ca7..20b73630 100644 --- a/app/src/main/res/layout/trailer_custom_layout.xml +++ b/app/src/main/res/layout/trailer_custom_layout.xml @@ -137,33 +137,110 @@ - + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e3f788f..e68c22b9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -96,6 +96,7 @@ Next Random @string/play_episode Go back + Play from the Beginning @string/home_change_provider_img_des Change Provider Preview Background From 4c7379c766837ffc11c62d4fa2c4e7aaa8afd5fc Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Sat, 20 Jul 2024 19:14:11 +0200 Subject: [PATCH 537/570] Revert #979 Episode download cache --- .../main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 1d23e503..802c1a64 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -67,8 +67,6 @@ object BackupUtils { OPEN_SUBTITLES_USER_KEY, SUBDL_SUBTITLES_USER_KEY, - DOWNLOAD_EPISODE_CACHE, - "biometric_key", // can lock down users if backup is shared on a incompatible device "nginx_user", // Nginx user key "download_path_key" // No access rights after restore data from backup @@ -266,4 +264,4 @@ object BackupUtils { } editor.apply() } -} \ No newline at end of file +} From 0c418fdf9bd41f6a94e9a8063c48bc4acd44ffb1 Mon Sep 17 00:00:00 2001 From: RowdyRushya Date: Sat, 20 Jul 2024 15:06:04 -0700 Subject: [PATCH 538/570] Updated VidSrc encryption methods (#1205) --- .../extractors/VidSrcExtractor.kt | 301 +++++++++++++----- 1 file changed, 227 insertions(+), 74 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt index a27bf188..5da919e2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt @@ -3,98 +3,251 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.* -import kotlinx.coroutines.delay -import java.net.URI +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.loadExtractor +import java.util.Base64 class VidSrcExtractor2 : VidSrcExtractor() { - override val mainUrl = "https://vidsrc.me/embed" - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val newUrl = url.lowercase().replace(mainUrl, super.mainUrl) - super.getUrl(newUrl, referer, subtitleCallback, callback) - } + override val mainUrl = "https://vidsrc.me" } open class VidSrcExtractor : ExtractorApi() { override val name = "VidSrc" - private val absoluteUrl = "https://v2.vidsrc.me" - override val mainUrl = "$absoluteUrl/embed" + override val mainUrl = "https://vidsrc.net" + private val apiUrl = "https://vidsrc.stream" override val requiresReferer = false - companion object { - /** Infinite function to validate the vidSrc pass */ - suspend fun validatePass(url: String) { - val uri = URI(url) - val host = uri.host - - // Basically turn https://tm3p.vidsrc.stream/ -> https://vidsrc.stream/ - val referer = host.split(".").let { - val size = it.size - "https://" + it.subList(maxOf(0, size - 2), size).joinToString(".") + "/" - } - - while (true) { - app.get(url, referer = referer) - delay(60_000) - } - } - } - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit ) { val iframedoc = app.get(url).document - val serverslist = - iframedoc.select("div#sources.button_content div#content div#list div").map { - val datahash = it.attr("data-hash") - if (datahash.isNotBlank()) { - val links = try { - app.get( - "$absoluteUrl/srcrcp/$datahash", - referer = "https://rcp.vidsrc.me/" - ).url - } catch (e: Exception) { - "" - } - links - } else "" - } + val srcrcpList = + iframedoc.select("div.serversList > div.server").mapNotNull { + val datahash = it.attr("data-hash") ?: return@mapNotNull null + val rcpLink = "$apiUrl/rcp/$datahash" + val rcpRes = app.get(rcpLink, referer = apiUrl).text + val srcrcpLink = + Regex("src:\\s*'(.*)',").find(rcpRes)?.destructured?.component1() + ?: return@mapNotNull null + "https:$srcrcpLink" + } - serverslist.amap { server -> - val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/") - 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 - val passRegex = Regex("""['"](.*set_pass[^"']*)""") - val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace( - Regex("""^//"""), "https://" - ) + srcrcpList.amap { server -> + val res = app.get(server, referer = apiUrl) + if (res.url.contains("/prorcp")) { + val encodedElement = res.document.select("div#reporting_content+div") + val decodedUrl = + decodeUrl(encodedElement.attr("id"), encodedElement.text()) ?: return@amap callback.invoke( - ExtractorLink( - this.name, - this.name, - srcm3u8, - "https://vidsrc.stream/", - Qualities.Unknown.value, - extractorData = pass, - isM3u8 = true - ) + ExtractorLink( + this.name, + this.name, + decodedUrl, + apiUrl, + Qualities.Unknown.value, + isM3u8 = true + ) ) } else { - loadExtractor(linkfixed, url, subtitleCallback, callback) + loadExtractor(res.url, url, subtitleCallback, callback) } } } -} \ No newline at end of file + private fun decodeUrl(encType: String, url: String): String? { + return when (encType) { + "NdonQLf1Tzyx7bMG" -> bMGyx71TzQLfdonN(url) + "sXnL9MQIry" -> Iry9MQXnLs(url) + "IhWrImMIGL" -> IGLImMhWrI(url) + "xTyBxQyGTA" -> GTAxQyTyBx(url) + "ux8qjPHC66" -> C66jPHx8qu(url) + "eSfH1IRMyL" -> MyL1IRSfHe(url) + "KJHidj7det" -> detdj7JHiK(url) + "o2VSUnjnZl" -> nZlUnj2VSo(url) + "Oi3v1dAlaM" -> laM1dAi3vO(url) + "TsA2KGDGux" -> GuxKGDsA2T(url) + "JoAHUMCLXV" -> LXVUMCoAHJ(url) + else -> null + } + } + + private fun bMGyx71TzQLfdonN(a: String): String { + val b = 3 + val c = mutableListOf() + var d = 0 + while (d < a.length) { + c.add(a.substring(d, minOf(d + b, a.length))) + d += b + } + val e = c.reversed().joinToString("") + return e + } + + private fun Iry9MQXnLs(a: String): String { + val b = "pWB9V)[*4I`nJpp?ozyB~dbr9yt!_n4u" + val d = a.chunked(2).map { it.toInt(16).toChar() }.joinToString("") + var c = "" + for (e in d.indices) { + c += (d[e].code xor b[e % b.length].code).toChar() + } + var e = "" + for (ch in c) { + e += (ch.code - 3).toChar() + } + return String(Base64.getDecoder().decode(e)) + } + + private fun IGLImMhWrI(a: String): String { + val b = a.reversed() + val c = + b + .map { + when (it) { + in 'a'..'m', in 'A'..'M' -> it + 13 + in 'n'..'z', in 'N'..'Z' -> it - 13 + else -> it + } + } + .joinToString("") + val d = c.reversed() + return String(Base64.getDecoder().decode(d)) + } + + private fun GTAxQyTyBx(a: String): String { + val b = a.reversed() + val c = b.filterIndexed { index, _ -> index % 2 == 0 } + return String(Base64.getDecoder().decode(c)) + } + + private fun C66jPHx8qu(a: String): String { + val b = a.reversed() + val c = "X9a(O;FMV2-7VO5x;Ao:dN1NoFs?j," + val d = b.chunked(2).map { it.toInt(16).toChar() }.joinToString("") + var e = "" + for (i in d.indices) { + e += (d[i].code xor c[i % c.length].code).toChar() + } + return e + } + + private fun MyL1IRSfHe(a: String): String { + val b = a.reversed() + val c = b.map { (it.code - 1).toChar() }.joinToString("") + val d = c.chunked(2).map { it.toInt(16).toChar() }.joinToString("") + return d + } + + private fun detdj7JHiK(a: String): String { + val b = a.substring(10, a.length - 16) + val c = "3SAY~#%Y(V%>5d/Yg\"\$G[Lh1rK4a;7ok" + val d = String(Base64.getDecoder().decode(b)) + val e = c.repeat((d.length + c.length - 1) / c.length).substring(0, d.length) + var f = "" + for (i in d.indices) { + f += (d[i].code xor e[i].code).toChar() + } + return f + } + + private fun nZlUnj2VSo(a: String): String { + val b = + mapOf( + 'x' to 'a', + 'y' to 'b', + 'z' to 'c', + 'a' to 'd', + 'b' to 'e', + 'c' to 'f', + 'd' to 'g', + 'e' to 'h', + 'f' to 'i', + 'g' to 'j', + 'h' to 'k', + 'i' to 'l', + 'j' to 'm', + 'k' to 'n', + 'l' to 'o', + 'm' to 'p', + 'n' to 'q', + 'o' to 'r', + 'p' to 's', + 'q' to 't', + 'r' to 'u', + 's' to 'v', + 't' to 'w', + 'u' to 'x', + 'v' to 'y', + 'w' to 'z', + 'X' to 'A', + 'Y' to 'B', + 'Z' to 'C', + 'A' to 'D', + 'B' to 'E', + 'C' to 'F', + 'D' to 'G', + 'E' to 'H', + 'F' to 'I', + 'G' to 'J', + 'H' to 'K', + 'I' to 'L', + 'J' to 'M', + 'K' to 'N', + 'L' to 'O', + 'M' to 'P', + 'N' to 'Q', + 'O' to 'R', + 'P' to 'S', + 'Q' to 'T', + 'R' to 'U', + 'S' to 'V', + 'T' to 'W', + 'U' to 'X', + 'V' to 'Y', + 'W' to 'Z' + ) + return a.map { b[it] ?: it }.joinToString("") + } + + private fun laM1dAi3vO(a: String): String { + val b = a.reversed() + val c = b.replace("-", "+").replace("_", "/") + val d = String(Base64.getDecoder().decode(c)) + var e = "" + val f = 5 + for (ch in d) { + e += (ch.code - f).toChar() + } + return e + } + + private fun GuxKGDsA2T(a: String): String { + val b = a.reversed() + val c = b.replace("-", "+").replace("_", "/") + val d = String(Base64.getDecoder().decode(c)) + var e = "" + val f = 7 + for (ch in d) { + e += (ch.code - f).toChar() + } + return e + } + + private fun LXVUMCoAHJ(a: String): String { + val b = a.reversed() + val c = b.replace("-", "+").replace("_", "/") + val d = String(Base64.getDecoder().decode(c)) + var e = "" + val f = 3 + for (ch in d) { + e += (ch.code - f).toChar() + } + return e + } +} From c8a863e332d0d952568385f438b2a035cca5b816 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 24 Jul 2024 22:38:16 +0200 Subject: [PATCH 539/570] Fixed ExampleInstrumentedTest --- .../java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index faacdf50..c7f02baf 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -154,7 +154,7 @@ class ExampleInstrumentedTest { fun providerCorrectHomepage() { runBlocking { getAllProviders().toList().amap { api -> - TestingUtils.testHomepage(api, ::println) + TestingUtils.testHomepage(api, TestingUtils.Logger()) } } println("Done providerCorrectHomepage") @@ -166,7 +166,6 @@ class ExampleInstrumentedTest { TestingUtils.getDeferredProviderTests( this, getAllProviders(), - ::println ) { _, _ -> } } } From dfd127265a066dfec18e797b3b2ddc7bf2ae51ef Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 25 Jul 2024 21:23:31 +0300 Subject: [PATCH 540/570] Trailers Fix (#1213) --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6e439d53..1ad35d89 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -200,7 +200,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:592f159") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:2d36945") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding From e3ff1cf4554bc1bea1333a456c62becb987fd01b Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 25 Jul 2024 21:23:49 +0300 Subject: [PATCH 541/570] feat(UI): Show Episode Runtime (#1207) --- .../metaproviders/TraktProvider.kt | 1 + .../cloudstream3/ui/result/EpisodeAdapter.kt | 15 +++++++-- .../cloudstream3/ui/result/ResultFragment.kt | 3 ++ .../ui/result/ResultViewModel2.kt | 6 ++-- .../main/res/layout/result_episode_large.xml | 30 +++++++++++++---- .../com/lagradost/cloudstream3/MainAPI.kt | 32 +++++++++++++++++-- 6 files changed, 75 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 7c375e0a..a1b9ff34 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -236,6 +236,7 @@ open class TraktProvider : MainAPI() { posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()), rating = episode.rating?.times(10)?.roundToInt(), description = episode.overview, + runTime = episode.runtime ).apply { this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") if (nextAir == null && this.date != null && this.date!! > unixTimeMS && this.season != 0) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index ed5e51f1..06be6bd5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -27,7 +27,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper import java.text.DateFormat import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 const val ACTION_PLAY_EPISODE_IN_VLC_PLAYER = 2 @@ -58,6 +59,7 @@ const val ACTION_MARK_AS_WATCHED = 18 const val ACTION_FCAST = 19 const val TV_EP_SIZE = 400 + data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) class EpisodeAdapter( @@ -274,7 +276,10 @@ class EpisodeAdapter( episodeDate.setText( txt( R.string.episode_upcoming_format, - secondsToReadable(card.airDate.minus(unixTimeMS).div(1000).toInt(), "") + secondsToReadable( + card.airDate.minus(unixTimeMS).div(1000).toInt(), + "" + ) ) ) } else { @@ -292,6 +297,12 @@ class EpisodeAdapter( episodeDate.isVisible = false } + episodeRuntime.setText( + txt( + card.runTime?.times(60L)?.toInt()?.let { secondsToReadable(it, "") } + ) + ) + if (isLayout(EMULATOR or PHONE)) { episodePoster.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) 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 c687eaa0..3eab0c71 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 @@ -51,6 +51,7 @@ data class ResultEpisode( /** Sum of all previous season episode counts + episode */ val totalEpisodeIndex: Int? = null, val airDate: Long? = null, + val runTime: Int? = null, ) fun ResultEpisode.getRealPosition(): Long { @@ -87,6 +88,7 @@ fun buildResultEpisode( parentId: Int, totalEpisodeIndex: Int? = null, airDate: Long? = null, + runTime: Int? = null, ): ResultEpisode { val posDur = getViewPos(id) val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None @@ -111,6 +113,7 @@ fun buildResultEpisode( videoWatchState, totalEpisodeIndex, airDate, + runTime, ) } 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 8e8dfe30..5086426f 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 @@ -2371,7 +2371,8 @@ class ResultViewModel2 : ViewModel() { loadResponse.type, mainId, totalIndex, - airDate = i.date + airDate = i.date, + runTime = i.runTime, ) val season = eps.seasonIndex ?: 0 @@ -2426,7 +2427,8 @@ class ResultViewModel2 : ViewModel() { loadResponse.type, mainId, totalIndex, - airDate = episode.date + airDate = episode.date, + runTime = episode.runTime, ) val season = ep.seasonIndex ?: 0 diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index e5a6881a..935beac1 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -44,7 +44,7 @@ android:nextFocusRight="@id/download_button" android:scaleType="centerCrop" tools:src="@drawable/example_poster" - tools:visibility="invisible"/> + tools:visibility="invisible" /> + tools:visibility="invisible" /> - + android:layout_gravity="start" + android:orientation="horizontal"> + + + + + + + Date: Thu, 25 Jul 2024 20:25:17 +0200 Subject: [PATCH 542/570] Add the option to hide video controls (#1210) --- .../ui/player/FullScreenPlayer.kt | 25 +++++++++++++++++++ .../ui/settings/SettingsPlayer.kt | 6 ++--- app/src/main/res/values-af/strings.xml | 1 + app/src/main/res/values-ajp/strings.xml | 1 + app/src/main/res/values-am/strings.xml | 1 + app/src/main/res/values-ar/strings.xml | 1 + app/src/main/res/values-ars/strings.xml | 1 + app/src/main/res/values-as/strings.xml | 1 + app/src/main/res/values-bg/strings.xml | 1 + app/src/main/res/values-bn/strings.xml | 1 + app/src/main/res/values-bp/strings.xml | 1 + app/src/main/res/values-cs/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-el/strings.xml | 1 + app/src/main/res/values-eo/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fa/strings.xml | 1 + app/src/main/res/values-fil/strings.xml | 4 ++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-gl/strings.xml | 1 + app/src/main/res/values-hi/strings.xml | 1 + app/src/main/res/values-hr/strings.xml | 1 + app/src/main/res/values-hu/strings.xml | 1 + app/src/main/res/values-in/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-iw/strings.xml | 1 + app/src/main/res/values-ja/strings.xml | 1 + app/src/main/res/values-kn/strings.xml | 1 + app/src/main/res/values-ko/strings.xml | 1 + app/src/main/res/values-lt/strings.xml | 1 + app/src/main/res/values-lv/strings.xml | 1 + app/src/main/res/values-mk/strings.xml | 1 + app/src/main/res/values-ml/strings.xml | 1 + app/src/main/res/values-ms/strings.xml | 1 + app/src/main/res/values-mt/strings.xml | 1 + app/src/main/res/values-my/strings.xml | 1 + app/src/main/res/values-ne/strings.xml | 1 + app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values-nn/strings.xml | 1 + app/src/main/res/values-no/strings.xml | 1 + app/src/main/res/values-or/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-pt/strings.xml | 1 + app/src/main/res/values-qt/strings.xml | 1 + app/src/main/res/values-ro/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sk/strings.xml | 1 + app/src/main/res/values-so/strings.xml | 1 + app/src/main/res/values-sv/strings.xml | 1 + app/src/main/res/values-ta/strings.xml | 1 + app/src/main/res/values-ti/strings.xml | 1 + app/src/main/res/values-tl/strings.xml | 1 + app/src/main/res/values-tr/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + app/src/main/res/values-ur/strings.xml | 1 + app/src/main/res/values-vi/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values-zh/strings.xml | 1 + app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/settings_player.xml | 6 +++++ 60 files changed, 93 insertions(+), 5 deletions(-) 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 a75b9899..ef7d6bc1 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 @@ -25,15 +25,18 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHO import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.view.animation.AnimationUtils +import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.core.graphics.blue import androidx.core.graphics.green import androidx.core.graphics.red +import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.preference.PreferenceManager +import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.screenHeight @@ -120,6 +123,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected var doubleTapPauseEnabled = true protected var playerRotateEnabled = false protected var autoPlayerRotateEnabled = false + private var hideControlsNames = false protected var subtitleDelay set(value) = try { @@ -1419,6 +1423,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { false ) + hideControlsNames = settingsManager.getBoolean(ctx.getString(R.string.hide_player_control_names_key), false) + val profiles = QualityDataHelper.getProfiles() val type = if (ctx.isUsingMobileData()) QualityDataHelper.QualityProfileType.Data @@ -1439,6 +1445,9 @@ open class FullScreenPlayer : AbstractPlayerFragment() { playerSpeedBtt.isVisible = playBackSpeedEnabled playerResizeBtt.isVisible = playerResizeEnabled playerRotateBtt.isVisible = playerRotateEnabled + if (hideControlsNames) { + hideControlsNames() + } } } catch (e: Exception) { logError(e) @@ -1591,6 +1600,22 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } + private fun PlayerCustomLayoutBinding.hideControlsNames() { + fun iterate(layout: LinearLayout) { + layout.children.forEach { + if (it is MaterialButton) { + it.textSize = 0f + it.iconPadding = 0 + it.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START + it.setPadding(0,0,0,0) + } else if (it is LinearLayout) { + iterate(it) + } + } + } + iterate(playerLockHolder.parent as LinearLayout) + } + override fun playerDimensionsLoaded(width: Int, height: Int) { isVerticalOrientation = height > width updateOrientation() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 20279cd1..7560d75f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -87,10 +87,6 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - /*(getPref(R.string.double_tap_seek_time_key) as? SeekBarPreference?)?.let { - - }*/ - getPref(R.string.prefer_limit_title_rez_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.limit_title_rez_pref_names) val prefValues = resources.getIntArray(R.array.limit_title_rez_pref_values) @@ -109,6 +105,8 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + getPref(R.string.hide_player_control_names_key)?.hideOn(TV) + getPref(R.string.quality_pref_key)?.setOnPreferenceClickListener { val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() prefValues.remove(Qualities.Unknown.value) diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 45e9a1d4..4adafee4 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -106,4 +106,5 @@ Voer lettertipes in deur dit in %s te plaas Rolverdeling: %s Nuwe episode notifikasie + hide_player_control_names_key diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index c78b6924..718b5235 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -640,4 +640,5 @@ تجاهل متاح الريپوزيتوري فتاح %s ع تلفونك أو كمپيوترك، وحط الكود اللي فوق + hide_player_control_names_key diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml index 7fd3274b..26fb84dd 100644 --- a/app/src/main/res/values-am/strings.xml +++ b/app/src/main/res/values-am/strings.xml @@ -108,4 +108,5 @@ ተጨማሪ መረጃ ዓይነቶችን በመጠቀም ይፈልጉ ቅርጸ-ቁምፊዎችን በ%s ውስጥ በማስቀመጥ ያጫኑ + hide_player_control_names_key diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index c2ed35cb..e85fee04 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -666,4 +666,5 @@ قم بزيارة %s على هاتفك الذكي أو جهاز الكمبيوتر وأدخل الرمز أعلاه لا يمكن الحصول على رمز PIN للجهاز، حاول المصادقة المحلية تنتهي صلاحية الرمز خلال %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index f3811d3d..f028ef5d 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -352,4 +352,5 @@ وثائقي موقع عنوان مشغل الفيديو بحد أقصى لعدد الأحرف + hide_player_control_names_key diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml index 7fb0e7bd..dd1b2eed 100644 --- a/app/src/main/res/values-as/strings.xml +++ b/app/src/main/res/values-as/strings.xml @@ -621,4 +621,5 @@ ছাবটাইটেল বাছনি কৰক পৰ্ব খেলাওক প্ৰয়োগ কৰক + hide_player_control_names_key diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 66e29882..89801322 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -601,4 +601,5 @@ Покажи предложения Добавя опция за промяна на скоростта в плеъра Този тест е направен за програмисти и не проверява работата на никакви добавки. + hide_player_control_names_key diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 3500e85a..1a02eebc 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -359,4 +359,5 @@ অ্যাকাউন্ট প্রস্থান %1$d%2$s + hide_player_control_names_key diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 6dc38cd8..51138312 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -656,4 +656,5 @@ Não é possível obter o código PIN do dispositivo, tente a autenticação local O código PIN expirou! O código expira em %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8e40b12b..2f7dcfed 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -658,4 +658,5 @@ Účty Lokální ověření PIN kód vypršel! + hide_player_control_names_key diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ee378ff6..12a68dbc 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -615,4 +615,5 @@ Zurücksetzen Akkuverbrauch der App ist bereits auf unbeschränkt eingestellt CloudStreams App-Info kann nicht geöffnet werden. + hide_player_control_names_key diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e7fa1f6a..269626cb 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -625,4 +625,5 @@ Τα δεδομένα σας στο CloudStream έχουν κάνει back up. Αν και η πιθανότητα είναι πολύ χαμηλή, όλες οι συσκευές συμπεριφέρονται διαφορετικά. Στη σπάνια περίπτωση, που απαγορευτεί η πρόσβασή σας από την εφαρμογή, διαγράψτε τα δεδομένα εφαρμογής και επαναφέρετέ τα από ένα ήδη υπάρχον backup. Συγνώμη για οποιαδήποτε ταλαιπωρία. Λογαριασμοί Ασφάλεια + hide_player_control_names_key diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 275a4bfb..9d3d07bc 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -127,4 +127,5 @@ Elŝutite Elŝutante Elŝuto Malsukcesite + hide_player_control_names_key diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 82f29381..bd281b55 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -634,4 +634,5 @@ ¡El código PIN ya ha caducado! El código caduca en %1$d mín y %2$d s No puedo obtener el código PIN del dispositivo; intente con la autenticación local + hide_player_control_names_key diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index db432a61..86dee8ef 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -191,4 +191,5 @@ پیش‌فرض کارتون تورنت + hide_player_control_names_key diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index 42eba3cc..2189dd75 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -1,2 +1,4 @@ - + + hide_player_control_names_key + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index fa1e1b61..78f3f2a5 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -620,4 +620,5 @@ Verrouillage biométrique Sélectionnez un appareil de diffusion Saison %1$d Episode %2$d sera publié dans + hide_player_control_names_key diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index ae3105cf..d04792f8 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -164,4 +164,5 @@ Selecciona o modo para filtrar a descarga dos complementos Instala automáticamente todos os complementos aínda non instalados dos repositorios engadidos. Mostrar actualizacións da aplicación + hide_player_control_names_key diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index e08a3b8b..bd50953c 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -209,4 +209,5 @@ रूपरेखा रंग उपशीर्षक ऊंचाई अक्षर शैली + hide_player_control_names_key diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 54448e58..90dbee79 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -651,4 +651,5 @@ CloudStream Wiki Računi Sigurnost + hide_player_control_names_key diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index ebaff041..72213b02 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -592,4 +592,5 @@ A PIN 4 karakter hosszú kell legyen Auto elforgatás Az automatikus videó orientáció alapján való képernyő elforgatás bekapcsolása + hide_player_control_names_key diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 951ba417..0edae603 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -647,4 +647,5 @@ CloudStream Wiki Keamanan Akun + hide_player_control_names_key diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index ff7ea6bd..8671a73a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -654,4 +654,5 @@ Impossibile ottenere il codice PIN del dispositivo, prova l\'autenticazione locale Il codice PIN è scaduto! Il codice scadrà tra %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 22626f50..1f34f0e1 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -550,4 +550,5 @@ \nיגרמו לעדיפות הסרטון להיות 10. \n \nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! + hide_player_control_names_key diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index acb2cfc3..fb2ca02d 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -242,4 +242,5 @@ 現在のエピソードが終了したら次のエピソードを開始する 長押しするとデフォルトにリセットされます ダウンロードを再開 + hide_player_control_names_key diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index f3fb665d..75f62bcc 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -130,4 +130,5 @@ Brightness ಅಥವಾ volume ಬದಲಾಯಿಸಲು ಎಡ ಅಥವಾ ಬಲಭಾಗದಲ್ಲಿ ಮೇಲಕ್ಕೆ ಅಥವಾ ಕೆಳಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ ಈಗಿನ ಎಪಿಸೋಡ್ ಮುಗಿದಾಗ ಮುಂದಿನ ಎಪಿಸೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸ್ವೈಪ್ ಮಾಡಿ + hide_player_control_names_key diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index bda82057..ec570e69 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -634,4 +634,5 @@ %s의 PIN 입력 즐겨찾기에서 제거 캐스트미러 + hide_player_control_names_key diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index f61bcfc0..0cb3addf 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -260,4 +260,5 @@ Ar tikrai norite išeiti\? Pašalinti iš žiūrimų Garso takelis + hide_player_control_names_key diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 566c721d..96272e71 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -527,4 +527,5 @@ Abonēto šovu atjaunināšana Abonēts Abonēts %s + hide_player_control_names_key diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 5e4d5c06..d4023ec4 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -624,4 +624,5 @@ Грешка при пристапот до таблата со исечоци, обидете се повторно. Грешка при копирање, копирајте го logcat и контактирајте со поддршката за апликацијата. Аудио книга + hide_player_control_names_key diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index d97e666c..213d4a00 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -280,4 +280,5 @@ എഡ്ജ് തരം ഔട്ട്ലൈൻ നിറം പശ്ചാത്തല നിറം + hide_player_control_names_key diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index dca98e53..aae74f4e 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -57,4 +57,5 @@ Tutup Ep cuba + hide_player_control_names_key diff --git a/app/src/main/res/values-mt/strings.xml b/app/src/main/res/values-mt/strings.xml index b2c0356a..37da0580 100644 --- a/app/src/main/res/values-mt/strings.xml +++ b/app/src/main/res/values-mt/strings.xml @@ -123,4 +123,5 @@ Bookmarks Neħħi Falla t-tniżżil + hide_player_control_names_key diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 0ebe3c6b..e7007d12 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -550,4 +550,5 @@ သင်နဂိုတည်းကသတ်မှတ်ပြီး လိုက်ဘရီရွေးချယ်ရန် ဖြင့်ဖွင့်မည် + hide_player_control_names_key diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 49cb6cfa..bc0199a1 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -128,4 +128,5 @@ प्लेयरको उपशीर्षकको सेटिङ रिपोजिटरी को नाम र यूआरएल कपी गरियो! + hide_player_control_names_key diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index b685489b..6029f78b 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -608,4 +608,5 @@ Link opnieuw geladen Autoroteer Roteer + hide_player_control_names_key diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 95c527f9..930841db 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -195,4 +195,5 @@ Bilde i bilde Fortsett å sjå Prøv tilkopling på nytt… + hide_player_control_names_key diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 7b013653..115cd2d3 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -538,4 +538,5 @@ Bruk Hjelp Profilbakgrunn + hide_player_control_names_key diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index bdc55780..07fc8a1d 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -159,4 +159,5 @@ କୌଣସି ତଥ୍ୟ ନାହିଁ %1$s ଅ %2$d ଆଦ୍ୟ ବାଦ୍ ଦିଅ + hide_player_control_names_key diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 8e940c61..209c9d8e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -635,4 +635,5 @@ Odrzuć Otwórz repozytorium Odwiedź %s na swoim smartfonie lub komputerze i wprowadź powyższy kod + hide_player_control_names_key diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index ce20a8af..59406383 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -621,4 +621,5 @@ Fcast Escolha o dispositivo Transmitir + hide_player_control_names_key diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index 5de97c7d..258552e2 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -247,4 +247,5 @@ oooooh uuaagh @string/home_play oouuhhh ahhooo-ahah + hide_player_control_names_key diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 344eae21..609190cf 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -641,4 +641,5 @@ Selectați divece-ul pe care doriți să faceți cast Cast mirror Fcast + hide_player_control_names_key diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5a9b843e..7f19ac8c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -622,4 +622,5 @@ Выйдет %s Fcast Выберите девайс для трансляции + hide_player_control_names_key diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index a53e1f53..947e2b6d 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -377,4 +377,5 @@ Pridať repozitár Názov repozitára Zobraziť komunitné repozitáre + hide_player_control_names_key diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index c750ea7a..5dc0bc23 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -485,4 +485,5 @@ Bilowga Bilow isku qasan Qoraalka dhamaadka + hide_player_control_names_key diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 04230ab8..dd2dffb9 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -626,4 +626,5 @@ CloudStream Wiki Konton Säkerhet + hide_player_control_names_key diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 9378e400..44729e09 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -615,4 +615,5 @@ %கள் பிடித்தவைகளிலிருந்து அகற்றப்பட்டன உங்கள் கிளவுட்ச்ட்ரீம் தரவு இப்போது காப்புப் பிரதி எடுக்கப்பட்டுள்ளது. இதன் சாத்தியம் மிகக் குறைவு என்றாலும், எல்லா சாதனங்களும் வித்தியாசமாக நடந்து கொள்ளலாம். அரிய விசயத்தில், பயன்பாட்டை அணுகுவதிலிருந்து நீங்கள் பூட்டப்படுகிறீர்கள், பயன்பாட்டு தரவை முழுவதுமாக அழித்து, காப்புப்பிரதியிலிருந்து மீட்டெடுக்கவும். இதிலிருந்து எழும் ஏதேனும் சிரமத்திற்கு நாங்கள் மிகவும் வருந்துகிறோம். ஊடகம் + hide_player_control_names_key diff --git a/app/src/main/res/values-ti/strings.xml b/app/src/main/res/values-ti/strings.xml index 46235bbd..6c154c8d 100644 --- a/app/src/main/res/values-ti/strings.xml +++ b/app/src/main/res/values-ti/strings.xml @@ -3,4 +3,5 @@ %1$s ክፋል %2$d ክፋል %d በ ላይ ይወጣል ተዋሳእቲ፡ %s + hide_player_control_names_key diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index b4308eb7..dd964877 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -265,4 +265,5 @@ Mga Subtitle ng Chromecast Mga setting ng mga subtitle ng Chromecast Maglaro ng Trailer + hide_player_control_names_key diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 3273a901..a55750e9 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -681,4 +681,5 @@ Cihaz PIN kodu alınamıyor, yerel kimlik doğrulamayı deneyin PIN kodunun süresi doldu! Kodun süresi %1$dm %2$ds içinde doluyor + hide_player_control_names_key diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f5770e86..fd24274c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -634,4 +634,5 @@ Термін дії коду закінчується через %1$dхв %2$dс Автентифікація по місцю Відхилити + hide_player_control_names_key diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 04cfd381..c87be59c 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -617,4 +617,5 @@ دیگر ایکسٹینشنز میں تلاش کریں سفارشات دکھائیں آپ کے CloudStream ڈیٹا کا اب بیک اپ لیا گیا ہے۔ اگرچہ اس کا امکان بہت کم ہے، لیکن مختلف ڈیوائس مختلف طریقے سے کام کر سکتے ہیں۔ اگر آپ ایپ تک رسائی حاصل کرنے سے قاصر ہیں تو، ایپ کا ڈیٹا مکمل طور پر صاف کریں اور بیک اپ سے بحال کریں۔ اس سے ہونے والی کسی بھی تکلیف کے لیے ہم بہت معذرت خواہ ہیں۔ + hide_player_control_names_key diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 92e088bf..44868647 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -643,4 +643,5 @@ Truy cập %s trên điện thoại hoặc máy tính và nhập mã bên trên Mã PIN đã hết hạn! Mã sẽ hết hạn trong %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index c50f284c..69eb8741 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -671,4 +671,5 @@ 為了確保下載與通知已訂閱的電視節目的不間斷,CloudStream 需要取得在背景執行的權限。若點選「確定」,將移至「應用程式資訊」,請找到「應用程式電池使用」並將電池用量設置為「無限制」。請注意,取得此權限並不表示 CS3 會明顯增加電池用量,而是只在必要時在背景執行,例如取得通知或使用官方擴充功能下載影片時。若選擇「取消」,您可以稍後在「一般設定」中調整此設定。 CloudStream Wiki 此裝置不支援生物特徵認證 + hide_player_control_names_key diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 97ba24ea..f2db04e2 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -673,4 +673,5 @@ 选择投射设备 %1$d季%2$d集将在 投射镜像 + hide_player_control_names_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e68c22b9..21067fff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -797,4 +797,6 @@ Can\'t get the device PIN code, try local authentication PIN code is now expired ! Code expires in %1$dm %2$ds + hide_player_control_names_key + Hide names of the player\'s controls \ No newline at end of file diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index 5d5b11d0..0039af3a 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -37,6 +37,12 @@ android:icon="@drawable/ic_baseline_text_format_24" android:key="@string/prefer_limit_title_rez_key" android:title="@string/limit_title_rez" /> + Date: Thu, 25 Jul 2024 20:26:21 +0200 Subject: [PATCH 543/570] Bump 4.4.0 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1ad35d89..2040cf39 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -60,8 +60,8 @@ android { minSdk = 21 targetSdk = 33 /* Android 14 is Fu*ked ^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/ - versionCode = 63 - versionName = "4.3.2" + versionCode = 64 + versionName = "4.4.0" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") From a28ee413680da64d059bdc90510f67b816e62568 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 28 Jul 2024 15:59:37 -0600 Subject: [PATCH 544/570] Fix for navigation UI bug (#1220) --- .../lagradost/cloudstream3/MainActivity.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index e8cbc4d8..bc2cb88e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -572,6 +572,35 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa binding?.apply { navRailView.isVisible = isNavVisible && landscape navView.isVisible = isNavVisible && !landscape + + /** + * We need to make sure if we return to a sub-fragment, + * the correct navigation item is selected so that it does not + * highlight the wrong one in UI. + */ + when (destination.id) { + in listOf(R.id.navigation_downloads, R.id.navigation_download_child) -> { + navRailView.menu.findItem(R.id.navigation_downloads).isChecked = true + navView.menu.findItem(R.id.navigation_downloads).isChecked = true + } + in listOf( + R.id.navigation_settings, + R.id.navigation_subtitles, + R.id.navigation_chrome_subtitles, + R.id.navigation_settings_player, + R.id.navigation_settings_updates, + R.id.navigation_settings_ui, + R.id.navigation_settings_account, + R.id.navigation_settings_providers, + R.id.navigation_settings_general, + R.id.navigation_settings_extensions, + R.id.navigation_settings_plugins, + R.id.navigation_test_providers + ) -> { + navRailView.menu.findItem(R.id.navigation_settings).isChecked = true + navView.menu.findItem(R.id.navigation_settings).isChecked = true + } + } } } From 0aa48f335a818e0ebf0e1cf045d302a782e79857 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:26:22 -0600 Subject: [PATCH 545/570] Fix subscription icon displaying for movie types in result previews (#1222) --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5086426f..ce0fbdc5 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 @@ -2163,7 +2163,7 @@ class ResultViewModel2 : ViewModel() { // lets say that we have subscribed, then we must be able to unsubscribe no matter what else if (data != null) { _subscribeStatus.postValue(true) - } + } else _subscribeStatus.postValue(null) } private fun postFavorites(loadResponse: LoadResponse) { @@ -2861,4 +2861,4 @@ class ResultViewModel2 : ViewModel() { } } } -} +} \ No newline at end of file From 04dda008c40bae83ca076c24f2c8f75f6fcdb870 Mon Sep 17 00:00:00 2001 From: epireyn <48213068+epireyn@users.noreply.github.com> Date: Mon, 29 Jul 2024 00:39:04 +0200 Subject: [PATCH 546/570] Clean up and mark questionable code issues (#1209) --- .../lagradost/cloudstream3/AcraApplication.kt | 12 +- .../lagradost/cloudstream3/CommonActivity.kt | 16 +- .../cloudstream3/DownloaderTestImpl.kt | 6 +- .../lagradost/cloudstream3/MainActivity.kt | 49 +++---- .../cloudstream3/NativeCrashHandler.kt | 53 ------- .../metaproviders/SyncRedirector.kt | 6 +- .../metaproviders/TraktProvider.kt | 2 +- .../cloudstream3/network/CloudflareKiller.kt | 6 +- .../cloudstream3/plugins/CloudstreamPlugin.kt | 3 +- .../lagradost/cloudstream3/plugins/Plugin.kt | 10 +- .../cloudstream3/plugins/PluginManager.kt | 34 +++-- .../cloudstream3/plugins/RepositoryManager.kt | 2 +- .../cloudstream3/plugins/VotingApi.kt | 9 +- .../subtitles/AbstractSubProvider.kt | 2 +- .../subtitles/AbstractSubtitleEntities.kt | 1 - .../syncproviders/AccountManager.kt | 12 +- .../syncproviders/providers/Addic7ed.kt | 14 +- .../syncproviders/providers/AniListApi.kt | 40 ++--- .../syncproviders/providers/LocalList.kt | 2 - .../syncproviders/providers/MALApi.kt | 128 ++++++++-------- .../providers/OpenSubtitlesApi.kt | 29 ++-- .../syncproviders/providers/SimklApi.kt | 137 +++++++++--------- .../syncproviders/providers/SubSource.kt | 1 + .../cloudstream3/ui/APIRepository.kt | 6 +- .../lagradost/cloudstream3/ui/BaseAdapter.kt | 2 + .../cloudstream3/ui/ControllerActivity.kt | 2 + .../cloudstream3/ui/CustomRecyclerViews.kt | 4 +- .../cloudstream3/ui/EasterEggMonke.kt | 2 +- .../ui/NonFinalAdapterListUpdateCallback.kt | 2 +- .../lagradost/cloudstream3/ui/WatchType.kt | 4 +- .../cloudstream3/ui/WebviewFragment.kt | 3 + .../ui/download/button/BaseFetchButton.kt | 1 + .../ui/download/button/DownloadButton.kt | 2 +- .../ui/home/HomeChildItemAdapter.kt | 5 +- .../cloudstream3/ui/home/HomeFragment.kt | 3 +- .../ui/home/HomeParentItemAdapter.kt | 13 +- .../ui/home/HomeParentItemAdapterPreview.kt | 7 +- .../cloudstream3/ui/home/HomeViewModel.kt | 10 +- .../ui/library/LibraryFragment.kt | 6 +- .../ui/library/ViewpagerAdapter.kt | 3 +- .../ui/player/AbstractPlayerFragment.kt | 7 +- .../cloudstream3/ui/player/CS3IPlayer.kt | 82 ++--------- .../ui/player/CustomSubtitleDecoderFactory.kt | 7 +- .../ui/player/CustomTextRenderer.kt | 3 +- .../ui/player/DownloadFileGenerator.kt | 2 +- .../ui/player/DownloadedPlayerActivity.kt | 4 - .../ui/player/FullScreenPlayer.kt | 42 +++--- .../cloudstream3/ui/player/GeneratorPlayer.kt | 20 ++- .../cloudstream3/ui/player/IPlayer.kt | 1 - .../cloudstream3/ui/player/LinkGenerator.kt | 1 - .../ui/player/NonFinalTextRenderer.java | 16 +- .../ui/player/OfflinePlaybackHelper.kt | 2 +- .../ui/player/PlayerGeneratorViewModel.kt | 2 +- .../ui/player/PlayerSubtitleHelper.kt | 3 + .../ui/player/PreviewGenerator.kt | 21 ++- .../ui/player/RepoLinkGenerator.kt | 1 + .../player/source_priority/PriorityAdapter.kt | 5 - .../player/source_priority/ProfilesAdapter.kt | 2 - .../source_priority/QualityDataHelper.kt | 7 +- .../source_priority/QualityProfileDialog.kt | 2 +- .../source_priority/SourcePriorityDialog.kt | 2 +- .../cloudstream3/ui/result/ActorAdaptor.kt | 5 +- .../cloudstream3/ui/result/EpisodeAdapter.kt | 6 +- .../cloudstream3/ui/result/ImageAdapter.kt | 15 +- .../ui/result/ResultFragmentPhone.kt | 49 ++++--- .../ui/result/ResultFragmentTv.kt | 45 +----- .../ui/result/ResultViewModel2.kt | 2 +- .../cloudstream3/ui/result/SelectAdaptor.kt | 3 +- .../cloudstream3/ui/search/SearchAdaptor.kt | 7 +- .../ui/search/SearchHistoryAdaptor.kt | 8 +- .../ui/search/SearchResultBuilder.kt | 7 +- .../ui/search/SyncSearchViewModel.kt | 4 +- .../ui/settings/AccountAdapter.kt | 7 +- .../ui/settings/SettingsFragment.kt | 1 - .../ui/settings/SettingsGeneral.kt | 2 - .../ui/settings/SettingsPlayer.kt | 9 +- .../ui/settings/SettingsProviders.kt | 2 +- .../ui/settings/SettingsUpdates.kt | 10 +- .../ui/settings/extensions/PluginAdapter.kt | 23 +-- .../ui/settings/extensions/PluginsFragment.kt | 2 +- .../settings/extensions/PluginsViewModel.kt | 1 - .../ui/setup/SetupFragmentMedia.kt | 1 - .../subtitles/ChromecastSubtitlesFragment.kt | 29 ++-- .../ui/subtitles/SubtitlesFragment.kt | 9 +- .../lagradost/cloudstream3/utils/AniSkip.kt | 2 +- .../cloudstream3/utils/AppContextUtils.kt | 5 +- .../cloudstream3/utils/BackupUtils.kt | 36 ++--- .../lagradost/cloudstream3/utils/DataStore.kt | 25 +++- .../utils/DownloadFileWorkManager.kt | 1 - .../cloudstream3/utils/InAppUpdater.kt | 40 ++--- .../cloudstream3/utils/PackageInstaller.kt | 5 +- .../cloudstream3/utils/PowerManagerAPI.kt | 6 +- .../lagradost/cloudstream3/utils/SyncUtil.kt | 8 +- .../lagradost/cloudstream3/utils/UIHelper.kt | 2 +- .../utils/VideoDownloadManager.kt | 8 +- .../cloudstream3/widget/FlowLayout.kt | 2 +- .../cloudstream3/PluginAdapterTest.kt | 16 ++ 97 files changed, 563 insertions(+), 721 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt create mode 100644 app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 598ff540..d6f978fe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -35,6 +35,7 @@ import java.io.File import java.io.FileNotFoundException import java.io.PrintStream import java.lang.ref.WeakReference +import java.util.Locale import kotlin.concurrent.thread import kotlin.system.exitProcess @@ -81,14 +82,8 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) : ACRA.errorReporter.handleException(error) try { PrintStream(errorFile).use { ps -> - ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")) - ps.println( - String.format( - "Fatal exception on thread %s (%d)", - thread.name, - thread.id - ) - ) + ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}") + ps.println("Fatal exception on thread ${thread.name} (${thread.id})") error.printStackTrace(ps) } } catch (ignored: FileNotFoundException) { @@ -106,7 +101,6 @@ class AcraApplication : Application() { override fun onCreate() { super.onCreate() - //NativeCrashHandler.initCrashHandler() ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index ba303fef..63912114 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -164,7 +164,7 @@ object CommonActivity { val toast = Toast(act) toast.duration = duration ?: Toast.LENGTH_SHORT toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) - toast.view = binding.root + toast.view = binding.root //fixme Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version. currentToast = toast toast.show() @@ -464,20 +464,6 @@ object CommonActivity { fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) { - //println("Keycode: $keyCode") - //showToast( - // this, - // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}", - // Toast.LENGTH_LONG - //) - - // Tested keycodes on remote: - // KeyEvent.KEYCODE_MEDIA_FAST_FORWARD - // KeyEvent.KEYCODE_MEDIA_REWIND - // KeyEvent.KEYCODE_MENU - // KeyEvent.KEYCODE_MEDIA_NEXT - // KeyEvent.KEYCODE_MEDIA_PREVIOUS - // KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE // 149 keycode_numpad 5 when (keyCode) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt index 934dd58a..8da7ca38 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt @@ -11,7 +11,7 @@ import java.util.concurrent.TimeUnit class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() { - private val client: OkHttpClient + private val client: OkHttpClient = builder.readTimeout(30, TimeUnit.SECONDS).build() override fun execute(request: Request): Response { val httpMethod: String = request.httpMethod() val url: String = request.url() @@ -74,8 +74,4 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do return instance } } - - init { - client = builder.readTimeout(30, TimeUnit.SECONDS).build() - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index bc2cb88e..eed69a50 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -82,13 +82,13 @@ import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver import com.lagradost.cloudstream3.services.SubscriptionWorkManager +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_PLAYER +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_REPO +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_SEARCH import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localListApi import com.lagradost.cloudstream3.syncproviders.SyncAPI @@ -347,7 +347,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa println("Repository url: $realUrl") loadRepository(realUrl) return true - } else if (str.contains(appString)) { + } else if (str.contains(APP_STRING)) { for (api in OAuth2Apis) { if (str.contains("/${api.redirectUrl}")) { ioSafe { @@ -377,15 +377,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } // This specific intent is used for the gradle deployWithAdb // https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46 - if (str == "$appString:") { + if (str == "$APP_STRING:") { PluginManager.hotReloadAllLocalPlugins(activity) } - } else if (safeURI(str)?.scheme == appStringRepo) { - val url = str.replaceFirst(appStringRepo, "https") + } else if (safeURI(str)?.scheme == APP_STRING_REPO) { + val url = str.replaceFirst(APP_STRING_REPO, "https") loadRepository(url) return true - } else if (safeURI(str)?.scheme == appStringSearch) { - val query = str.substringAfter("$appStringSearch://") + } else if (safeURI(str)?.scheme == APP_STRING_SEARCH) { + val query = str.substringAfter("$APP_STRING_SEARCH://") nextSearchQuery = try { URLDecoder.decode(query, "UTF-8") @@ -399,7 +399,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa R.id.navigation_search activity?.findViewById(R.id.nav_rail_view)?.selectedItemId = R.id.navigation_search - } else if (safeURI(str)?.scheme == appStringPlayer) { + } else if (safeURI(str)?.scheme == APP_STRING_PLAYER) { val uri = Uri.parse(str) val name = uri.getQueryParameter("name") val url = URLDecoder.decode(uri.authority, "UTF-8") @@ -413,9 +413,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ) ) ) - } else if (safeURI(str)?.scheme == appStringResumeWatching) { + } else if (safeURI(str)?.scheme == APP_STRING_RESUME_WATCHING) { val id = - str.substringAfter("$appStringResumeWatching://").toIntOrNull() + str.substringAfter("$APP_STRING_RESUME_WATCHING://").toIntOrNull() ?: return false ioSafe { val resumeWatchingCard = @@ -469,7 +469,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ) DubStatus.Dubbed else DubStatus.Subbed, null ) } else { - viewModel.loadSmall(this, result) + viewModel.loadSmall(result) } } @@ -605,7 +605,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } //private var mCastSession: CastSession? = null - lateinit var mSessionManager: SessionManager + var mSessionManager: SessionManager? = null private val mSessionManagerListener: SessionManagerListener by lazy { SessionManagerListenerImpl() } private inner class SessionManagerListenerImpl : SessionManagerListener { @@ -645,8 +645,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa setActivityInstance(this) try { if (isCastApiAvailable()) { - //mCastSession = mSessionManager.currentCastSession - mSessionManager.addSessionManagerListener(mSessionManagerListener) + mSessionManager?.addSessionManagerListener(mSessionManagerListener) } } catch (e: Exception) { logError(e) @@ -662,7 +661,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } try { if (isCastApiAvailable()) { - mSessionManager.removeSessionManagerListener(mSessionManagerListener) + mSessionManager?.removeSessionManagerListener(mSessionManagerListener) //mCastSession = null } } catch (e: Exception) { @@ -766,7 +765,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa list.forEach { custom -> allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } ?.let { - allProviders.add(it.javaClass.newInstance().apply { + allProviders.add(it.javaClass.getDeclaredConstructor().newInstance().apply { name = custom.name lang = custom.lang mainUrl = custom.url.trimEnd('/') @@ -1147,7 +1146,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa super.onCreate(savedInstanceState) try { if (isCastApiAvailable()) { - mSessionManager = CastContext.getSharedInstance(this).sessionManager + CastContext.getSharedInstance(this) {it.run()}.addOnSuccessListener { mSessionManager = it.sessionManager } } } catch (t: Throwable) { logError(t) @@ -1449,13 +1448,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa val value = viewModel.watchStatus.value ?: WatchType.NONE this@MainActivity.showBottomDialog( - WatchType.values().map { getString(it.stringRes) }.toList(), + WatchType.entries.map { getString(it.stringRes) }.toList(), value.ordinal, this@MainActivity.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { viewModel.updateWatchStatus( - WatchType.values()[it], + WatchType.entries[it], this@MainActivity ) } @@ -1465,12 +1464,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ?: SyncWatchType.NONE this@MainActivity.showBottomDialog( - SyncWatchType.values().map { getString(it.stringRes) }.toList(), + SyncWatchType.entries.map { getString(it.stringRes) }.toList(), value.ordinal, this@MainActivity.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - syncViewModel.setStatus(SyncWatchType.values()[it].internalId) + syncViewModel.setStatus(SyncWatchType.entries[it].internalId) syncViewModel.publishUserData() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt deleted file mode 100644 index 7be90440..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.lagradost.cloudstream3 - -import com.lagradost.cloudstream3.MainActivity.Companion.lastError -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.plugins.PluginManager.checkSafeModeFile -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -object NativeCrashHandler { - // external fun triggerNativeCrash() - /*private external fun initNativeCrashHandler() - private external fun getSignalStatus(): Int - - private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch { - - //launch { - // delay(10000) - // triggerNativeCrash() - //} - - while (true) { - delay(10_000) - val signal = getSignalStatus() - // Signal is initialized to zero - if (signal == 0) continue - - // Do not crash in safe mode! - if (lastError != null) continue - if (checkSafeModeFile()) continue - - AcraApplication.exceptionHandler?.uncaughtException( - Thread.currentThread(), - RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n") - ) - } - } - - fun initCrashHandler() { - try { - System.loadLibrary("native-lib") - initNativeCrashHandler() - } catch (t: Throwable) { - // Make debug crash. - if (BuildConfig.DEBUG) throw t - logError(t) - return - } - - initSignalPolling() - }*/ -} \ 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 index 75e96bec..bc646a8d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt @@ -2,15 +2,13 @@ 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+)""") + SyncIdName.MyAnimeList to Regex("""myanimelist\.net/anime/(\d+)"""), + SyncIdName.Anilist to Regex("""anilist\.co/anime/(\d+)""") ) suspend fun redirect( diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index a1b9ff34..addee9a0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -296,7 +296,7 @@ open class TraktProvider : MainAPI() { return try { val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) val dateTime = dateString?.let { format.parse(it)?.time } ?: return false - APIHolder.unixTimeMS < dateTime + unixTimeMS < dateTime } catch (t: Throwable) { logError(t) false 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 ce2fb3a2..85a9db5d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.network -import android.util.Base64 import android.util.Log import android.webkit.CookieManager import androidx.annotation.AnyThread @@ -10,7 +9,10 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.nicehttp.Requests.Companion.await import com.lagradost.nicehttp.cookies import kotlinx.coroutines.runBlocking -import okhttp3.* +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response import java.net.URI diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt index e89ccfeb..ddf5b286 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt @@ -2,5 +2,4 @@ package com.lagradost.cloudstream3.plugins @Suppress("unused") @Target(AnnotationTarget.CLASS) -annotation class CloudstreamPlugin( -) \ No newline at end of file +annotation class CloudstreamPlugin \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index 7f08af92..fc836587 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -34,7 +34,7 @@ abstract class Plugin { */ fun registerMainAPI(element: MainAPI) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI") - element.sourcePlugin = this.__filename + element.sourcePlugin = this.filename // Race condition causing which would case duplicates if not for distinctBy synchronized(APIHolder.allProviders) { APIHolder.allProviders.add(element) @@ -48,7 +48,7 @@ abstract class Plugin { */ fun registerExtractorAPI(element: ExtractorApi) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi") - element.sourcePlugin = this.__filename + element.sourcePlugin = this.filename extractorApis.add(element) } @@ -68,7 +68,11 @@ abstract class Plugin { */ var resources: Resources? = null /** Full file path to the plugin. */ - var __filename: String? = null + @Deprecated("Renamed to `filename` to follow conventions", replaceWith = ReplaceWith("filename")) + var __filename: String? + get() = filename + set(value) {filename = value} + var filename: String? = null /** * This will add a button in the settings allowing you to add custom settings 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 6b2b75f2..bc2a1780 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -1,13 +1,16 @@ package com.lagradost.cloudstream3.plugins +import android.Manifest import android.app.* import android.content.Context +import android.content.pm.PackageManager import android.content.res.AssetManager import android.content.res.Resources import android.os.Build import android.os.Environment import android.util.Log import android.widget.Toast +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.FragmentActivity @@ -163,7 +166,7 @@ object PluginManager { private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins" - public var currentlyLoading: String? = null + var currentlyLoading: String? = null // Maps filepath to plugin val plugins: MutableMap = @@ -339,7 +342,7 @@ object PluginManager { //Omit non-NSFW if mode is set to NSFW only if (mode == AutoDownloadMode.NsfwOnly) { - if (tvtypes.contains(TvType.NSFW.name) == false) { + if (!tvtypes.contains(TvType.NSFW.name)) { return@mapNotNull null } } @@ -504,10 +507,12 @@ object PluginManager { val version: Int = manifest.version ?: PLUGIN_VERSION_NOT_SET.also { Log.d(TAG, "No manifest version for ${data.internalName}") } + + @Suppress("UNCHECKED_CAST") val pluginClass: Class<*> = loader.loadClass(manifest.pluginClassName) as Class val pluginInstance: Plugin = - pluginClass.newInstance() as Plugin + pluginClass.getDeclaredConstructor().newInstance() as Plugin // Sets with the proper version setPluginData(data.copy(version = version)) @@ -517,14 +522,16 @@ object PluginManager { return true } - pluginInstance.__filename = file.absolutePath + pluginInstance.filename = file.absolutePath if (manifest.requiresResources) { Log.d(TAG, "Loading resources for ${data.internalName}") // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk - val assets = AssetManager::class.java.newInstance() + val assets = AssetManager::class.java.getDeclaredConstructor().newInstance() val addAssetPath = AssetManager::class.java.getMethod("addAssetPath", String::class.java) addAssetPath.invoke(assets, file.absolutePath) + + @Suppress("DEPRECATION") pluginInstance.resources = Resources( assets, context.resources.displayMetrics, @@ -566,14 +573,14 @@ object PluginManager { // remove all registered apis synchronized(APIHolder.apis) { - APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach { + APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { removePluginMapping(it) } } synchronized(APIHolder.allProviders) { - APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename } + APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename } } - extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename } + extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename } classLoaders.values.removeIf { v -> v == plugin } @@ -720,9 +727,14 @@ object PluginManager { } val notification = builder.build() - with(NotificationManagerCompat.from(context)) { - // notificationId is a unique int for each notification that you must define - notify((System.currentTimeMillis() / 1000).toInt(), notification) + // notificationId is a unique int for each notification that you must define + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + NotificationManagerCompat.from(context) + .notify((System.currentTimeMillis() / 1000).toInt(), notification) } return notification } catch (e: Exception) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt index b80a590e..c6ec9df7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -73,7 +73,7 @@ object RepositoryManager { val PREBUILT_REPOSITORIES: Array by lazy { getKey("PREBUILT_REPOSITORIES") ?: emptyArray() } - val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$") + private val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$") /* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */ fun convertRawGitUrl(url: String): String { diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt index a45ab5f0..d1b702f4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.sync.withLock object VotingApi { // please do not cheat the votes lol private const val LOGKEY = "VotingApi" - private const val apiDomain = "https://counterapi.com/api" + private const val API_DOMAIN = "https://counterapi.com/api" private fun transformUrl(url: String): String = // dont touch or all votes get reset MessageDigest @@ -49,13 +49,13 @@ object VotingApi { // please do not cheat the votes lol .joinToString("-") private suspend fun readVote(pluginUrl: String): Int { - var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true" + val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true" Log.d(LOGKEY, "Requesting: $url") return app.get(url).parsedSafe()?.value ?: 0 } private suspend fun writeVote(pluginUrl: String): Boolean { - var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}" + val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}" Log.d(LOGKEY, "Requesting: $url") return app.get(url).parsedSafe()?.value != null } @@ -69,8 +69,7 @@ object VotingApi { // please do not cheat the votes lol getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false fun canVote(pluginUrl: String): Boolean { - if (!PluginManager.urlPlugins.contains(pluginUrl)) return false - return true + return PluginManager.urlPlugins.contains(pluginUrl) } private val voteLock = Mutex() diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt index 857fba11..df64caab 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt @@ -59,7 +59,7 @@ class SubtitleResource { return file } - fun unzip(file: File): List> { + private fun unzip(file: File): List> { val entries = mutableListOf>() ZipInputStream(file.inputStream()).use { zipInputStream -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt index ed4ccb74..685b499b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.subtitles -import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.TvType class AbstractSubtitleEntities { 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 0259ccad..2e14c3c4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -56,22 +56,22 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { subSourceApi ) - const val appString = "cloudstreamapp" - const val appStringRepo = "cloudstreamrepo" - const val appStringPlayer = "cloudstreamplayer" + const val APP_STRING = "cloudstreamapp" + const val APP_STRING_REPO = "cloudstreamrepo" + const val APP_STRING_PLAYER = "cloudstreamplayer" // Instantly start the search given a query - const val appStringSearch = "cloudstreamsearch" + const val APP_STRING_SEARCH = "cloudstreamsearch" // Instantly resume watching a show - const val appStringResumeWatching = "cloudstreamcontinuewatching" + const val APP_STRING_RESUME_WATCHING = "cloudstreamcontinuewatching" val unixTime: Long get() = System.currentTimeMillis() / 1000L val unixTimeMs: Long get() = System.currentTimeMillis() - const val maxStale = 60 * 10 + const val MAX_STALE = 60 * 10 fun secondsToReadable(seconds: Int, completedValue: String): String { var secondsLong = seconds.toLong() diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt index 507c5e2a..db467639 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt @@ -18,13 +18,13 @@ class Addic7ed : AbstractSubApi { override fun logOut() {} companion object { - const val host = "https://www.addic7ed.com" + const val HOST = "https://www.addic7ed.com" const val TAG = "ADDIC7ED" } private fun fixUrl(url: String): String { - return if (url.startsWith("/")) host + url - else if (!url.startsWith("http")) "$host/$url" + return if (url.startsWith("/")) HOST + url + else if (!url.startsWith("http")) "$HOST/$url" else url } @@ -62,7 +62,7 @@ class Addic7ed : AbstractSubApi { } val title = queryText.substringBefore("(").trim() - val url = "$host/search.php?search=${title}&Submit=Search" + val url = "$HOST/search.php?search=${title}&Submit=Search" val hostDocument = app.get(url).document var searchResult = "" if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url @@ -74,8 +74,8 @@ class Addic7ed : AbstractSubApi { hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(") ?.substringBefore(",") val doc = app.get( - "$host/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined", - referer = "$host/" + "$HOST/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined", + referer = "$HOST/" ).document doc.select("#season tr:contains($queryLang)").mapNotNull { node -> if (node.selectFirst("td")?.text() @@ -97,7 +97,7 @@ class Addic7ed : AbstractSubApi { val link = fixUrl(node.select("a.buttonDownload").attr("href")) val isHearingImpaired = !node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty() - cleanResources(results, name, link, mapOf("referer" to "$host/"), isHearingImpaired) + cleanResources(results, name, link, mapOf("referer" to "$HOST/"), isHearingImpaired) } return results } 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 8a82cf94..e51d3d65 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 @@ -63,7 +63,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun handleRedirect(url: String): Boolean { val sanitizer = - splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR + splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR val token = sanitizer["access_token"]!! val expiresIn = sanitizer["expires_in"]!! @@ -87,7 +87,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun search(name: String): List? { val data = searchShows(name) ?: return null - return data.data?.Page?.media?.map { + return data.data?.page?.media?.map { SyncAPI.SyncSearchResult( it.title.romaji ?: return null, this.name, @@ -101,7 +101,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun getResult(id: String): SyncAPI.SyncResult { val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1) ?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId") - val season = getSeason(internalId).data.Media + val season = getSeason(internalId).data.media return SyncAPI.SyncResult( season.id.toString(), @@ -301,12 +301,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { //println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}") val shows = searchShows(name.replace(blackListRegex, "")) - shows?.data?.Page?.media?.find { + shows?.data?.page?.media?.find { (malId ?: "NONE") == it.idMal.toString() }?.let { return it } val filtered = - shows?.data?.Page?.media?.filter { + shows?.data?.page?.media?.filter { (((it.startDate.year ?: year.toString()) == year.toString() || year == null)) } @@ -496,7 +496,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { val data = postApi(q, true) val d = parseJson(data ?: return null) - val main = d.data?.Media + val main = d.data?.media if (main?.mediaListEntry != null) { return AniListTitleHolder( title = main.title, @@ -536,7 +536,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { headers = mapOf( "Authorization" to "Bearer " + (getAuth() ?: return@suspendSafeApiCall null), - if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" + if (cache) "Cache-Control" to "max-stale=$MAX_STALE" else "Cache-Control" to "no-cache" ), cacheTime = 0, data = mapOf( @@ -647,7 +647,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class Data( - @JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection + @JsonProperty("MediaListCollection") val mediaListCollection: MediaListCollection ) private fun getAniListListCached(): Array? { @@ -659,7 +659,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { if (checkToken()) return null return if (requireLibraryRefresh) { - val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray() + val list = getFullAniListList()?.data?.mediaListCollection?.lists?.toTypedArray() if (list != null) { setKey(ANILIST_CACHED_LIST, list) } @@ -678,7 +678,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { // To fill empty lists when AniList does not return them val baseMap = - AniListStatusType.values().filter { it.value >= 0 }.associate { + AniListStatusType.entries.filter { it.value >= 0 }.associate { it.stringRes to emptyList() } @@ -764,7 +764,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { /** Used to query a saved MediaItem on the list to get the id for removal */ data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null) - data class MediaListItem(@JsonProperty("MediaList") val MediaList: MediaListId? = null) + data class MediaListItem(@JsonProperty("MediaList") val mediaList: MediaListId? = null) data class MediaListId(@JsonProperty("id") val id: Long? = null) private suspend fun postDataAboutId( @@ -787,7 +787,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { """ val response = postApi(idQuery) val listId = - tryParseJson(response)?.data?.MediaList?.id ?: return false + tryParseJson(response)?.data?.mediaList?.id ?: return false """ mutation(${'$'}id: Int = $listId) { DeleteMediaListEntry(id: ${'$'}id) { @@ -836,7 +836,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { val data = postApi(q) if (data.isNullOrBlank()) return null val userData = parseJson(data) - val u = userData.data?.Viewer + val u = userData.data?.viewer val user = AniListUser( u?.id, u?.name, @@ -858,8 +858,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { suspend fun getSeasonRecursive(id: Int) { val season = getSeason(id) seasons.add(season) - if (season.data.Media.format?.startsWith("TV") == true) { - season.data.Media.relations?.edges?.forEach { + if (season.data.media.format?.startsWith("TV") == true) { + season.data.media.relations?.edges?.forEach { if (it.node?.format != null) { if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) { getSeasonRecursive(it.node.id) @@ -878,7 +878,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class SeasonData( - @JsonProperty("Media") val Media: SeasonMedia, + @JsonProperty("Media") val media: SeasonMedia, ) data class SeasonMedia( @@ -1050,7 +1050,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class AniListData( - @JsonProperty("Viewer") val Viewer: AniListViewer?, + @JsonProperty("Viewer") val viewer: AniListViewer?, ) data class AniListRoot( @@ -1090,7 +1090,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class LikeData( - @JsonProperty("Viewer") val Viewer: LikeViewer?, + @JsonProperty("Viewer") val viewer: LikeViewer?, ) data class LikeRoot( @@ -1130,7 +1130,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class GetDataData( - @JsonProperty("Media") val Media: GetDataMedia?, + @JsonProperty("Media") val media: GetDataMedia?, ) data class GetDataRoot( @@ -1163,7 +1163,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class GetSearchPage( - @JsonProperty("Page") val Page: GetSearchData?, + @JsonProperty("Page") val page: GetSearchData?, ) data class GetSearchData( 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 index 00f8d00c..f819cd3b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -119,8 +119,6 @@ class LocalList : SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, -// ListSorting.RatingHigh, -// ListSorting.RatingLow, ) ) } 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 24ef7136..6046a0f2 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 @@ -19,14 +19,18 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType 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.AppContextUtils.splitQuery +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import java.net.URL import java.security.SecureRandom import java.text.ParseException import java.text.SimpleDateFormat -import java.util.* +import java.time.Instant +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.TimeZone /** max 100 via https://myanimelist.net/apiconfig/references/api/v2#tag/anime */ const val MAL_MAX_SEARCH_LIMIT = 25 @@ -51,7 +55,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } override fun loginInfo(): AuthAPI.LoginInfo? { - //getMalUser(true)? getKey(accountId, MAL_USER_KEY)?.let { user -> return AuthAPI.LoginInfo( profilePicture = user.picture, @@ -84,7 +87,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { this.name, node.id.toString(), "$mainUrl/anime/${node.id}/", - node.main_picture?.large ?: node.main_picture?.medium + node.mainPicture?.large ?: node.mainPicture?.medium ) } } @@ -178,7 +181,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private fun parseDate(string: String?): Long? { return try { - SimpleDateFormat("yyyy-MM-dd")?.parse(string ?: return null)?.time + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(string ?: return null)?.time } catch (e: Exception) { null } @@ -190,7 +193,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { apiName = this.name, syncId = node.id.toString(), url = "$mainUrl/anime/${node.id}", - posterUrl = node.main_picture?.large + posterUrl = node.mainPicture?.large ) } @@ -244,12 +247,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { val internalId = id.toIntOrNull() ?: return null val data = - getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status") + getDataAboutMalId(internalId)?.myListStatus //?: throw ErrorLoadingException("No my_list_status") return SyncAPI.SyncStatus( score = data?.score, - status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)) , + status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)), isFavorite = null, - watchedEpisodes = data?.num_episodes_watched, + watchedEpisodes = data?.numEpisodesWatched, ) } @@ -291,7 +294,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private fun parseDateLong(string: String?): Long? { return try { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse( + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse( string ?: return null )?.time?.div(1000) } catch (e: Exception) { @@ -302,7 +305,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun handleRedirect(url: String): Boolean { val sanitizer = - splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR + splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR val state = sanitizer["state"]!! if (state == "RequestID$requestId") { val currentCode = sanitizer["code"]!! @@ -351,9 +354,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { try { if (response != "") { val token = parseJson(response) - 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) + setKey(accountId, MAL_UNIXTIME_KEY, (token.expiresIn + unixTime)) + setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refreshToken) + setKey(accountId, MAL_TOKEN_KEY, token.accessToken) requireLibraryRefresh = true } } catch (e: Exception) { @@ -395,53 +398,53 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class Node( @JsonProperty("id") val id: Int, @JsonProperty("title") val title: String, - @JsonProperty("main_picture") val main_picture: MainPicture?, - @JsonProperty("alternative_titles") val alternative_titles: AlternativeTitles?, - @JsonProperty("media_type") val media_type: String?, - @JsonProperty("num_episodes") val num_episodes: Int?, + @JsonProperty("main_picture") val mainPicture: MainPicture?, + @JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?, + @JsonProperty("media_type") val mediaType: String?, + @JsonProperty("num_episodes") val numEpisodes: Int?, @JsonProperty("status") val status: String?, - @JsonProperty("start_date") val start_date: String?, - @JsonProperty("end_date") val end_date: String?, - @JsonProperty("average_episode_duration") val average_episode_duration: Int?, + @JsonProperty("start_date") val startDate: String?, + @JsonProperty("end_date") val endDate: String?, + @JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?, @JsonProperty("synopsis") val synopsis: String?, @JsonProperty("mean") val mean: Double?, @JsonProperty("genres") val genres: List?, @JsonProperty("rank") val rank: Int?, @JsonProperty("popularity") val popularity: Int?, - @JsonProperty("num_list_users") val num_list_users: Int?, - @JsonProperty("num_favorites") val num_favorites: Int?, - @JsonProperty("num_scoring_users") val num_scoring_users: Int?, - @JsonProperty("start_season") val start_season: StartSeason?, + @JsonProperty("num_list_users") val numListUsers: Int?, + @JsonProperty("num_favorites") val numFavorites: Int?, + @JsonProperty("num_scoring_users") val numScoringUsers: Int?, + @JsonProperty("start_season") val startSeason: StartSeason?, @JsonProperty("broadcast") val broadcast: Broadcast?, @JsonProperty("nsfw") val nsfw: String?, - @JsonProperty("created_at") val created_at: String?, - @JsonProperty("updated_at") val updated_at: String? + @JsonProperty("created_at") val createdAt: String?, + @JsonProperty("updated_at") val updatedAt: String? ) data class ListStatus( @JsonProperty("status") val status: String?, @JsonProperty("score") val score: Int, - @JsonProperty("num_episodes_watched") val num_episodes_watched: Int, - @JsonProperty("is_rewatching") val is_rewatching: Boolean, - @JsonProperty("updated_at") val updated_at: String, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int, + @JsonProperty("is_rewatching") val isRewatching: Boolean, + @JsonProperty("updated_at") val updatedAt: String, ) data class Data( @JsonProperty("node") val node: Node, - @JsonProperty("list_status") val list_status: ListStatus?, + @JsonProperty("list_status") val listStatus: 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), + this.listStatus?.numEpisodesWatched, + this.node.numEpisodes, + this.listStatus?.score?.times(10), + parseDateLong(this.listStatus?.updatedAt), "MAL", TvType.Anime, - this.node.main_picture?.large ?: this.node.main_picture?.medium, + this.node.mainPicture?.large ?: this.node.mainPicture?.medium, null, null, plot = this.node.synopsis, @@ -470,8 +473,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ) data class Broadcast( - @JsonProperty("day_of_the_week") val day_of_the_week: String?, - @JsonProperty("start_time") val start_time: String? + @JsonProperty("day_of_the_week") val dayOfTheWeek: String?, + @JsonProperty("start_time") val startTime: String? ) private fun getMalAnimeListCached(): Array? { @@ -491,14 +494,14 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata { val list = getMalAnimeListSmart()?.groupBy { - convertToStatus(it.list_status?.status ?: "").stringRes + convertToStatus(it.listStatus?.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 { + MalStatusType.entries.filter { it.value >= 0 }.associate { it.stringRes to emptyList() } @@ -573,7 +576,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ).text val values = parseJson(res) val titles = - values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) } + values.data.map { MalTitleHolder(it.listStatus, it.node.id, it.node.title) } for (t in titles) { allTitles[t.id] = t } @@ -582,11 +585,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } } - fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { + private fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { // No time remaining if the show has already ended try { endDate?.let { - if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null + if (SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(it) + ?.before(Date.from(Instant.now())) != false + ) return@convertJapanTimeToTimeRemaining null } } catch (e: ParseException) { logError(e) @@ -603,7 +608,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH) val currentYear = currentDate.get(Calendar.YEAR) - val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm") + val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm", Locale.getDefault()) dateFormat.timeZone = TimeZone.getTimeZone("Japan") val parsedDate = dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null @@ -647,13 +652,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { id: Int, status: MalStatusType? = null, score: Int? = null, - num_watched_episodes: Int? = null, + numWatchedEpisodes: Int? = null, ): Boolean { val res = setScoreRequest( id, if (status == null) null else malStatusAsString[maxOf(0, status.value)], score, - num_watched_episodes + numWatchedEpisodes ) return if (res.isNullOrBlank()) { @@ -670,17 +675,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } } + @Suppress("UNCHECKED_CAST") private suspend fun setScoreRequest( id: Int, status: String? = null, score: Int? = null, - num_watched_episodes: Int? = null, + numWatchedEpisodes: Int? = null, ): String? { val data = mapOf( "status" to status, "score" to score?.toString(), - "num_watched_episodes" to num_watched_episodes?.toString() - ).filter { it.value != null } as Map + "num_watched_episodes" to numWatchedEpisodes?.toString() + ).filterValues { it != null } as Map return app.put( "$apiUrl/v2/anime/$id/my_list_status", @@ -693,10 +699,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class ResponseToken( - @JsonProperty("token_type") val token_type: String, - @JsonProperty("expires_in") val expires_in: Int, - @JsonProperty("access_token") val access_token: String, - @JsonProperty("refresh_token") val refresh_token: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("expires_in") val expiresIn: Int, + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("refresh_token") val refreshToken: String, ) data class MalRoot( @@ -705,7 +711,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class MalDatum( @JsonProperty("node") val node: MalNode, - @JsonProperty("list_status") val list_status: MalStatus, + @JsonProperty("list_status") val listStatus: MalStatus, ) data class MalNode( @@ -722,16 +728,16 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class MalStatus( @JsonProperty("status") val status: String, @JsonProperty("score") val score: Int, - @JsonProperty("num_episodes_watched") val num_episodes_watched: Int, - @JsonProperty("is_rewatching") val is_rewatching: Boolean, - @JsonProperty("updated_at") val updated_at: String, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int, + @JsonProperty("is_rewatching") val isRewatching: Boolean, + @JsonProperty("updated_at") val updatedAt: String, ) data class MalUser( @JsonProperty("id") val id: Int, @JsonProperty("name") val name: String, @JsonProperty("location") val location: String, - @JsonProperty("joined_at") val joined_at: String, + @JsonProperty("joined_at") val joinedAt: String, @JsonProperty("picture") val picture: String?, ) @@ -744,9 +750,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class SmallMalAnime( @JsonProperty("id") val id: Int, @JsonProperty("title") val title: String?, - @JsonProperty("num_episodes") val num_episodes: Int, - @JsonProperty("my_list_status") val my_list_status: MalStatus?, - @JsonProperty("main_picture") val main_picture: MalMainPicture?, + @JsonProperty("num_episodes") val numEpisodes: Int, + @JsonProperty("my_list_status") val myListStatus: MalStatus?, + @JsonProperty("main_picture") val mainPicture: MalMainPicture?, ) data class MalSearchNode( 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 6412ff1b..37b95614 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager -import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.AppUtils import okhttp3.Interceptor import okhttp3.Response @@ -30,10 +29,10 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi companion object { const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile - const val apiKey = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2" - const val host = "https://api.opensubtitles.com/api/v1" + const val API_KEY = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2" + const val HOST = "https://api.opensubtitles.com/api/v1" const val TAG = "OPENSUBS" - const val coolDownDuration: Long = 1000L * 30L // CoolDown if 429 error code in ms + const val COOLDOWN_DURATION: Long = 1000L * 30L // CoolDown if 429 error code in ms var currentCoolDown: Long = 0L var currentSession: SubtitleOAuthEntity? = null } @@ -49,7 +48,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi chain.request().newBuilder() .removeHeader("user-agent") .addHeader("user-agent", userAgent) - .addHeader("Api-Key", apiKey) + .addHeader("Api-Key", API_KEY) .build() ) } @@ -66,7 +65,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi } private fun throwGotTooManyRequests() { - currentCoolDown = unixTimeMs + coolDownDuration + currentCoolDown = unixTimeMs + COOLDOWN_DURATION throw ErrorLoadingException("Too many requests") } @@ -115,7 +114,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi private suspend fun initLogin(username: String, password: String): Boolean { //Log.i(TAG, "DATA = [$username] [$password]") val response = app.post( - url = "$host/login", + url = "$HOST/login", headers = mapOf( "Content-Type" to "application/json", ), @@ -134,7 +133,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi SubtitleOAuthEntity( user = username, pass = password, - access_token = token.token ?: run { + accessToken = token.token ?: run { return false }) ) @@ -197,8 +196,8 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val searchQueryUrl = when (imdbId > 0) { //Use imdb_id to search if its valid - true -> "$host/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" - false -> "$host/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" + true -> "$HOST/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" + false -> "$HOST/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" } val req = app.get( @@ -233,7 +232,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber val year = featureDetails?.year ?: query.year val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie - val isHearingImpaired = attr.hearing_impaired ?: false + val isHearingImpaired = attr.hearingImpaired ?: false //Log.i(TAG, "Result id/name => ${item.id} / $name") item.attributes?.files?.forEach { file -> val resultData = file.fileId?.toString() ?: "" @@ -266,11 +265,11 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi throwIfCantDoRequest() val req = app.post( - url = "$host/download", + url = "$HOST/download", headers = mapOf( Pair( "Authorization", - "Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}" + "Bearer ${currentSession?.accessToken ?: throw ErrorLoadingException("No access token active in current session")}" ), Pair("Content-Type", "application/json"), Pair("Accept", "*/*") @@ -299,7 +298,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi data class SubtitleOAuthEntity( var user: String, var pass: String, - var access_token: String, + var accessToken: String, ) data class OAuthToken( @@ -324,7 +323,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi @JsonProperty("url") var url: String? = null, @JsonProperty("files") var files: List? = listOf(), @JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(), - @JsonProperty("hearing_impaired") var hearing_impaired: Boolean? = null, + @JsonProperty("hearing_impaired") var hearingImpaired: Boolean? = null, ) data class ResultFiles( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 27975d19..e5db626b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -38,6 +38,7 @@ import java.security.SecureRandom import java.text.SimpleDateFormat import java.time.Instant import java.util.Date +import java.util.Locale import java.util.TimeZone import kotlin.time.Duration import kotlin.time.DurationUnit @@ -144,8 +145,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } companion object { - private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID - private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET + private const val CLIENT_ID: String = BuildConfig.SIMKL_CLIENT_ID + private const val CLIENT_SECRET: String = BuildConfig.SIMKL_CLIENT_SECRET private var lastLoginState = "" const val SIMKL_TOKEN_KEY: String = "simkl_token" @@ -154,10 +155,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time" /** 2014-09-01T09:10:11Z -> 1409562611 */ - private const val simklDateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + private const val SIMKL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'" fun getUnixTime(string: String?): Long? { return try { - SimpleDateFormat(simklDateFormat).apply { + SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply { this.timeZone = TimeZone.getTimeZone("UTC") }.parse( string ?: return null @@ -171,7 +172,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** 1409562611 -> 2014-09-01T09:10:11Z */ fun getDateTime(unixTime: Long?): String? { return try { - SimpleDateFormat(simklDateFormat).apply { + SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply { this.timeZone = TimeZone.getTimeZone("UTC") }.format( Date.from( @@ -208,7 +209,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { companion object { fun fromString(string: String): SimklListStatusType? { - return SimklListStatusType.values().firstOrNull { + return SimklListStatusType.entries.firstOrNull { it.originalName == string } } @@ -219,17 +220,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonInclude(JsonInclude.Include.NON_EMPTY) data class TokenRequest( @JsonProperty("code") val code: String, - @JsonProperty("client_id") val client_id: String = clientId, - @JsonProperty("client_secret") val client_secret: String = clientSecret, - @JsonProperty("redirect_uri") val redirect_uri: String = "$appString://simkl", - @JsonProperty("grant_type") val grant_type: String = "authorization_code" + @JsonProperty("client_id") val clientId: String = CLIENT_ID, + @JsonProperty("client_secret") val clientSecret: String = CLIENT_SECRET, + @JsonProperty("redirect_uri") val redirectUri: String = "$APP_STRING://simkl", + @JsonProperty("grant_type") val grantType: String = "authorization_code" ) data class TokenResponse( /** No expiration date */ - val access_token: String, - val token_type: String, - val scope: String + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("scope") val scope: String ) // ------------------- @@ -261,15 +262,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { // ------------------- data class ActivitiesResponse( - val all: String?, - val tv_shows: UpdatedAt, - val anime: UpdatedAt, - val movies: UpdatedAt, + @JsonProperty("all") val all: String?, + @JsonProperty("tv_shows") val tvShows: UpdatedAt, + @JsonProperty("anime") val anime: UpdatedAt, + @JsonProperty("movies") val movies: UpdatedAt, ) { data class UpdatedAt( - val all: String?, - val removed_from_list: String?, - val rated_at: String?, + @JsonProperty("all") val all: String?, + @JsonProperty("removed_from_list") val removedFromList: String?, + @JsonProperty("rated_at") val ratedAt: String?, ) } @@ -308,7 +309,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("title") val title: String?, @JsonProperty("year") val year: Int?, @JsonProperty("ids") val ids: Ids?, - @JsonProperty("total_episodes") val total_episodes: Int? = null, + @JsonProperty("total_episodes") val totalEpisodes: Int? = null, @JsonProperty("status") val status: String? = null, @JsonProperty("poster") val poster: String? = null, @JsonProperty("type") val type: String? = null, @@ -540,7 +541,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } debugPrint { "Requesting episodes from $url" } - return app.get(url, params = mapOf("client_id" to clientId)) + return app.get(url, params = mapOf("client_id" to CLIENT_ID)) .parsedSafe>()?.also { val cacheTime = if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value @@ -558,7 +559,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("seasons") seasons: List? = null, @JsonProperty("episodes") episodes: List? = null, @JsonProperty("rating") val rating: Int? = null, - @JsonProperty("rated_at") val rated_at: String? = null, + @JsonProperty("rated_at") val ratedAt: String? = null, ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -567,7 +568,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("year") year: Int?, @JsonProperty("ids") ids: Ids?, @JsonProperty("rating") val rating: Int, - @JsonProperty("rated_at") val rated_at: String? = getDateTime(unixTime) + @JsonProperty("rated_at") val ratedAt: String? = getDateTime(unixTime) ) : MediaObject(title, year, ids) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -576,7 +577,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("year") year: Int?, @JsonProperty("ids") ids: Ids?, @JsonProperty("to") val to: String, - @JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime) + @JsonProperty("watched_at") val watchedAt: String? = getDateTime(unixTime) ) : MediaObject(title, year, ids) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -631,24 +632,24 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } interface Metadata { - val last_watched_at: String? + val lastWatchedAt: String? val status: String? - val user_rating: Int? - val last_watched: String? - val watched_episodes_count: Int? - val total_episodes_count: Int? + val userRating: Int? + val lastWatched: String? + val watchedEpisodesCount: Int? + val totalEpisodesCount: Int? fun getIds(): ShowMetadata.Show.Ids fun toLibraryItem(): SyncAPI.LibraryItem } data class MovieMetadata( - override val last_watched_at: String?, - override val status: String, - override val user_rating: Int?, - override val last_watched: String?, - override val watched_episodes_count: Int?, - override val total_episodes_count: Int?, + @JsonProperty("last_watched_at") override val lastWatchedAt: String?, + @JsonProperty("status") override val status: String, + @JsonProperty("user_rating") override val userRating: Int?, + @JsonProperty("last_watched") override val lastWatched: String?, + @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, + @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, val movie: ShowMetadata.Show ) : Metadata { override fun getIds(): ShowMetadata.Show.Ids { @@ -660,10 +661,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.movie.title, "https://simkl.com/tv/${movie.ids.simkl}", movie.ids.simkl.toString(), - this.watched_episodes_count, - this.total_episodes_count, - this.user_rating?.times(10), - getUnixTime(last_watched_at) ?: 0, + this.watchedEpisodesCount, + this.totalEpisodesCount, + this.userRating?.times(10), + getUnixTime(lastWatchedAt) ?: 0, "Simkl", TvType.Movie, this.movie.poster?.let { getPosterUrl(it) }, @@ -675,12 +676,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } data class ShowMetadata( - @JsonProperty("last_watched_at") override val last_watched_at: String?, + @JsonProperty("last_watched_at") override val lastWatchedAt: String?, @JsonProperty("status") override val status: String, - @JsonProperty("user_rating") override val user_rating: Int?, - @JsonProperty("last_watched") override val last_watched: String?, - @JsonProperty("watched_episodes_count") override val watched_episodes_count: Int?, - @JsonProperty("total_episodes_count") override val total_episodes_count: Int?, + @JsonProperty("user_rating") override val userRating: Int?, + @JsonProperty("last_watched") override val lastWatched: String?, + @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, + @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, @JsonProperty("show") val show: Show ) : Metadata { override fun getIds(): Show.Ids { @@ -692,10 +693,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.show.title, "https://simkl.com/tv/${show.ids.simkl}", show.ids.simkl.toString(), - this.watched_episodes_count, - this.total_episodes_count, - this.user_rating?.times(10), - getUnixTime(last_watched_at) ?: 0, + this.watchedEpisodesCount, + this.totalEpisodesCount, + this.userRating?.times(10), + getUnixTime(lastWatchedAt) ?: 0, "Simkl", TvType.Anime, this.show.poster?.let { getPosterUrl(it) }, @@ -749,7 +750,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { chain.request() .newBuilder() .addHeader("Authorization", "Bearer $token") - .addHeader("simkl-api-key", clientId) + .addHeader("simkl-api-key", CLIENT_ID) .build() ) } @@ -810,7 +811,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { val episodeConstructor = SimklEpisodeConstructor( searchResult.ids?.simkl, searchResult.type, - searchResult.total_episodes, + searchResult.totalEpisodes, searchResult.hasEnded() ) @@ -832,12 +833,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ) } ?: return null, - score = foundItem.user_rating, - watchedEpisodes = foundItem.watched_episodes_count, - maxEpisodes = searchResult.total_episodes, + score = foundItem.userRating, + watchedEpisodes = foundItem.watchedEpisodesCount, + maxEpisodes = searchResult.totalEpisodes, episodeConstructor = episodeConstructor, - oldEpisodes = foundItem.watched_episodes_count ?: 0, - oldScore = foundItem.user_rating, + oldEpisodes = foundItem.watchedEpisodesCount ?: 0, + oldScore = foundItem.userRating, oldStatus = foundItem.status ) } else { @@ -845,7 +846,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { status = SyncWatchType.fromInternalId(SimklListStatusType.None.value), score = 0, watchedEpisodes = 0, - maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes, + maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.totalEpisodes, episodeConstructor = episodeConstructor, oldEpisodes = 0, oldStatus = null, @@ -891,12 +892,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ - suspend fun searchByIds(serviceMap: Map): Array? { + private suspend fun searchByIds(serviceMap: Map): Array? { if (serviceMap.isEmpty()) return emptyArray() return app.get( "$mainUrl/search/id", - params = mapOf("client_id" to clientId) + serviceMap.map { (service, id) -> + params = mapOf("client_id" to CLIENT_ID) + serviceMap.map { (service, id) -> service.originalName to id } ).parsedSafe() @@ -904,14 +905,14 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun search(name: String): List? { return app.get( - "$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name) + "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name) ).parsedSafe>()?.mapNotNull { it.toSyncSearchResult() } } override fun authenticate(activity: FragmentActivity?) { lastLoginState = BigInteger(130, SecureRandom()).toString(32) val url = - "https://simkl.com/oauth/authorize?response_type=code&client_id=$clientId&redirect_uri=$appString://${redirectUrl}&state=$lastLoginState" + "https://simkl.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}&state=$lastLoginState" openBrowser(url, activity) } @@ -961,15 +962,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { val activities = getActivities() val lastCacheUpdate = getKey(accountId, SIMKL_CACHED_LIST_TIME) val lastRemoval = listOf( - activities?.tv_shows?.removed_from_list, - activities?.anime?.removed_from_list, - activities?.movies?.removed_from_list + activities?.tvShows?.removedFromList, + activities?.anime?.removedFromList, + activities?.movies?.removedFromList ).maxOf { getUnixTime(it) ?: -1 } val lastRealUpdate = listOf( - activities?.tv_shows?.all, + activities?.tvShows?.all, activities?.anime?.all, activities?.movies?.all, ).maxOf { @@ -1039,7 +1040,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun getDevicePin(): OAuth2API.PinAuthData? { val pinAuthResp = app.get( - "$mainUrl/oauth/pin?client_id=$clientId&redirect_uri=$appString://${redirectUrl}" + "$mainUrl/oauth/pin?client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}" ).parsedSafe() ?: return null return OAuth2API.PinAuthData( @@ -1053,7 +1054,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean { val pinAuthResp = app.get( - "$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$clientId" + "$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$CLIENT_ID" ).parsedSafe() ?: return false if (pinAuthResp.accessToken != null) { @@ -1088,7 +1089,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ).parsedSafe() ?: return false switchToNewAccount() - setKey(accountId, SIMKL_TOKEN_KEY, token.access_token) + setKey(accountId, SIMKL_TOKEN_KEY, token.accessToken) val user = getUser() if (user == null) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt index 0e233ece..8dad1f88 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt @@ -59,6 +59,7 @@ class SubSourceApi : AbstractSubProvider { it?.subs?.filter { sub -> sub.releaseName!!.contains( String.format( + null, "E%02d", query.epNumber ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index a075cc2e..9150cfc5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -50,7 +50,7 @@ class APIRepository(val api: MainAPI) { private val cache = threadSafeListOf() private var cacheIndex: Int = 0 - const val cacheSize = 20 + const val CACHE_SIZE = 20 } private fun afterPluginsLoaded(forceReload: Boolean) { @@ -94,9 +94,9 @@ class APIRepository(val api: MainAPI) { val add = SavedLoadResponse(unixTime, response, lookingForHash) synchronized(cache) { - if (cache.size > cacheSize) { + if (cache.size > CACHE_SIZE) { cache[cacheIndex] = add // rolling cache - cacheIndex = (cacheIndex + 1) % cacheSize + cacheIndex = (cacheIndex + 1) % CACHE_SIZE } else { cache.add(add) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt index d90177f5..e930961c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt @@ -112,6 +112,7 @@ abstract class BaseAdapter< holder.onViewDetachedFromWindow() } + @Suppress("UNCHECKED_CAST") fun save(recyclerView: RecyclerView) { for (child in recyclerView.children) { val holder = @@ -124,6 +125,7 @@ abstract class BaseAdapter< stateViewModel.layoutManagerStates[id]?.clear() } + @Suppress("UNCHECKED_CAST") private fun getState(holder: ViewHolderState): S? = stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 6bafa975..1eaac505 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -6,6 +6,7 @@ import android.view.Menu import android.view.View.* import android.widget.* import androidx.appcompat.app.AlertDialog +import androidx.media3.common.util.UnstableApi import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule @@ -263,6 +264,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi var isLoadingMore = false + override fun onMediaStatusUpdated() { super.onMediaStatusUpdated() val meta = getCurrentMetaData() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt index 1a9549e1..78ad2a6b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt @@ -8,8 +8,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, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt index c7041776..4879d2e0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt @@ -51,7 +51,7 @@ class EasterEggMonke : AppCompatActivity() { FrameLayout.LayoutParams.WRAP_CONTENT) binding.frame.addView(newStar) - newStar.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX + newStar.scaleX += Math.random().toFloat() * 1.5f newStar.scaleY = newStar.scaleX starW *= newStar.scaleX starH *= newStar.scaleY diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt index f721401e..12a5ae2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt @@ -15,7 +15,7 @@ open class NonFinalAdapterListUpdateCallback /** * Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter. * - * @param adapter The Adapter to send updates to. + * @param mAdapter The Adapter to send updates to. */(private var mAdapter: RecyclerView.Adapter<*>) : ListUpdateCallback { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt index 9532d1a9..b778ba5a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt @@ -13,7 +13,7 @@ enum class WatchType(val internalId: Int, @StringRes val stringRes: Int, @Drawab NONE(5, R.string.type_none, R.drawable.ic_baseline_add_24); companion object { - fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE + fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE } } @@ -36,6 +36,6 @@ enum class SyncWatchType(val internalId: Int, @StringRes val stringRes: Int, @Dr REWATCHING(5, R.string.type_re_watching, R.drawable.ic_baseline_bookmark_24); companion object { - fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE + fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt index 15e66b38..5e2b97e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt @@ -8,8 +8,10 @@ import android.webkit.JavascriptInterface import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient +import androidx.annotation.OptIn import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import androidx.media3.common.util.UnstableApi import androidx.navigation.fragment.findNavController import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.USER_AGENT @@ -29,6 +31,7 @@ class WebviewFragment : Fragment() { } binding?.webView?.webViewClient = object : WebViewClient() { + @OptIn(UnstableApi::class) override fun shouldOverrideUrlLoading( view: WebView?, request: WebResourceRequest? diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index f10e103e..45132131 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -54,6 +54,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } init { + @Suppress("LeakingThis") resetViewData() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt index d97a4b88..20a44461 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt @@ -13,7 +13,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper class DownloadButton(context: Context, attributeSet: AttributeSet) : PieFetchButton(context, attributeSet) { - var mainText: TextView? = null + private var mainText: TextView? = null override fun onAttachedToWindow() { super.onAttachedToWindow() progressText = findViewById(R.id.result_movie_download_text_precentage) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index ebed901f..b25486eb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout +import com.lagradost.cloudstream3.utils.UIHelper.isBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState(view) { @@ -54,7 +54,7 @@ class HomeChildItemAdapter( var hasNext: Boolean = false override fun onCreateContent(parent: ViewGroup): ViewHolderState { - val expanded = parent.context.IsBottomLayout() + val expanded = parent.context.isBottomLayout() /* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid val root = LayoutInflater.from(parent.context).inflate(layout, parent, false) @@ -133,7 +133,6 @@ class HomeChildItemAdapter( item, position, holder.itemView, - null, // nextFocusBehavior, nextFocusUp, nextFocusDown ) 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 82a92d80..49de2503 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 @@ -17,7 +17,6 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.* import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -234,7 +233,7 @@ class HomeFragment : Fragment() { return bottomSheetDialogBuilder } - fun getPairList( + private fun getPairList( anime: Chip?, cartoons: Chip?, tvs: Chip?, 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 916cb9ae..8bc0aa28 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 @@ -1,6 +1,8 @@ package com.lagradost.cloudstream3.ui.home +import android.os.Build import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -53,12 +55,12 @@ open class ParentItemAdapter( "value", recyclerView?.layoutManager?.onSaveInstanceState() ) - (recyclerView?.adapter as? BaseAdapter<*,*>)?.save(recyclerView) + (recyclerView?.adapter as? BaseAdapter<*, *>)?.save(recyclerView) } override fun restore(state: Bundle) { (binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState( - state.getParcelable("value") + state.getSafeParcelable("value") ) } } @@ -169,4 +171,9 @@ open class ParentItemAdapter( submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) } .toMutableList()) } -} \ No newline at end of file +} + +@Suppress("DEPRECATION") +inline fun Bundle.getSafeParcelable(key: String): T? = + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelable(key) + else getParcelable(key, T::class.java) \ No newline at end of file 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 2e98dd1f..339ef1e1 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 @@ -117,15 +117,12 @@ class HomeParentItemAdapterPreview( } override fun restore(state: Bundle) { - state.getParcelable("resumeRecyclerView")?.let { recycle -> + state.getSafeParcelable("resumeRecyclerView")?.let { recycle -> resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - state.getParcelable("bookmarkRecyclerView")?.let { recycle -> + state.getSafeParcelable("bookmarkRecyclerView")?.let { recycle -> bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - //state.getInt("previewViewpager").let { recycle -> - // previewViewpager.setCurrentItem(recycle,true) - //} } val previewAdapter = HomeScrollAdapter(fragment = fragment) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 9e70d088..24ca4df2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -152,7 +152,7 @@ class HomeViewModel : ViewModel() { } }?.distinctBy { it.first } ?: return@launchSafe - val length = WatchType.values().size + val length = WatchType.entries.size val currentWatchTypes = mutableSetOf() for (watch in watchStatusIds) { @@ -387,7 +387,9 @@ class HomeViewModel : ViewModel() { } is Resource.Failure -> { + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") _page.postValue(data!!) + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") _preview.postValue(data!!) } @@ -397,9 +399,7 @@ class HomeViewModel : ViewModel() { } fun click(callback: SearchClickCallback) { - if (callback.action == SEARCH_ACTION_FOCUSED) { - //focusCallback(callback.card) - } else { + if (callback.action != SEARCH_ACTION_FOCUSED) { SearchHelper.handleSearchClickCallback(callback) } } @@ -516,7 +516,7 @@ class HomeViewModel : ViewModel() { } else { _page.postValue(Resource.Loading()) if (preferredApiName != null) - _apiName.postValue(preferredApiName) + _apiName.postValue(preferredApiName!!) } } else { // if the api is found, then set it to it and save key 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 index 7144de09..5b240693 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -600,8 +600,4 @@ class LibraryFragment : Fragment() { } } -class MenuSearchView(context: Context) : SearchView(context) { - override fun onActionViewCollapsed() { - super.onActionViewCollapsed() - } -} \ No newline at end of file +class MenuSearchView(context: Context) : SearchView(context) \ 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 index cfd22220..0110187f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.BaseAdapter import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.home.getSafeParcelable import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -32,7 +33,7 @@ class ViewpagerAdapterViewHolderState(val binding: LibraryViewpagerPageBinding) } override fun restore(state: Bundle) { - state.getParcelable("pageRecyclerview")?.let { recycle -> + state.getSafeParcelable("pageRecyclerview")?.let { recycle -> binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 9d838c97..88c34c87 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -25,6 +25,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.media3.common.PlaybackException +import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import androidx.media3.ui.* @@ -216,7 +217,7 @@ abstract class AbstractPlayerFragment( return } player.handleEvent( - CSPlayerEvent.values()[intent.getIntExtra( + CSPlayerEvent.entries[intent.getIntExtra( EXTRA_CONTROL_TYPE, 0 )], source = PlayerEventSource.UI @@ -603,12 +604,12 @@ abstract class AbstractPlayerFragment( } fun nextResize() { - resizeMode = (resizeMode + 1) % PlayerResize.values().size + resizeMode = (resizeMode + 1) % PlayerResize.entries.size resize(resizeMode, true) } fun resize(resize: Int, showToast: Boolean) { - resize(PlayerResize.values()[resize], showToast) + resize(PlayerResize.entries[resize], showToast) } @SuppressLint("UnsafeOptInUsageError") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 735e4095..86d67b28 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -9,7 +9,11 @@ import android.os.Looper import android.util.Log import android.util.Rational import android.widget.FrameLayout -import androidx.media3.common.C.* +import androidx.annotation.OptIn +import androidx.media3.common.C.TIME_UNSET +import androidx.media3.common.C.TRACK_TYPE_AUDIO +import androidx.media3.common.C.TRACK_TYPE_TEXT +import androidx.media3.common.C.TRACK_TYPE_VIDEO import androidx.media3.common.Format import androidx.media3.common.MediaItem import androidx.media3.common.MimeTypes @@ -19,9 +23,10 @@ import androidx.media3.common.TrackGroup import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.Tracks import androidx.media3.common.VideoSize +import androidx.media3.common.util.UnstableApi import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.datasource.DataSource -import androidx.media3.datasource.DefaultDataSourceFactory +import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.HttpDataSource import androidx.media3.datasource.cache.CacheDataSource @@ -66,7 +71,6 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File -import java.lang.IllegalArgumentException import java.util.UUID import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext @@ -84,7 +88,7 @@ const val toleranceBeforeUs = 300_000L * seek position, in microseconds. Must be non-negative. */ const val toleranceAfterUs = 300_000L - +@OptIn(UnstableApi::class) class CS3IPlayer : IPlayer { private var isPlaying = false private var exoPlayer: ExoPlayer? = null @@ -257,7 +261,6 @@ class CS3IPlayer : IPlayer { private var currentSubtitles: SubtitleData? = null - @SuppressLint("UnsafeOptInUsageError") private fun List.getTrack(id: String?): Pair? { if (id == null) return null // This beast of an expression does: @@ -342,7 +345,6 @@ class CS3IPlayer : IPlayer { }.flatten() } - @SuppressLint("UnsafeOptInUsageError") private fun Tracks.Group.getFormats(): List> { return (0 until this.mediaTrackGroup.length).mapNotNull { i -> if (this.isSupported) @@ -371,7 +373,6 @@ class CS3IPlayer : IPlayer { ) } - @SuppressLint("UnsafeOptInUsageError") override fun getVideoTracks(): CurrentTracks { val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList() val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO } @@ -391,7 +392,6 @@ class CS3IPlayer : IPlayer { /** * @return True if the player should be reloaded * */ - @SuppressLint("UnsafeOptInUsageError") override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean { Log.i(TAG, "setPreferredSubtitles init $subtitle") currentSubtitles = subtitle @@ -451,7 +451,7 @@ class CS3IPlayer : IPlayer { } ?: false } - var currentSubtitleOffset: Long = 0 + private var currentSubtitleOffset: Long = 0 override fun setSubtitleOffset(offset: Long) { currentSubtitleOffset = offset @@ -459,7 +459,7 @@ class CS3IPlayer : IPlayer { } override fun getSubtitleOffset(): Long { - return currentSubtitleOffset //currentTextRenderer?.getRenderOffsetMs() ?: currentSubtitleOffset + return currentSubtitleOffset } override fun getCurrentPreferredSubtitle(): SubtitleData? { @@ -470,7 +470,6 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") override fun getAspectRatio(): Rational? { return exoPlayer?.videoFormat?.let { format -> Rational(format.width, format.height) @@ -481,14 +480,13 @@ class CS3IPlayer : IPlayer { subtitleHelper.setSubStyle(style) } - @SuppressLint("UnsafeOptInUsageError") override fun saveData() { Log.i(TAG, "saveData") updatedTime() exoPlayer?.let { exo -> playbackPosition = exo.currentPosition - currentWindow = exo.currentWindowIndex + currentWindow = exo.currentMediaItemIndex isPlaying = exo.isPlaying } } @@ -500,7 +498,7 @@ class CS3IPlayer : IPlayer { updatedTime() exoPlayer?.apply { - setPlayWhenReady(false) + playWhenReady = false stop() release() } @@ -563,7 +561,6 @@ class CS3IPlayer : IPlayer { var requestSubtitleUpdate: (() -> Unit)? = null - @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(headers: Map): HttpDataSource.Factory { val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT) return source.apply { @@ -571,7 +568,6 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory { val provider = getApiFromNameNull(link.source) val interceptor = provider?.getVideoInterceptor(link) @@ -604,53 +600,10 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") private fun Context.createOfflineSource(): DataSource.Factory { - return DefaultDataSourceFactory(this, USER_AGENT) + return DefaultDataSource.Factory(this, DefaultHttpDataSource.Factory().setUserAgent(USER_AGENT)) } - /*private fun getSubSources( - onlineSourceFactory: DataSource.Factory?, - offlineSourceFactory: DataSource.Factory?, - subHelper: PlayerSubtitleHelper, - ): Pair, List> { - val activeSubtitles = ArrayList() - val subSources = subHelper.getAllSubtitles().mapNotNull { sub -> - val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url)) - .setMimeType(sub.mimeType) - .setLanguage("_${sub.name}") - .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) - .build() - when (sub.origin) { - SubtitleOrigin.DOWNLOADED_FILE -> { - if (offlineSourceFactory != null) { - activeSubtitles.add(sub) - SingleSampleMediaSource.Factory(offlineSourceFactory) - .createMediaSource(subConfig, C.TIME_UNSET) - } else { - null - } - } - SubtitleOrigin.URL -> { - if (onlineSourceFactory != null) { - activeSubtitles.add(sub) - SingleSampleMediaSource.Factory(onlineSourceFactory) - .createMediaSource(subConfig, C.TIME_UNSET) - } else { - null - } - } - SubtitleOrigin.OPEN_SUBTITLES -> { - // TODO - throw NotImplementedError() - } - } - } - println("SUBSRC: ${subSources.size} activeSubtitles : ${activeSubtitles.size} of ${subHelper.getAllSubtitles().size} ") - return Pair(subSources, activeSubtitles) - }*/ - - @SuppressLint("UnsafeOptInUsageError") private fun getCache(context: Context, cacheSize: Long): SimpleCache? { return try { val databaseProvider = StandaloneDatabaseProvider(context) @@ -682,7 +635,6 @@ class CS3IPlayer : IPlayer { return getMediaItemBuilder(mimeType).setUri(url).build() } - @SuppressLint("UnsafeOptInUsageError") private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector { val trackSelector = DefaultTrackSelector(context) trackSelector.parameters = trackSelector.buildUponParameters() @@ -696,7 +648,6 @@ class CS3IPlayer : IPlayer { var currentTextRenderer: CustomTextRenderer? = null - @SuppressLint("UnsafeOptInUsageError") private fun buildExoPlayer( context: Context, mediaItemSlices: List, @@ -736,7 +687,7 @@ class CS3IPlayer : IPlayer { textRendererOutput, eventHandler.looper, CustomSubtitleDecoderFactory() - ).also { this.currentTextRenderer = it } + ).also { renderer -> this.currentTextRenderer = renderer } currentTextRenderer } else it }.toTypedArray() @@ -1033,7 +984,7 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") + //fixme: Use onPlaybackStateChanged(int) and onPlayWhenReadyChanged(boolean, int) instead. override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { exoPlayer?.let { exo -> event( @@ -1169,7 +1120,6 @@ class CS3IPlayer : IPlayer { private var lastTimeStamps: List = emptyList() - @SuppressLint("UnsafeOptInUsageError") override fun addTimeStamps(timeStamps: List) { lastTimeStamps = timeStamps timeStamps.forEach { timestamp -> @@ -1187,7 +1137,6 @@ class CS3IPlayer : IPlayer { updatedTime(source = PlayerEventSource.Player) } - @SuppressLint("UnsafeOptInUsageError") fun onRenderFirst() { if (hasUsedFirstRender) { // this insures that we only call this once per player load return @@ -1254,7 +1203,6 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") private fun getSubSources( onlineSourceFactory: HttpDataSource.Factory?, offlineSourceFactory: DataSource.Factory?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt index 20d093a6..07ce413e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player import android.content.Context import android.util.Log +import androidx.annotation.OptIn import androidx.preference.PreferenceManager import androidx.media3.common.Format import androidx.media3.common.MimeTypes @@ -31,7 +32,7 @@ import java.nio.charset.Charset * @param fallbackFormat used to create a decoder based on mimetype if the subtitle string is not * enough to identify the subtitle format. **/ -@UnstableApi +@OptIn(UnstableApi::class) class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { companion object { fun updateForcedEncoding(context: Context) { @@ -72,7 +73,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { RegexOption.IGNORE_CASE ), ) - val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\d\s]*?[])}]\s*""")) + val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\s]*?[])}]\s*""")) //https://emptycharacter.com/ //https://www.fileformat.info/info/unicode/char/200b/index.htm @@ -262,7 +263,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { } /** See https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java */ -@UnstableApi +@OptIn(UnstableApi::class) class CustomSubtitleDecoderFactory : SubtitleDecoderFactory { override fun supportsFormat(format: Format): Boolean { // return SubtitleDecoderFactory.DEFAULT.supportsFormat(format) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt index d6b0735d..f2b863fb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt @@ -1,11 +1,12 @@ package com.lagradost.cloudstream3.ui.player import android.os.Looper +import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.text.SubtitleDecoderFactory import androidx.media3.exoplayer.text.TextOutput -@UnstableApi +@OptIn(UnstableApi::class) class CustomTextRenderer( offset: Long, output: TextOutput?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index 3b242172..a8a3106a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -49,7 +49,7 @@ class DownloadFileGenerator( return null } - fun cleanDisplayName(name: String): String { + private fun cleanDisplayName(name: String): String { return name.substringBeforeLast('.').trim() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 92ef279d..4279b542 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -8,14 +8,10 @@ import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.safefile.SafeFile import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri -const val DTAG = "PlayerActivity" - class DownloadedPlayerActivity : AppCompatActivity() { private val dTAG = "DownloadedPlayerAct" 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 ef7d6bc1..b2e80749 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 @@ -25,6 +25,7 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHO import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.view.animation.AnimationUtils +import androidx.annotation.OptIn import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.core.graphics.blue @@ -35,6 +36,7 @@ import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged +import androidx.media3.common.util.UnstableApi import androidx.preference.PreferenceManager import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.CommonActivity.keyEventListener @@ -50,7 +52,6 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvid import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout @@ -245,6 +246,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { fadeAnimation.duration = 100 fadeAnimation.fillAfter = true + @OptIn(UnstableApi::class) val sView = subView val sStyle = subStyle if (sView != null && sStyle != null) { @@ -300,42 +302,40 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun restoreOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation - var orientation = 0 - when (currentOrientation) { + val orientation = when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - - Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> - orientation = dynamicOrientation() + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE Configuration.ORIENTATION_PORTRAIT -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + + else -> dynamicOrientation() } activity.requestedOrientation = orientation } private fun toggleOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation - var orientation = 0 - when (currentOrientation) { + val orientation: Int = when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT - - Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> - orientation = dynamicOrientation() + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT Configuration.ORIENTATION_PORTRAIT -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + + else -> dynamicOrientation() } activity.requestedOrientation = orientation } open fun lockOrientation(activity: Activity) { - val display = + @Suppress("DEPRECATION") + val display = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + else activity.display!! val rotation = display.rotation val currentOrientation = activity.resources.configuration.orientation - var orientation = 0 + val orientation: Int when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> orientation = @@ -344,15 +344,14 @@ open class FullScreenPlayer : AbstractPlayerFragment() { else ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE - Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> - orientation = dynamicOrientation() - Configuration.ORIENTATION_PORTRAIT -> orientation = if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT + + else -> orientation = dynamicOrientation() } activity.requestedOrientation = orientation } @@ -1167,6 +1166,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { return true } + @SuppressLint("GestureBackNavigation") private fun handleKeyEvent(event: KeyEvent, hasNavigated: Boolean): Boolean { if (hasNavigated) { autoHide() @@ -1581,7 +1581,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } // cs3 is peak media center - setRemainingTimeCounter(durationMode || Globals.isLayout(Globals.TV)) + setRemainingTimeCounter(durationMode || isLayout(TV)) playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ -> updateRemainingTime() } 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 1f7cc5bd..8e8f6bf5 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 @@ -6,6 +6,7 @@ import android.app.Dialog import android.content.Context import android.content.Intent import android.content.res.ColorStateList +import android.os.Build import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -13,6 +14,7 @@ import android.view.View import android.view.ViewGroup import android.widget.* import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.OptIn import androidx.core.animation.addListener import androidx.core.content.ContextCompat import androidx.core.view.isGone @@ -21,6 +23,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import androidx.media3.common.Format.NO_VALUE import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -63,6 +66,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.safefile.SafeFile import kotlinx.coroutines.Job +import java.io.Serializable import java.util.* import kotlin.math.abs @@ -234,7 +238,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun closestQuality(target: Int?): Qualities { if (target == null) return Qualities.Unknown - return Qualities.values().minBy { abs(it.value - target) } + return Qualities.entries.minBy { abs(it.value - target) } } private fun getLinkPriority( @@ -367,8 +371,6 @@ class GeneratorPlayer : FullScreenPlayer() { binding.subtitleAdapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE binding.subtitleAdapter.adapter = arrayAdapter - val adapter = - binding.subtitleAdapter.adapter as? ArrayAdapter binding.subtitleAdapter.setOnItemClickListener { _, _, position, _ -> currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener @@ -379,8 +381,8 @@ class GeneratorPlayer : FullScreenPlayer() { fun setSubtitlesList(list: List) { currentSubtitles = list - adapter?.clear() - adapter?.addAll(currentSubtitles) + arrayAdapter.clear() + arrayAdapter.addAll(currentSubtitles) } val currentTempMeta = getMetaData() @@ -522,7 +524,7 @@ class GeneratorPlayer : FullScreenPlayer() { //TODO: Set year text from currently loaded movie on Player //dialog.subtitles_search_year?.setText(currentTempMeta.year) } - + @OptIn(UnstableApi::class) private fun openSubPicker() { try { subsPathPicker.launch( @@ -795,7 +797,6 @@ class GeneratorPlayer : FullScreenPlayer() { settingsManager.edit().putString( ctx.getString(R.string.subtitles_encoding_key), prefValues[it] ).apply() - updateForcedEncoding(ctx) dismiss() player.seekTime(-1) // to update subtitles, a dirty trick @@ -1290,7 +1291,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun unwrapBundle(savedInstanceState: Bundle?) { Log.i(TAG, "unwrapBundle = $savedInstanceState") savedInstanceState?.let { bundle -> - sync.addSyncs(bundle.getSerializable("syncData") as? HashMap?) + sync.addSyncs(bundle.getSafeSerializable>("syncData")) } } @@ -1507,3 +1508,6 @@ class GeneratorPlayer : FullScreenPlayer() { } } } + +@Suppress("DEPRECATION") +inline fun Bundle.getSafeSerializable(key: String) : T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getSerializable(key) as? T else getSerializable(key, T::class.java) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index 5f7161f7..89c6f73b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -8,7 +8,6 @@ import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink enum class PlayerEventType(val value: Int) { - //Stop(-1), Pause(0), Play(1), SeekForward(2), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index 89e3c8de..07ea56dd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -4,7 +4,6 @@ import android.net.Uri import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.loadExtractor diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java index 3482f21c..232440cc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java @@ -29,6 +29,7 @@ import android.os.Message; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.text.Cue; @@ -66,7 +67,7 @@ import java.util.stream.Collectors; * obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s * is delegated to a {@link TextOutput}. */ -@UnstableApi +@OptIn(markerClass = UnstableApi.class) public class NonFinalTextRenderer extends BaseRenderer implements Callback { private static final String TAG = "TextRenderer"; @@ -74,7 +75,7 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { /** * @param trackType The track type that the renderer handles. One of the {@link C} {@code * TRACK_TYPE_*} constants. - * @param outputHandler + * @param outputHandler todo description */ public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) { super(trackType); @@ -416,13 +417,11 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_UPDATE_OUTPUT: - invokeUpdateOutputInternal((List) msg.obj); - return true; - default: - throw new IllegalStateException(); + if (msg.what == MSG_UPDATE_OUTPUT) { + invokeUpdateOutputInternal((List) msg.obj); + return true; } + throw new IllegalStateException(); } private void invokeUpdateOutputInternal(List cues) { @@ -441,7 +440,6 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { } ).collect(Collectors.toList()); - output.onCues(fixedCues); output.onCues(new CueGroup(fixedCues, 0L)); } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt index e6de1266..f00f8a61 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt @@ -4,8 +4,8 @@ import android.app.Activity import android.content.ContentUris import android.net.Uri import androidx.core.content.ContextCompat.getString +import androidx.media3.common.util.UnstableApi import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.safefile.SafeFile diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 1ba5a29f..122eaa97 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.launch class PlayerGeneratorViewModel : ViewModel() { companion object { - val TAG = "PlayViewGen" + const val TAG = "PlayViewGen" } private var generator: IGenerator? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index 25d7e3dd..02a7ee03 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -4,7 +4,9 @@ import android.util.Log import android.util.TypedValue import android.view.ViewGroup import android.widget.FrameLayout +import androidx.annotation.OptIn import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat @@ -47,6 +49,7 @@ data class SubtitleData( } } +@OptIn(UnstableApi::class) class PlayerSubtitleHelper { private var activeSubtitles: Set = emptySet() private var allSubtitles: Set = emptySet() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index 7c78ce63..2d1feaab 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -239,7 +239,11 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG // generated images 1:1 to idx of hsl private var images: Array = arrayOf() - private val TAG = "PreviewImgM3u8" + companion object { + private const val TAG = "PreviewImgM3u8" + } + + // prefixSum[i] = sum(hsl.ts[0..i].time) // where [0] = 0, [1] = hsl.ts[0].time aka time at start of segment, do [b] - [a] for range a,b @@ -388,13 +392,6 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG logError(t) continue } - - /* - val buffer = hsl.resolveLinkSafe(index) ?: continue - tmpFile?.writeBytes(buffer) - val buff = FileOutputStream(tmpFile) - retriever.setDataSource(buff.fd) - val frame = retriever.getFrameAtTime(0L)*/ } } @@ -412,14 +409,16 @@ private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGe null } + companion object { + private const val TAG = "PreviewImgMp4" + } + override fun hasPreview(): Boolean { synchronized(images) { return loadedLod >= MIN_LOD } } - val TAG = "PreviewImgMp4" - override fun getPreviewImage(fraction: Float): Bitmap? { synchronized(images) { if (loadedLod < MIN_LOD) { @@ -524,7 +523,7 @@ private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGe val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) Log.i(TAG, "Generating preview for ${fraction * 100}%") val frame = durationUs * fraction - val img = retriever.image(frame.toLong(), params); + val img = retriever.image(frame.toLong(), params) if (!scope.isActive) return if (img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt index 90bd1ca7..6943c641 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.player import android.util.Log +import androidx.media3.common.util.UnstableApi import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.LoadResponse diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt index 1e2c9f67..ce457740 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt @@ -17,7 +17,6 @@ class PriorityAdapter(override val items: MutableList>) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return PriorityViewHolder( PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false), - //LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false) ) } @@ -31,10 +30,6 @@ class PriorityAdapter(override val items: MutableList>) : val binding: PlayerPrioritizeItemBinding, ) : RecyclerView.ViewHolder(binding.root) { fun bind(item: SourcePriority) { - /* val plusButton: ImageView = itemView.add_button - val subtractButton: ImageView = itemView.subtract_button - val priorityText: TextView = itemView.priority_text - val priorityNumber: TextView = itemView.priority_number*/ binding.priorityText.text = item.name fun updatePriority() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt index b587276f..45f6aa66 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt @@ -29,8 +29,6 @@ class ProfilesAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProfilesViewHolder( PlayerQualityProfileItemBinding.inflate(LayoutInflater.from(parent.context),parent,false) - //LayoutInflater.from(parent.context) - // .inflate(R.layout.player_quality_profile_item, parent, false) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt index 96249db4..3267efd7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.player.source_priority -import android.content.Context import androidx.annotation.StringRes import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey @@ -104,7 +103,7 @@ object QualityDataHelper { * Must under all circumstances at least return one profile **/ fun getProfiles(): List { - val availableTypes = QualityProfileType.values().toMutableList() + val availableTypes = QualityProfileType.entries.toMutableList() val profiles = (1..PROFILE_COUNT).map { profileNumber -> // Get the real type val type = getQualityProfileType(profileNumber) @@ -140,12 +139,12 @@ object QualityDataHelper { } } - QualityProfileType.values().forEach { + QualityProfileType.entries.forEach { if (it.unique) insertType(profiles, it) } debugAssert({ - !QualityProfileType.values().all { type -> + !QualityProfileType.entries.all { type -> !type.unique || profiles.any { it.type == type } } }, { "All unique quality types do not exist" }) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt index e3629158..0537092c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt @@ -65,7 +65,7 @@ class QualityProfileDialog( setDefaultBtt.setOnClickListener { val currentProfile = getCurrentProfile() ?: return@setOnClickListener - val choices = QualityDataHelper.QualityProfileType.values() + val choices = QualityDataHelper.QualityProfileType.entries .filter { it != QualityDataHelper.QualityProfileType.None } val choiceNames = choices.map { txt(it.stringRes).asString(context) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt index 1b59882e..bc6282af 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt @@ -47,7 +47,7 @@ class SourcePriorityDialog( ) qualitiesRecyclerView.adapter = PriorityAdapter( - Qualities.values().mapNotNull { + Qualities.entries.mapNotNull { SourcePriority( it, Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 61188905..0ca326dd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -3,8 +3,6 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.annotation.IdRes -import androidx.annotation.LayoutRes import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -70,8 +68,7 @@ class ActorAdaptor( } } - private inner class CardViewHolder - constructor( + private inner class CardViewHolder( val binding: CastItemBinding, private val focusCallback: (View?) -> Unit = {} ) : diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 06be6bd5..d12521b3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -169,8 +169,7 @@ class EpisodeAdapter( return cardList.size } - class EpisodeCardViewHolderLarge - constructor( + class EpisodeCardViewHolderLarge( val binding: ResultEpisodeLargeBinding, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, @@ -335,8 +334,7 @@ class EpisodeAdapter( } } - class EpisodeCardViewHolderSmall - constructor( + class EpisodeCardViewHolderSmall( val binding: ResultEpisodeBinding, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt index 7b7bae43..eecd6262 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt @@ -8,18 +8,6 @@ import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -/* -class ImageAdapter(context: Context, val resource: Int) : ArrayAdapter(context, resource) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val newConvertView = convertView ?: run { - val mInflater = context - .getSystemService(Activity.LAYOUT_INFLATER_SERVICE) as LayoutInflater - mInflater.inflate(resource, null) - } - getItem(position)?.let { (newConvertView as? ImageView?)?.setImageResource(it) } - return newConvertView - } -}*/ const val IMAGE_CLICK = 0 const val IMAGE_LONG_CLICK = 1 @@ -66,8 +54,7 @@ class ImageAdapter( diffResult.dispatchUpdatesTo(this) } - class ImageViewHolder - constructor(val binding: ResultMiniImageBinding) : + class ImageViewHolder(val binding: ResultMiniImageBinding) : RecyclerView.ViewHolder(binding.root) { fun bind( img: Int, 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 2f297098..f1399e8d 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 @@ -78,11 +78,12 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.VideoDownloadHelper open class ResultFragmentPhone : FullScreenPlayer() { - private val gestureRegionsListener = object : PanelsChildGestureRegionObserver.GestureRegionsListener { - override fun onGestureRegionsUpdate(gestureRegions: List) { - binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) + private val gestureRegionsListener = + object : PanelsChildGestureRegionObserver.GestureRegionsListener { + override fun onGestureRegionsUpdate(gestureRegions: List) { + binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) + } } - } protected lateinit var viewModel: ResultViewModel2 protected lateinit var syncModel: SyncViewModel @@ -336,7 +337,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { } - // ===== ===== ===== resultBinding?.apply { @@ -430,16 +430,16 @@ open class ResultFragmentPhone : FullScreenPlayer() { if (newStatus == null) return@toggleSubscriptionStatus val message = if (newStatus) { - // Kinda icky to have this here, but it works. - SubscriptionWorkManager.enqueuePeriodicWork(context) - R.string.subscription_new - } else { - R.string.subscription_deleted - } + // Kinda icky to have this here, but it works. + SubscriptionWorkManager.enqueuePeriodicWork(context) + R.string.subscription_new + } else { + R.string.subscription_deleted + } - val name = (viewModel.page.value as? Resource.Success)?.value?.title - ?: txt(R.string.no_data).asStringNull(context) ?: "" - showToast(txt(message, name), Toast.LENGTH_SHORT) + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + showToast(txt(message, name), Toast.LENGTH_SHORT) } context?.let { openBatteryOptimizationSettings(it) } } @@ -473,8 +473,16 @@ open class ResultFragmentPhone : FullScreenPlayer() { if (act.isCastApiAvailable()) { try { CastButtonFactory.setUpMediaRouteButton(act, this) - val castContext = CastContext.getSharedInstance(act.applicationContext) - isGone = castContext.castState == CastState.NO_DEVICES_AVAILABLE + CastContext.getSharedInstance(act.applicationContext) { + it.run() + }.addOnCompleteListener { + isGone = if (it.isSuccessful) { + it.result.castState == CastState.NO_DEVICES_AVAILABLE + } else { + true + } + + } // this shit leaks for some reason //castContext.addCastStateListener { state -> // media_route_button?.isGone = state == CastState.NO_DEVICES_AVAILABLE @@ -961,12 +969,12 @@ open class ResultFragmentPhone : FullScreenPlayer() { setOnClickListener { fab -> activity?.showBottomDialog( - WatchType.values().map { fab.context.getString(it.stringRes) }.toList(), + WatchType.entries.map { fab.context.getString(it.stringRes) }.toList(), watchType.ordinal, fab.context.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - viewModel.updateWatchStatus(WatchType.values()[it], context) + viewModel.updateWatchStatus(WatchType.entries[it], context) } } } @@ -1046,7 +1054,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { text?.asStringNull(ctx) ?: return@mapNotNull null ) }) { - viewModel.changeDubStatus(DubStatus.values()[itemId]) + viewModel.changeDubStatus(DubStatus.entries[itemId]) } } } @@ -1103,7 +1111,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { override fun onPause() { super.onPause() - PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(gestureRegionsListener) + PanelsChildGestureRegionObserver.Provider.get() + .addGestureRegionsUpdateListener(gestureRegionsListener) } private fun setRecommendations(rec: List?, validApiName: String?) { 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 a0207060..1878f0b8 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 @@ -56,7 +56,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage class ResultFragmentTv : Fragment() { - protected lateinit var viewModel: ResultViewModel2 + private lateinit var viewModel: ResultViewModel2 private var binding: FragmentResultTvBinding? = null override fun onDestroyView() { @@ -418,10 +418,6 @@ class ResultFragmentTv : Fragment() { resultCastItems.layoutManager = object : LinearListLayout(view.context) { - override fun onInterceptFocusSearch(focused: View, direction: Int): View? { - return super.onInterceptFocusSearch(focused, direction) - } - override fun onRequestChildFocus( parent: RecyclerView, state: RecyclerView.State, @@ -649,7 +645,7 @@ class ResultFragmentTv : Fragment() { binding?.apply { - (data as? Resource.Success)?.value?.let { (text, ep) -> + (data as? Resource.Success)?.value?.let { (_, ep) -> resultPlayMovieButton.setOnClickListener { viewModel.handleAction( @@ -817,45 +813,8 @@ class ResultFragmentTv : Fragment() { } } - /* - * Okay so what is this fuckery? - * Basically Android TV will crash if you request a new focus while - * the adapter gets updated. - * - * This means that if you load thumbnails and request a next focus at the same time - * the app will crash without any way to catch it! - * - * How to bypass this? - * This code basically steals the focus for 500ms and puts it in an inescapable view - * then lets out the focus by requesting focus to result_episodes - */ - - val hasEpisodes = - !(resultEpisodes.adapter as? EpisodeAdapter?)?.cardList.isNullOrEmpty() - /*val focus = activity?.currentFocus - - if (hasEpisodes) { - // Make it impossible to focus anywhere else! - temporaryNoFocus.isFocusable = true - temporaryNoFocus.requestFocus() - }*/ (resultEpisodes.adapter as? EpisodeAdapter)?.updateList(episodes.value) - - /* if (hasEpisodes) main { - - delay(500) - // This might make some people sad as it changes the focus when leaving an episode :( - if(focus?.requestFocus() == true) { - temporaryNoFocus.isFocusable = false - return@main - } - temporaryNoFocus.isFocusable = false - temporaryNoFocus.requestFocus() - } - - if (hasNoFocus()) - binding?.resultEpisodes?.requestFocus()*/ } } } 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 ce0fbdc5..6443a923 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 @@ -2723,7 +2723,7 @@ class ResultViewModel2 : ViewModel() { val id: Int?, ) : LoadResponse - fun loadSmall(activity: Activity?, searchResponse: SearchResponse) = ioSafe { + fun loadSmall(searchResponse: SearchResponse) = ioSafe { val url = searchResponse.url _page.postValue(Resource.Loading(url)) _episodes.postValue(Resource.Loading()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt index 5a23bfc1..8752e275 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt @@ -63,8 +63,7 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter Unit, resView: AutofitRecyclerView diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt index 0a2ecb81..4ef5fa69 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt @@ -1,16 +1,11 @@ package com.lagradost.cloudstream3.ui.search import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.databinding.AccountSingleBinding import com.lagradost.cloudstream3.databinding.SearchHistoryItemBinding data class SearchHistoryItem( @@ -63,8 +58,7 @@ class SearchHistoryAdaptor( diffResult.dispatchUpdatesTo(this) } - class CardViewHolder - constructor( + class CardViewHolder( val binding: SearchHistoryItemBinding, private val clickCallback: (SearchHistoryCallback) -> Unit, ) : 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 f597132b..92575e58 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,5 +1,6 @@ package com.lagradost.cloudstream3.ui.search +import android.annotation.SuppressLint import android.content.Context import android.view.View import android.widget.ImageView @@ -37,16 +38,12 @@ object SearchResultBuilder { } } - /** - * @param nextFocusBehavior True if first, False if last, Null if between. - * Used to prevent escaping the adapter horizontally (focus wise). - */ + @SuppressLint("StringFormatInvalid") fun bind( clickCallback: (SearchClickCallback) -> Unit, card: SearchResponse, position: Int, itemView: View, - nextFocusBehavior: Boolean? = null, nextFocusUp: Int? = null, nextFocusDown: Int? = null, colorCallback : ((Palette) -> Unit)? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt index 9e03079f..71077e91 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt @@ -3,11 +3,9 @@ package com.lagradost.cloudstream3.ui.search import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis +//TODO Relevance of this class since it's not used class SyncSearchViewModel { - private val repos = SyncApis - data class SyncSearchResultSearchResponse( override val name: String, override val url: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt index 1dc79dc0..d7bd69f1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.settings +import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -13,7 +14,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage class AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.LoginInfo) class AccountAdapter( - val cardList: List, + private val cardList: List, private val clickCallback: (AccountClickCallback) -> Unit ) : RecyclerView.Adapter() { @@ -42,12 +43,12 @@ class AccountAdapter( return cardList[position].accountIndex.toLong() } - class CardViewHolder - constructor(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) : + class CardViewHolder(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) : RecyclerView.ViewHolder(binding.root) { // private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! // private val accountName: TextView = itemView.findViewById(R.id.account_name)!! + @SuppressLint("StringFormatInvalid") fun bind(card: AuthAPI.LoginInfo) { // just in case name is null account index will show, should never happened binding.accountName.text = card.name ?: "%s %d".format( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 6ba93c0f..88335eea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -26,7 +26,6 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper 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 fd61962c..7cb1a848 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 @@ -28,10 +28,8 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.ui.EasterEggMonke import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR -import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount -import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 7560d75f..21707ca7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -10,7 +10,6 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV -import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn @@ -108,7 +107,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { getPref(R.string.hide_player_control_names_key)?.hideOn(TV) getPref(R.string.quality_pref_key)?.setOnPreferenceClickListener { - val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() + val prefValues = Qualities.entries.map { it.value }.reversed().toMutableList() prefValues.remove(Qualities.Unknown.value) val prefNames = prefValues.map { Qualities.getStringByInt(it) } @@ -116,7 +115,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { val currentQuality = settingsManager.getInt( getString(R.string.quality_pref_key), - Qualities.values().last().value + Qualities.entries.last().value ) activity?.showBottomDialog( @@ -132,7 +131,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { } getPref(R.string.quality_pref_mobile_data_key)?.setOnPreferenceClickListener { - val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() + val prefValues = Qualities.entries.map { it.value }.reversed().toMutableList() prefValues.remove(Qualities.Unknown.value) val prefNames = prefValues.map { Qualities.getStringByInt(it) } @@ -140,7 +139,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { val currentQuality = settingsManager.getInt( getString(R.string.quality_pref_mobile_data_key), - Qualities.values().last().value + Qualities.entries.last().value ) activity?.showBottomDialog( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index cfb46c39..cb7d25fd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -34,7 +34,7 @@ class SettingsProviders : PreferenceFragmentCompat() { getPref(R.string.display_sub_key)?.setOnPreferenceClickListener { activity?.getApiDubstatusSettings()?.let { current -> - val dublist = DubStatus.values() + val dublist = DubStatus.entries val names = dublist.map { it.name } val currentList = ArrayList() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index 4aaa5e12..260c6674 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -128,7 +128,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { } binding.saveBtt.setOnClickListener { - val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) + val date = SimpleDateFormat("yyyy_MM_dd_HH_mm", Locale.getDefault()).format(Date(currentTimeMillis())) var fileStream: OutputStream? = null try { fileStream = VideoDownloadManager.setupStream( @@ -169,10 +169,10 @@ class SettingsUpdates : PreferenceFragmentCompat() { prefValues.indexOf(currentInstaller), getString(R.string.apk_installer_settings), true, - {}) { + {}) { num -> try { settingsManager.edit() - .putInt(getString(R.string.apk_installer_key), prefValues[it]) + .putInt(getString(R.string.apk_installer_key), prefValues[num]) .apply() } catch (e: Exception) { logError(e) @@ -209,9 +209,9 @@ class SettingsUpdates : PreferenceFragmentCompat() { prefValues.indexOf(current), getString(R.string.automatic_plugin_download_mode_title), true, - {}) { + {}) { num -> settingsManager.edit() - .putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() + .putInt(getString(R.string.auto_download_plugins_key), prefValues[num]).apply() (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } return@setOnPreferenceClickListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index 909c30be..9fb3f282 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -1,9 +1,11 @@ package com.lagradost.cloudstream3.ui.settings.extensions +import android.annotation.SuppressLint import android.text.format.Formatter.formatShortFileSize import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup +import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible @@ -27,11 +29,10 @@ import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx -import org.junit.Assert -import org.junit.Test import java.text.DecimalFormat import kotlin.math.floor import kotlin.math.log10 +import kotlin.math.pow data class PluginViewData( @@ -95,21 +96,13 @@ class PluginAdapter( } companion object { - private tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { if (current >= max) return max if (current >= target) return current return findClosestBase2(target, current * 2, max) } - @Test - fun testFindClosestBase2() { - Assert.assertEquals(16, findClosestBase2(0)) - Assert.assertEquals(256, findClosestBase2(170)) - Assert.assertEquals(256, findClosestBase2(256)) - Assert.assertEquals(512, findClosestBase2(257)) - Assert.assertEquals(512, findClosestBase2(700)) - } - private val iconSizeExact = 32.toPx private val iconSize by lazy { findClosestBase2(iconSizeExact, 16, 512) @@ -122,10 +115,7 @@ class PluginAdapter( val base = value / 3 return if (value >= 3 && base < suffix.size) { DecimalFormat("#0.00").format( - numValue / Math.pow( - 10.0, - (base * 3).toDouble() - ) + numValue / 10.0.pow((base * 3).toDouble()) ) + suffix[base] } else { DecimalFormat().format(numValue) @@ -136,6 +126,7 @@ class PluginAdapter( inner class PluginViewHolder(val binding: RepositoryItemBinding) : RecyclerView.ViewHolder(binding.root) { + @SuppressLint("SetTextI18n") fun bind( data: PluginViewData, ) { 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 c5319c37..4878049b 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 @@ -190,7 +190,7 @@ class PluginsFragment : Fragment() { bindChips( binding?.tvtypesChipsScroll?.tvtypesChips, emptyList(), - TvType.values().toList(), + TvType.entries.toList(), callback = { list -> pluginViewModel.tvTypes.clear() pluginViewModel.tvTypes.addAll(list.map { it.name }) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt index 56014eb4..fd5422b2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt @@ -10,7 +10,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.CommonActivity.showToast -import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index f9197213..49a93608 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -10,7 +10,6 @@ import androidx.core.util.forEach import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager -import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt index bb9558b8..c76a218e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt @@ -15,8 +15,11 @@ import android.widget.Toast import androidx.fragment.app.Fragment import androidx.media3.common.text.Cue import com.fasterxml.jackson.annotation.JsonProperty -import com.google.android.gms.cast.TextTrackStyle -import com.google.android.gms.cast.TextTrackStyle.* +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DEPRESSED +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DROP_SHADOW +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_NONE +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_OUTLINE +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_RAISED import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent @@ -42,7 +45,7 @@ data class SaveChromeCaptionStyle( @JsonProperty("fontGenericFamily") var fontGenericFamily: Int? = null, @JsonProperty("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent @JsonProperty("edgeColor") var edgeColor: Int = Color.BLACK, // BLACK - @JsonProperty("edgeType") var edgeType: Int = TextTrackStyle.EDGE_TYPE_OUTLINE, + @JsonProperty("edgeType") var edgeType: Int = EDGE_TYPE_OUTLINE, @JsonProperty("foregroundColor") var foregroundColor: Int = Color.WHITE, @JsonProperty("fontScale") var fontScale: Float = 1.05f, @JsonProperty("windowColor") var windowColor: Int = Color.TRANSPARENT, @@ -99,7 +102,7 @@ class ChromecastSubtitlesFragment : Fragment() { } private fun onColorSelected(stuff: Pair) { - context?.setColor(stuff.first, stuff.second) + setColor(stuff.first, stuff.second) if (hide) activity?.hideSystemUI() } @@ -122,7 +125,7 @@ class ChromecastSubtitlesFragment : Fragment() { return if (color == Color.TRANSPARENT) Color.BLACK else color } - private fun Context.setColor(id: Int, color: Int?) { + private fun setColor(id: Int, color: Int?) { val realColor = color ?: getDefColor(id) when (id) { 0 -> state.foregroundColor = realColor @@ -135,7 +138,7 @@ class ChromecastSubtitlesFragment : Fragment() { updateState() } - private fun Context.updateState() { + private fun updateState() { //subtitle_text?.setStyle(fromSaveToStyle(state)) } @@ -173,7 +176,7 @@ class ChromecastSubtitlesFragment : Fragment() { fixPaddingStatusbar(binding?.subsRoot) state = getCurrentSavedStyle() - context?.updateState() + updateState() val isTvSettings = isLayout(TV or EMULATOR) @@ -195,7 +198,7 @@ class ChromecastSubtitlesFragment : Fragment() { } this.setOnLongClickListener { - it.context.setColor(id, null) + setColor(id, null) showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } @@ -247,13 +250,13 @@ class ChromecastSubtitlesFragment : Fragment() { dismissCallback ) { index -> state.edgeType = edgeTypes.map { it.first }[index] - textView.context.updateState() + updateState() } } binding?.subsEdgeType?.setOnLongClickListener { state.edgeType = defaultState.edgeType - it.context.updateState() + updateState() showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } @@ -323,12 +326,12 @@ class ChromecastSubtitlesFragment : Fragment() { dismissCallback ) { index -> state.fontFamily = fontTypes.map { it.first }[index] - textView.context.updateState() + updateState() } } - binding?.subsFont?.setOnLongClickListener { textView -> + binding?.subsFont?.setOnLongClickListener { _ -> state.fontFamily = defaultState.fontFamily - textView.context.updateState() + updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index 1466afed..8821905e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -14,11 +14,13 @@ import android.view.ViewGroup import android.widget.TextView import android.widget.Toast import androidx.annotation.FontRes +import androidx.annotation.OptIn import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty import androidx.media3.common.text.Cue +import androidx.media3.common.util.UnstableApi import androidx.media3.ui.CaptionStyleCompat import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -28,7 +30,6 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding -import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStore.setKey @@ -46,7 +47,7 @@ const val SUBTITLE_KEY = "subtitle_settings" const val SUBTITLE_AUTO_SELECT_KEY = "subs_auto_select" const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download" -data class SaveCaptionStyle( +data class SaveCaptionStyle @OptIn(UnstableApi::class) constructor( @JsonProperty("foregroundColor") var foregroundColor: Int, @JsonProperty("backgroundColor") var backgroundColor: Int, @JsonProperty("windowColor") var windowColor: Int, @@ -67,7 +68,7 @@ data class SaveCaptionStyle( const val DEF_SUBS_ELEVATION = 20 -@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) +@OptIn(androidx.media3.common.util.UnstableApi::class) class SubtitlesFragment : Fragment() { companion object { val applyStyleEvent = Event() @@ -167,7 +168,7 @@ class SubtitlesFragment : Fragment() { activity?.hideSystemUI() } - private fun onDialogDismissed(id: Int) { + private fun onDialogDismissed(@Suppress("UNUSED_PARAMETER") id: Int) { if (hide) activity?.hideSystemUI() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt index e9b69c5b..f0c948a4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt @@ -83,7 +83,7 @@ object EpisodeSkip { startMs = start, endMs = end ) - }?.let { list -> + }.let { list -> out.addAll(list) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index f0aae7bc..b13de062 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -43,7 +43,6 @@ 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 import com.google.android.gms.common.ConnectionResult @@ -58,7 +57,7 @@ import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEv import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.plugins.RepositoryManager -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WebviewFragment import com.lagradost.cloudstream3.ui.player.SubtitleData @@ -161,7 +160,7 @@ object AppContextUtils { .setTitle(title) .setPosterArtUri(Uri.parse(card.posterUrl)) .setIntentUri(Uri.parse(card.id?.let { - "$appStringResumeWatching://$it" + "$APP_STRING_RESUME_WATCHING://$it" } ?: card.url)) .setInternalProviderId(card.url) .setLastEngagementTimeUtcMillis( 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 802c1a64..b25be59f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -81,12 +81,12 @@ object BackupUtils { // Kinda hack, but I couldn't think of a better way data class BackupVars( - @JsonProperty("_Bool") val _Bool: Map?, - @JsonProperty("_Int") val _Int: Map?, - @JsonProperty("_String") val _String: Map?, - @JsonProperty("_Float") val _Float: Map?, - @JsonProperty("_Long") val _Long: Map?, - @JsonProperty("_StringSet") val _StringSet: Map?>?, + @JsonProperty("_Bool") val bool: Map?, + @JsonProperty("_Int") val int: Map?, + @JsonProperty("_String") val string: Map?, + @JsonProperty("_Float") val float: Map?, + @JsonProperty("_Long") val long: Map?, + @JsonProperty("_StringSet") val stringSet: Map?>?, ) data class BackupFile( @@ -134,21 +134,21 @@ object BackupUtils { ) { if (context == null) return if (restoreSettings) { - context.restoreMap(backupFile.settings._Bool, true) - context.restoreMap(backupFile.settings._Int, true) - context.restoreMap(backupFile.settings._String, true) - context.restoreMap(backupFile.settings._Float, true) - context.restoreMap(backupFile.settings._Long, true) - context.restoreMap(backupFile.settings._StringSet, true) + context.restoreMap(backupFile.settings.bool, true) + context.restoreMap(backupFile.settings.int, true) + context.restoreMap(backupFile.settings.string, true) + context.restoreMap(backupFile.settings.float, true) + context.restoreMap(backupFile.settings.long, true) + context.restoreMap(backupFile.settings.stringSet, true) } if (restoreDataStore) { - context.restoreMap(backupFile.datastore._Bool) - context.restoreMap(backupFile.datastore._Int) - context.restoreMap(backupFile.datastore._String) - context.restoreMap(backupFile.datastore._Float) - context.restoreMap(backupFile.datastore._Long) - context.restoreMap(backupFile.datastore._StringSet) + context.restoreMap(backupFile.datastore.bool) + context.restoreMap(backupFile.datastore.int) + context.restoreMap(backupFile.datastore.string) + context.restoreMap(backupFile.datastore.float) + context.restoreMap(backupFile.datastore.long) + context.restoreMap(backupFile.datastore.stringSet) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index 19c817b9..b5192aae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -56,16 +56,27 @@ data class Editor( ) { /** Always remember to call apply after */ fun setKeyRaw(path: String, value: T) { - when (value) { - is Boolean -> editor.putBoolean(path, value) - is Int -> editor.putInt(path, value) - is String -> editor.putString(path, value) - is Float -> editor.putFloat(path, value) - is Long -> editor.putLong(path, value) - (value as? Set != null) -> editor.putStringSet(path, value as Set) + @Suppress("UNCHECKED_CAST") + if (isStringSet(value)) { + editor.putStringSet(path, value as Set) + } else { + when (value) { + is Boolean -> editor.putBoolean(path, value) + is Int -> editor.putInt(path, value) + is String -> editor.putString(path, value) + is Float -> editor.putFloat(path, value) + is Long -> editor.putLong(path, value) + } } } + private fun isStringSet(value: Any?) : Boolean { + if (value is Set<*>) { + return value.filterIsInstance().size == value.size + } + return false + } + fun apply() { editor.apply() System.gc() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt index 421e4420..c92da214 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt @@ -7,7 +7,6 @@ import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_INFO diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index 89bb0031..59f534ff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -32,26 +32,26 @@ import java.io.InputStreamReader class InAppUpdater { companion object { - const val GITHUB_USER_NAME = "recloudstream" - const val GITHUB_REPO = "cloudstream" + private const val GITHUB_USER_NAME = "recloudstream" + private const val GITHUB_REPO = "cloudstream" - const val LOG_TAG = "InAppUpdater" + private const val LOG_TAG = "InAppUpdater" // === IN APP UPDATER === data class GithubAsset( @JsonProperty("name") val name: String, @JsonProperty("size") val size: Int, // Size bytes - @JsonProperty("browser_download_url") val browser_download_url: String, // download link - @JsonProperty("content_type") val content_type: String, // application/vnd.android.package-archive + @JsonProperty("browser_download_url") val browserDownloadUrl: String, // download link + @JsonProperty("content_type") val contentType: String, // application/vnd.android.package-archive ) data class GithubRelease( - @JsonProperty("tag_name") val tag_name: String, // Version code + @JsonProperty("tag_name") val tagName: String, // Version code @JsonProperty("body") val body: String, // Desc @JsonProperty("assets") val assets: List, - @JsonProperty("target_commitish") val target_commitish: String, // branch + @JsonProperty("target_commitish") val targetCommitish: String, // branch @JsonProperty("prerelease") val prerelease: Boolean, - @JsonProperty("node_id") val node_id: String //Node Id + @JsonProperty("node_id") val nodeId: String //Node Id ) data class GithubObject( @@ -61,7 +61,7 @@ class InAppUpdater { ) data class GithubTag( - @JsonProperty("object") val github_object: GithubObject, + @JsonProperty("object") val githubObject: GithubObject, ) data class Update( @@ -114,7 +114,7 @@ class InAppUpdater { response.filter { rel -> !rel.prerelease }.sortedWith(compareBy { release -> - release.assets.firstOrNull { it.content_type == "application/vnd.android.package-archive" }?.name?.let { it1 -> + release.assets.firstOrNull { it.contentType == "application/vnd.android.package-archive" }?.name?.let { it1 -> versionRegex.find( it1 )?.groupValues?.let { @@ -134,7 +134,7 @@ class InAppUpdater { foundAsset?.name?.let { assetName -> val foundVersion = versionRegex.find(assetName) val shouldUpdate = - if (foundAsset.browser_download_url != "" && foundVersion != null) currentVersion?.versionName?.let { versionName -> + if (foundAsset.browserDownloadUrl != "" && foundVersion != null) currentVersion?.versionName?.let { versionName -> versionRegexLocal.find(versionName)?.groupValues?.let { it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() } @@ -146,10 +146,10 @@ class InAppUpdater { return if (foundVersion != null) { Update( shouldUpdate, - foundAsset.browser_download_url, + foundAsset.browserDownloadUrl, foundVersion.groupValues[2], found.body, - found.node_id + found.nodeId ) } else { Update(false, null, null, null, null) @@ -168,33 +168,33 @@ class InAppUpdater { val found = response.lastOrNull { rel -> - rel.prerelease || rel.tag_name == "pre-release" + rel.prerelease || rel.tagName == "pre-release" } val foundAsset = found?.assets?.filter { it -> - it.content_type == "application/vnd.android.package-archive" + it.contentType == "application/vnd.android.package-archive" }?.getOrNull(0) val tagResponse = parseJson(app.get(tagUrl, headers = headers).text) - Log.d(LOG_TAG, "Fetched GitHub tag: ${tagResponse.github_object.sha.take(7)}") + Log.d(LOG_TAG, "Fetched GitHub tag: ${tagResponse.githubObject.sha.take(7)}") val shouldUpdate = (getString(R.string.commit_hash) .trim { c -> c.isWhitespace() } .take(7) != - tagResponse.github_object.sha + tagResponse.githubObject.sha .trim { c -> c.isWhitespace() } .take(7)) return if (foundAsset != null) { Update( shouldUpdate, - foundAsset.browser_download_url, - tagResponse.github_object.sha.take(10), + foundAsset.browserDownloadUrl, + tagResponse.githubObject.sha.take(10), found.body, - found.node_id + found.nodeId ) } else { Update(false, null, null, null, null) 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 bc81a5b9..4b3f02f1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt @@ -11,7 +11,6 @@ import android.os.Build import android.widget.Toast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.utils.Coroutines.main import java.io.InputStream @@ -57,7 +56,7 @@ class ApkInstaller(private val service: PackageInstallerService) { PackageInstaller.STATUS_FAILURE )) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT) + val userAction = intent.getSafeParcelableExtra(Intent.EXTRA_INTENT) userAction?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(userAction) } @@ -146,3 +145,5 @@ class ApkInstaller(private val service: PackageInstallerService) { } } +@Suppress("DEPRECATION") +inline fun Intent.getSafeParcelableExtra(key: String): T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelableExtra(key) else getParcelableExtra(key, T::class.java) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt index 27609730..0d3da8e7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt @@ -17,8 +17,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -const val packageName = BuildConfig.APPLICATION_ID -const val TAG = "PowerManagerAPI" +private const val PACKAGE_NAME = BuildConfig.APPLICATION_ID +private const val TAG = "PowerManagerAPI" object BatteryOptimizationChecker { @@ -72,7 +72,7 @@ object BatteryOptimizationChecker { val intent = Intent() try { intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - .setData(Uri.fromParts("package", packageName, null)) + .setData(Uri.fromParts("package", PACKAGE_NAME, null)) context.startActivity(intent, Bundle()) } catch (t: Throwable) { Log.e(TAG, "Unable to invoke any intent", t) 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 71d3a1ef..351e77c8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -73,8 +73,8 @@ object SyncUtil { val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text val mapped = parseJson(response) - val overrideMal = mapped?.malId ?: mapped?.Mal?.id ?: mapped?.Anilist?.malId - val overrideAnilist = mapped?.aniId ?: mapped?.Anilist?.id + val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId + val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id if (overrideMal != null) { return overrideMal.toString() to overrideAnilist?.toString() @@ -135,8 +135,8 @@ object SyncUtil { @JsonProperty("createdAt") val createdAt: String?, @JsonProperty("updatedAt") val updatedAt: String?, @JsonProperty("deletedAt") val deletedAt: String?, - @JsonProperty("Mal") val Mal: Mal?, - @JsonProperty("Anilist") val Anilist: Anilist?, + @JsonProperty("Mal") val mal: Mal?, + @JsonProperty("Anilist") val anilist: Anilist?, @JsonProperty("malUrl") val malUrl: String? ) 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 8670de53..ad1b6502 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -553,7 +553,7 @@ object UIHelper { return result } - fun Context?.IsBottomLayout(): Boolean { + fun Context?.isBottomLayout(): Boolean { if (this == null) return true val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) return settingsManager.getBoolean(getString(R.string.bottom_title_key), true) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 197bacc6..a3f6d789 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -293,6 +293,7 @@ object VideoDownloadManager { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) } else { + //fixme Specify a better flag PendingIntent.getActivity(context, 0, intent, 0) } builder.setContentIntent(pendingIntent) @@ -475,10 +476,10 @@ object VideoDownloadManager { } } - private const val reservedChars = "|\\?*<\":>+[]/\'" + private const val RESERVED_CHARS = "|\\?*<\":>+[]/\'" fun sanitizeFilename(name: String, removeSpaces: Boolean = false): String { var tempName = name - for (c in reservedChars) { + for (c in RESERVED_CHARS) { tempName = tempName.replace(c, ' ') } if (removeSpaces) tempName = tempName.replace(" ", "") @@ -1699,7 +1700,7 @@ object VideoDownloadManager { } */ fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? = - getDownloadFileInfo(context, id, removeKeys = true) + getDownloadFileInfo(context, id) private fun DownloadedFileInfo.toFile(context: Context): SafeFile? { return basePathToFile(context, this.basePath)?.gotoDirectory(relativePath) @@ -1709,7 +1710,6 @@ object VideoDownloadManager { private fun getDownloadFileInfo( context: Context, id: Int, - removeKeys: Boolean = false ): DownloadedFileInfoResult? { try { val info = diff --git a/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt index d4725d53..2aea0b8d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt @@ -19,7 +19,7 @@ class FlowLayout : ViewGroup { @SuppressLint("CustomViewStyleable") internal constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) { val t = c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout) - itemSpacing = t.getDimensionPixelSize(R.styleable.FlowLayout_Layout_itemSpacing, 0); + itemSpacing = t.getDimensionPixelSize(R.styleable.FlowLayout_Layout_itemSpacing, 0) t.recycle() } diff --git a/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt b/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt new file mode 100644 index 00000000..5dbf4d7c --- /dev/null +++ b/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt @@ -0,0 +1,16 @@ +package com.lagradost.cloudstream3 + +import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter.Companion.findClosestBase2 +import org.junit.Assert +import org.junit.Test + +class PluginAdapterTest { + @Test + fun testFindClosestBase2() { + Assert.assertEquals(16, findClosestBase2(0)) + Assert.assertEquals(256, findClosestBase2(170)) + Assert.assertEquals(256, findClosestBase2(256)) + Assert.assertEquals(512, findClosestBase2(257)) + Assert.assertEquals(512, findClosestBase2(700)) + } +} \ No newline at end of file From 82f8ab489e88145e0f498dc3537d26d59157b7dc Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 29 Jul 2024 00:58:35 +0200 Subject: [PATCH 547/570] Fix prerelease test function --- .../ui/settings/extensions/PluginAdapter.kt | 17 ++++++++++++++--- .../lagradost/cloudstream3/PluginAdapterTest.kt | 16 ---------------- 2 files changed, 14 insertions(+), 19 deletions(-) delete mode 100644 app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index 9fb3f282..d159539d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -33,7 +33,8 @@ import java.text.DecimalFormat import kotlin.math.floor import kotlin.math.log10 import kotlin.math.pow - +import org.junit.Test +import org.junit.Assert data class PluginViewData( val plugin: Plugin, @@ -96,13 +97,23 @@ class PluginAdapter( } companion object { - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { + private tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { if (current >= max) return max if (current >= target) return current return findClosestBase2(target, current * 2, max) } + // DO NOT MOVE, as running this test will result in ExceptionInInitializerError on prerelease due to static variables using Resources.getSystem() + // this test function is only to show how the function works + @Test + fun testFindClosestBase2() { + Assert.assertEquals(16, findClosestBase2(0)) + Assert.assertEquals(256, findClosestBase2(170)) + Assert.assertEquals(256, findClosestBase2(256)) + Assert.assertEquals(512, findClosestBase2(257)) + Assert.assertEquals(512, findClosestBase2(700)) + } + private val iconSizeExact = 32.toPx private val iconSize by lazy { findClosestBase2(iconSizeExact, 16, 512) diff --git a/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt b/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt deleted file mode 100644 index 5dbf4d7c..00000000 --- a/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.lagradost.cloudstream3 - -import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter.Companion.findClosestBase2 -import org.junit.Assert -import org.junit.Test - -class PluginAdapterTest { - @Test - fun testFindClosestBase2() { - Assert.assertEquals(16, findClosestBase2(0)) - Assert.assertEquals(256, findClosestBase2(170)) - Assert.assertEquals(256, findClosestBase2(256)) - Assert.assertEquals(512, findClosestBase2(257)) - Assert.assertEquals(512, findClosestBase2(700)) - } -} \ No newline at end of file From 150ad5fc9f4c90a8de8c42bd53b190a594606156 Mon Sep 17 00:00:00 2001 From: epireyn <48213068+epireyn@users.noreply.github.com> Date: Mon, 29 Jul 2024 01:00:44 +0200 Subject: [PATCH 548/570] Add sorting by release date (#1206) --- .../cloudstream3/syncproviders/SyncApi.kt | 6 +++++- .../syncproviders/providers/AniListApi.kt | 10 +++++++--- .../syncproviders/providers/LocalList.kt | 5 +++++ .../syncproviders/providers/MALApi.kt | 9 +++++++++ .../syncproviders/providers/SimklApi.kt | 7 ++++++- .../ui/library/LibraryViewModel.kt | 4 +++- .../cloudstream3/utils/DataStoreHelper.kt | 20 +++++++++++++++---- app/src/main/res/values/strings.xml | 2 ++ 8 files changed, 53 insertions(+), 10 deletions(-) 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 878e0cb3..dcb8bbea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt @@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.UiText import me.xdrop.fuzzywuzzy.FuzzySearch +import java.util.Date interface SyncAPI : OAuth2API { /** @@ -124,6 +125,8 @@ interface SyncAPI : OAuth2API { ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed() ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) } ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime } + ListSorting.ReleaseDateNew -> items.sortedByDescending { it.releaseDate } + ListSorting.ReleaseDateOld -> items.sortedBy { it.releaseDate } else -> items } } @@ -158,9 +161,10 @@ interface SyncAPI : OAuth2API { override var posterUrl: String?, override var posterHeaders: Map?, override var quality: SearchQuality?, + val releaseDate: Date?, override var id: Int? = null, val plot : String? = null, val rating: Int? = null, - val tags: List? = null, + val tags: List? = null ) : SearchResponse } \ 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 e51d3d65..6112c7db 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 @@ -16,15 +16,16 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType 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.AppContextUtils.splitQuery +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject +import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear import java.net.URL import java.net.URLEncoder -import java.util.* +import java.util.Locale class AniListApi(index: Int) : AccountManager(index), SyncAPI { override var name = "AniList" @@ -631,8 +632,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ?: this.media.coverImage.medium, null, null, + this.media.seasonYear.toYear(), null, - plot = this.media.description + plot = this.media.description, ) } } @@ -689,6 +691,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) 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 index f819cd3b..0d9a4d13 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -119,6 +119,11 @@ class LocalList : SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, +// ListSorting.RatingHigh, +// ListSorting.RatingLow, + ) ) } 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 6046a0f2..08c18653 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 @@ -27,6 +27,7 @@ import java.security.SecureRandom import java.text.ParseException import java.text.SimpleDateFormat import java.time.Instant +import java.time.format.DateTimeFormatter import java.util.Calendar import java.util.Date import java.util.Locale @@ -448,6 +449,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { null, null, plot = this.node.synopsis, + releaseDate = if (this.node.startDate == null) null else try {Date.from( + Instant.from( + DateTimeFormatter.ofPattern(if (this.node.startDate.length == 4) "yyyy" else if (this.node.startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd") + .parse(this.node.startDate) + ) + )} catch (_: RuntimeException) {null} ) } } @@ -512,6 +519,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index e5db626b..50517f9d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -31,6 +31,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear import okhttp3.Interceptor import okhttp3.Response import java.math.BigInteger @@ -670,7 +671,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.movie.poster?.let { getPosterUrl(it) }, null, null, - movie.ids.simkl, + this.movie.year?.toYear(), + movie.ids.simkl ) } } @@ -702,6 +704,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.show.poster?.let { getPosterUrl(it) }, null, null, + this.show.year?.toYear(), show.ids.simkl ) } @@ -1027,6 +1030,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) 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 index 1bd01c86..6c602e6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -23,6 +23,8 @@ enum class ListSorting(@StringRes val stringRes: Int) { UpdatedOld(R.string.sort_updated_old), AlphabeticalA(R.string.sort_alphabetical_a), AlphabeticalZ(R.string.sort_alphabetical_z), + ReleaseDateNew(R.string.sort_release_date_new), + ReleaseDateOld(R.string.sort_release_date_old), } const val LAST_SYNC_API_KEY = "last_sync_api" @@ -132,4 +134,4 @@ class LibraryViewModel : ViewModel() { MainActivity.reloadLibraryEvent -= ::reloadPages super.onCleared() } -} \ No newline at end of file +} 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 43124a53..2fa5f6a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -2,8 +2,8 @@ package com.lagradost.cloudstream3.utils import android.content.Context import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys @@ -11,6 +11,13 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.DubStatus +import com.lagradost.cloudstream3.EpisodeResponse +import com.lagradost.cloudstream3.MainActivity +import com.lagradost.cloudstream3.R +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 @@ -18,6 +25,9 @@ import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia +import java.util.Calendar +import java.util.Date +import java.util.GregorianCalendar import kotlin.reflect.KClass import kotlin.reflect.KProperty @@ -195,6 +205,8 @@ object DataStoreHelper { return this } + fun Int.toYear() : Date = GregorianCalendar.getInstance().also { it.set(Calendar.YEAR, this) }.time + /** * Used to display notifications on new episodes and posters in library. **/ @@ -242,7 +254,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } @@ -273,7 +285,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } @@ -304,7 +316,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21067fff..37a3f993 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -797,6 +797,8 @@ Can\'t get the device PIN code, try local authentication PIN code is now expired ! Code expires in %1$dm %2$ds + Release Date (New to Old) + Release Date (Old to New) hide_player_control_names_key Hide names of the player\'s controls \ No newline at end of file From b2f08847e1dc736aa54a26c84d8f71e8ea83c166 Mon Sep 17 00:00:00 2001 From: epireyn <48213068+epireyn@users.noreply.github.com> Date: Mon, 29 Jul 2024 01:01:45 +0200 Subject: [PATCH 549/570] Add system dark theme (#1208) --- app/src/main/AndroidManifest.xml | 2 +- .../lagradost/cloudstream3/CommonActivity.kt | 28 +++++++++++++++++-- .../lagradost/cloudstream3/MainActivity.kt | 2 ++ .../cloudstream3/ui/settings/SettingsUI.kt | 14 +++++++--- app/src/main/res/values-es/array.xml | 2 ++ app/src/main/res/values-pl/array.xml | 2 ++ app/src/main/res/values-tr/array.xml | 2 ++ app/src/main/res/values-vi/array.xml | 2 ++ app/src/main/res/values/array.xml | 2 ++ 9 files changed, 49 insertions(+), 7 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a23ef725..888be999 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -97,7 +97,7 @@ --> = Build.VERSION_CODES.Q) { + loadThemes(act) + } + } + + private fun mapSystemTheme(act: Activity): Int { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val currentNightMode = + act.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return when (currentNightMode) { + Configuration.UI_MODE_NIGHT_NO -> R.style.LightMode // Night mode is not active, we're using the light theme + else -> R.style.AppTheme // Night mode is active, we're using dark theme + } + } else { + return R.style.AppTheme + } + } + fun loadThemes(act: Activity?) { if (act == null) return val settingsManager = PreferenceManager.getDefaultSharedPreferences(act) val currentTheme = when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) { + "System" -> mapSystemTheme(act) "Black" -> R.style.AppTheme "Light" -> R.style.LightMode "Amoled" -> R.style.AmoledMode @@ -352,8 +376,8 @@ object CommonActivity { currentLook = currentLook.parent as? View ?: break }*/ - private fun View.hasContent() : Boolean { - return isShown && when(this) { + private fun View.hasContent(): Boolean { + return isShown && when (this) { //is RecyclerView -> this.childCount > 0 is ViewGroup -> this.childCount > 0 else -> true diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index eed69a50..b59265ee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -68,6 +68,7 @@ import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.setActivityInstance import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale +import com.lagradost.cloudstream3.CommonActivity.updateTheme import com.lagradost.cloudstream3.databinding.ActivityMainBinding import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding @@ -484,6 +485,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) updateLocale() // android fucks me by chaining lang when rotating the phone + updateTheme(this) // Update if system theme val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt index cc14e761..8c3ad0ad 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt @@ -88,10 +88,9 @@ class SettingsUI : PreferenceFragmentCompat() { getPref(R.string.app_theme_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.themes_names).toMutableList() val prefValues = resources.getStringArray(R.array.themes_names_values).toMutableList() - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less + val removeIncompatible = { text: String -> val toRemove = prefValues - .mapIndexed { idx, s -> if (s.startsWith("Monet")) idx else null } + .mapIndexed { idx, s -> if (s.startsWith(text)) idx else null } .filterNotNull() var offset = 0 toRemove.forEach { idx -> @@ -100,6 +99,12 @@ class SettingsUI : PreferenceFragmentCompat() { offset += 1 } } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less + removeIncompatible("Monet") + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { // Remove system on android 9 and less + removeIncompatible("System") + } val currentLayout = settingsManager.getString(getString(R.string.app_theme_key), prefValues.first()) @@ -123,7 +128,8 @@ class SettingsUI : PreferenceFragmentCompat() { } getPref(R.string.primary_color_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.themes_overlay_names).toMutableList() - val prefValues = resources.getStringArray(R.array.themes_overlay_names_values).toMutableList() + val prefValues = + resources.getStringArray(R.array.themes_overlay_names_values).toMutableList() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less val toRemove = prefValues diff --git a/app/src/main/res/values-es/array.xml b/app/src/main/res/values-es/array.xml index 05d49f98..eb197f43 100644 --- a/app/src/main/res/values-es/array.xml +++ b/app/src/main/res/values-es/array.xml @@ -247,6 +247,7 @@ Gris Amoled Destello + Sistema Material You @@ -254,6 +255,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values-pl/array.xml b/app/src/main/res/values-pl/array.xml index 9f76f423..a43d7bcf 100644 --- a/app/src/main/res/values-pl/array.xml +++ b/app/src/main/res/values-pl/array.xml @@ -256,6 +256,7 @@ Szary Amoled Flashbang + System Material You @@ -263,6 +264,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values-tr/array.xml b/app/src/main/res/values-tr/array.xml index 5c723f72..22a94ebf 100644 --- a/app/src/main/res/values-tr/array.xml +++ b/app/src/main/res/values-tr/array.xml @@ -281,6 +281,7 @@ Gri Amoled Flaş Bombası + Sistem Material You @@ -288,6 +289,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values-vi/array.xml b/app/src/main/res/values-vi/array.xml index aac94100..f363befd 100644 --- a/app/src/main/res/values-vi/array.xml +++ b/app/src/main/res/values-vi/array.xml @@ -248,6 +248,7 @@ Xám Amoled Sáng + Hệ thống Material You @@ -255,6 +256,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index 3be12510..03715faf 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -318,6 +318,7 @@ Gray Amoled Flashbang + System Material You @@ -325,6 +326,7 @@ Black Amoled Light + System Monet From 63e27c2ea5c7c57d41846038678c38654e28e495 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 30 Jul 2024 21:16:11 +0300 Subject: [PATCH 550/570] Fix Trailers on API<33 (#1226) Recent NewPipeExtractor updates pushed minimum sdk to 33 which needs desugar_jdk_libs_nio --- app/build.gradle.kts | 8 ++++---- library/build.gradle.kts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2040cf39..ee6cda6c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -140,7 +140,7 @@ android { abortOnError = false checkReleaseBuilds = false } - + buildFeatures { buildConfig = true } @@ -200,7 +200,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:2d36945") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:176da72") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding @@ -213,7 +213,7 @@ dependencies { implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors implementation("androidx.tvprovider:tvprovider:1.0.0") implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures - implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication + implementation("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview implementation("io.github.g0dkar:qrcode-kotlin:4.2.0") // QR code for PIN Auth on TV @@ -223,7 +223,7 @@ dependencies { implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9 implementation("com.uwetrottmann.tmdb2:tmdb-java:2.11.0") // TMDB API v3 Wrapper Made with RetroFit - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.0.4") //nio flavor needed for NewPipeExtractor implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API Level 25 or Less. */ diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 516e1ee9..00bc3c14 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -27,7 +27,7 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") implementation("me.xdrop:fuzzywuzzy:1.4.0") // Match extractors implementation("org.mozilla:rhino:1.7.15") // run JavaScript - implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") + implementation("com.github.teamnewpipe:NewPipeExtractor:176da72") } } } From 30adb1cd9d8aa7d538d027fb4e9506cd31224869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Tue, 30 Jul 2024 21:38:51 +0300 Subject: [PATCH 551/570] fixed: Test Search & VidMoxy, RapidVid extractors (#1219) --- .../cloudstream3/utils/TestingUtils.kt | 5 ++- .../extractors/ContentXExtractor.kt | 41 +++++++++---------- .../extractors/HDMomPlayerExtractor.kt | 21 +++++----- .../extractors/HDPlayerSystemExtractor.kt | 23 +++++------ .../extractors/MailRuExtractor.kt | 21 ++++------ .../extractors/OdnoklassnikiExtractor.kt | 18 ++++---- .../extractors/PeaceMakerstExtractor.kt | 37 ++++++++--------- .../extractors/RapidVidExtractor.kt | 37 ++++++++++------- .../extractors/SibNetExtractor.kt | 11 +++-- .../cloudstream3/extractors/TRsTXExtractor.kt | 30 +++++++------- .../extractors/TauVideoExtractor.kt | 11 +++-- .../extractors/VidMoxyExtractor.kt | 37 ++++++++++------- .../extractors/VideoSeyredExtractor.kt | 13 +++--- 13 files changed, 155 insertions(+), 150 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index 5e2b2bc1..049f92fb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -4,6 +4,7 @@ import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* import org.junit.Assert +import kotlin.random.Random object TestingUtils { open class TestResult(val success: Boolean) { @@ -280,8 +281,8 @@ object TestingUtils { // Test Search Results val searchQueries = - // Use the first 3 home page results as queries since they are guaranteed to exist - (homePageList.take(3).map { it.name } + + // Use the random 3 home page results as queries since they are guaranteed to exist + (homePageList.shuffled(Random).take(3).map { it.name.split(" ").first() } + // If home page is sparse then use generic search queries listOf("over", "iron", "guy")).take(3) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt index 27a5c52a..13a717b6 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt @@ -12,53 +12,52 @@ open class ContentX : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - Log.d("Kekik_${this.name}", "url » ${url}") + val extRef = referer ?: "" - val i_source = app.get(url, referer=ext_ref).text - val i_extract = Regex("""window\.openPlayer\('([^']+)'""").find(i_source)!!.groups[1]?.value ?: throw ErrorLoadingException("i_extract is null") + val iSource = app.get(url, referer=extRef).text + val iExtract = Regex("""window\.openPlayer\('([^']+)'""").find(iSource)!!.groups[1]?.value ?: throw ErrorLoadingException("iExtract is null") - val sub_urls = mutableSetOf() - Regex("""\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(i_source).forEach { - val (sub_url, sub_lang) = it.destructured + val subUrls = mutableSetOf() + Regex("""\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(iSource).forEach { + val (subUrl, subLang) = it.destructured - if (sub_url in sub_urls) { return@forEach } - sub_urls.add(sub_url) + if (subUrl in subUrls) { return@forEach } + subUrls.add(subUrl) subtitleCallback.invoke( SubtitleFile( - lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), - url = fixUrl(sub_url.replace("\\", "")) + lang = subLang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(subUrl.replace("\\", "")) ) ) } - val vid_source = app.get("${mainUrl}/source2.php?v=${i_extract}", referer=ext_ref).text - val vid_extract = Regex("""file\":\"([^\"]+)""").find(vid_source)!!.groups[1]?.value ?: throw ErrorLoadingException("vid_extract is null") - val m3u_link = vid_extract.replace("\\", "") + val vidSource = app.get("${mainUrl}/source2.php?v=${iExtract}", referer=extRef).text + val vidExtract = Regex("""file\":\"([^\"]+)""").find(vidSource)!!.groups[1]?.value ?: throw ErrorLoadingException("vidExtract is null") + val m3uLink = vidExtract.replace("\\", "") callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = m3u_link, + url = m3uLink, referer = url, quality = Qualities.Unknown.value, isM3u8 = true ) ) - val i_dublaj = Regex(""",\"([^']+)\",\"Türkçe""").find(i_source)!!.groups[1]?.value - if (i_dublaj != null) { - val dublaj_source = app.get("${mainUrl}/source2.php?v=${i_dublaj}", referer=ext_ref).text - val dublaj_extract = Regex("""file\":\"([^\"]+)""").find(dublaj_source)!!.groups[1]?.value ?: throw ErrorLoadingException("dublaj_extract is null") - val dublaj_link = dublaj_extract.replace("\\", "") + val iDublaj = Regex(""",\"([^']+)\",\"Türkçe""").find(iSource)!!.groups[1]?.value + if (iDublaj != null) { + val dublajSource = app.get("${mainUrl}/source2.php?v=${iDublaj}", referer=extRef).text + val dublajExtract = Regex("""file\":\"([^\"]+)""").find(dublajSource)!!.groups[1]?.value ?: throw ErrorLoadingException("dublajExtract is null") + val dublajLink = dublajExtract.replace("\\", "") callback.invoke( ExtractorLink( source = "${this.name} Türkçe Dublaj", name = "${this.name} Türkçe Dublaj", - url = dublaj_link, + url = dublajLink, referer = url, quality = Qualities.Unknown.value, isM3u8 = true diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt index 1f70ce61..1152cb4b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt @@ -16,24 +16,23 @@ open class HDMomPlayer : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val m3u_link:String? - val ext_ref = referer ?: "" - val i_source = app.get(url, referer=ext_ref).text + val m3uLink:String? + val extRef = referer ?: "" + val iSource = app.get(url, referer=extRef).text - val bePlayer = Regex("""bePlayer\('([^']+)',\s*'(\{[^\}]+\})'\);""").find(i_source)?.groupValues + val bePlayer = Regex("""bePlayer\('([^']+)',\s*'(\{[^\}]+\})'\);""").find(iSource)?.groupValues if (bePlayer != null) { val bePlayerPass = bePlayer.get(1) val bePlayerData = bePlayer.get(2) val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") - Log.d("Kekik_${this.name}", "encrypted » ${encrypted}") - m3u_link = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1) + m3uLink = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1) } else { - m3u_link = Regex("""file:\"([^\"]+)""").find(i_source)?.groupValues?.get(1) + m3uLink = Regex("""file:\"([^\"]+)""").find(iSource)?.groupValues?.get(1) - val track_str = Regex("""tracks:\[([^\]]+)""").find(i_source)?.groupValues?.get(1) - if (track_str != null) { - val tracks:List = jacksonObjectMapper().readValue("[${track_str}]") + val trackStr = Regex("""tracks:\[([^\]]+)""").find(iSource)?.groupValues?.get(1) + if (trackStr != null) { + val tracks:List = jacksonObjectMapper().readValue("[${trackStr}]") for (track in tracks) { if (track.file == null || track.label == null) continue @@ -53,7 +52,7 @@ open class HDMomPlayer : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = m3u_link ?: throw ErrorLoadingException("m3u link not found"), + url = m3uLink ?: throw ErrorLoadingException("m3u link not found"), referer = url, quality = Qualities.Unknown.value, isM3u8 = true diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt index 8318c3fb..e3cf3aee 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt @@ -13,37 +13,36 @@ open class HDPlayerSystem : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val vid_id = if (url.contains("video/")) { + val extRef = referer ?: "" + val vidId = if (url.contains("video/")) { url.substringAfter("video/") } else { url.substringAfter("?data=") } - val post_url = "${mainUrl}/player/index.php?data=${vid_id}&do=getVideo" - Log.d("Kekik_${this.name}", "post_url » ${post_url}") + val postUrl = "${mainUrl}/player/index.php?data=${vidId}&do=getVideo" val response = app.post( - post_url, + postUrl, data = mapOf( - "hash" to vid_id, - "r" to ext_ref + "hash" to vidId, + "r" to extRef ), - referer = ext_ref, + referer = extRef, headers = mapOf( "Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With" to "XMLHttpRequest" ) ) - val video_response = response.parsedSafe() ?: throw ErrorLoadingException("failed to parse response") - val m3u_link = video_response.securedLink + val videoResponse = response.parsedSafe() ?: throw ErrorLoadingException("failed to parse response") + val m3uLink = videoResponse.securedLink callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = m3u_link, - referer = ext_ref, + url = m3uLink, + referer = extRef, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt index ce742e97..07346c70 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt @@ -13,28 +13,25 @@ open class MailRu : ExtractorApi() { override val requiresReferer = false override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - Log.d("Kekik_${this.name}", "url » ${url}") + val extRef = referer ?: "" - val vid_id = url.substringAfter("video/embed/").trim() - val video_req = app.get("${mainUrl}/+/video/meta/${vid_id}", referer=url) - val video_key = video_req.cookies["video_key"].toString() - Log.d("Kekik_${this.name}", "video_key » ${video_key}") + val vidId = url.substringAfter("video/embed/").trim() + val videoReq = app.get("${mainUrl}/+/video/meta/${vidId}", referer=url) + val videoKey = videoReq.cookies["video_key"].toString() - val video_data = AppUtils.tryParseJson(video_req.text) ?: throw ErrorLoadingException("Video not found") + val videoData = AppUtils.tryParseJson(videoReq.text) ?: throw ErrorLoadingException("Video not found") - for (video in video_data.videos) { - Log.d("Kekik_${this.name}", "video » ${video}") + for (video in videoData.videos) { - val video_url = if (video.url.startsWith("//")) "https:${video.url}" else video.url + val videoUrl = if (video.url.startsWith("//")) "https:${video.url}" else video.url callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = video_url, + url = videoUrl, referer = url, - headers = mapOf("Cookie" to "video_key=${video_key}"), + headers = mapOf("Cookie" to "video_key=${videoKey}"), quality = getQualityFromName(video.key), isM3u8 = false ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt index 6db0830c..31b3d50b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt @@ -13,22 +13,20 @@ open class Odnoklassniki : ExtractorApi() { override val requiresReferer = false override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - Log.d("Kekik_${this.name}", "url » ${url}") + val extRef = referer ?: "" - val user_agent = mapOf("User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36") + val userAgent = mapOf("User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36") - val video_req = app.get(url, headers=user_agent).text.replace("\\"", "\"").replace("\\\\", "\\") + val videoReq = app.get(url, headers=userAgent).text.replace("\\"", "\"").replace("\\\\", "\\") .replace(Regex("\\\\u([0-9A-Fa-f]{4})")) { matchResult -> Integer.parseInt(matchResult.groupValues[1], 16).toChar().toString() } - val videos_str = Regex("""\"videos\":(\[[^\]]*\])""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found") - val videos = AppUtils.tryParseJson>(videos_str) ?: throw ErrorLoadingException("Video not found") + val videosStr = Regex("""\"videos\":(\[[^\]]*\])""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found") + val videos = AppUtils.tryParseJson>(videosStr) ?: throw ErrorLoadingException("Video not found") for (video in videos) { - Log.d("Kekik_${this.name}", "video » ${video}") - val video_url = if (video.url.startsWith("//")) "https:${video.url}" else video.url + val videoUrl = if (video.url.startsWith("//")) "https:${video.url}" else video.url val quality = video.name.uppercase() .replace("MOBILE", "144p") @@ -44,10 +42,10 @@ open class Odnoklassniki : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = video_url, + url = videoUrl, referer = url, quality = getQualityFromName(quality), - headers = user_agent, + headers = userAgent, isM3u8 = false ) ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt index 0a005036..3a5cf727 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt @@ -13,39 +13,38 @@ open class PeaceMakerst : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val m3u_link:String? - val ext_ref = referer ?: "" - val post_url = "${url}?do=getVideo" - Log.d("Kekik_${this.name}", "post_url » ${post_url}") + val m3uLink:String? + val extRef = referer ?: "" + val postUrl = "${url}?do=getVideo" val response = app.post( - post_url, + postUrl, data = mapOf( "hash" to url.substringAfter("video/"), - "r" to ext_ref, + "r" to extRef, "s" to "" ), - referer = ext_ref, + referer = extRef, headers = mapOf( "Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With" to "XMLHttpRequest" ) ) if (response.text.contains("teve2.com.tr\\/embed\\/")) { - val teve2_id = response.text.substringAfter("teve2.com.tr\\/embed\\/").substringBefore("\"") - val teve2_response = app.get( - "https://www.teve2.com.tr/action/media/${teve2_id}", - referer = "https://www.teve2.com.tr/embed/${teve2_id}" + val teve2Id = response.text.substringAfter("teve2.com.tr\\/embed\\/").substringBefore("\"") + val teve2Response = app.get( + "https://www.teve2.com.tr/action/media/${teve2Id}", + referer = "https://www.teve2.com.tr/embed/${teve2Id}" ).parsedSafe() ?: throw ErrorLoadingException("teve2 response is null") - m3u_link = teve2_response.media.link.serviceUrl + "//" + teve2_response.media.link.securePath + m3uLink = teve2Response.media.link.serviceUrl + "//" + teve2Response.media.link.securePath } else { - val video_response = response.parsedSafe() ?: throw ErrorLoadingException("peace response is null") - val video_sources = video_response.videoSources - if (video_sources.isNotEmpty()) { - m3u_link = video_sources.lastOrNull()?.file + val videoResponse = response.parsedSafe() ?: throw ErrorLoadingException("peace response is null") + val videoSources = videoResponse.videoSources + if (videoSources.isNotEmpty()) { + m3uLink = videoSources.lastOrNull()?.file } else { - m3u_link = null + m3uLink = null } } @@ -53,8 +52,8 @@ open class PeaceMakerst : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = m3u_link ?: throw ErrorLoadingException("m3u link not found"), - referer = ext_ref, + url = m3uLink ?: throw ErrorLoadingException("m3u link not found"), + referer = extRef, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt index 607d2d78..1088f2e9 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt @@ -12,36 +12,45 @@ open class RapidVid : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_req = app.get(url, referer=ext_ref).text + val extRef = referer ?: "" + val videoReq = app.get(url, referer=extRef).text - val sub_urls = mutableSetOf() - Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach { - val (sub_url, sub_lang) = it.destructured + val subUrls = mutableSetOf() + Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(videoReq).forEach { + val (subUrl, subLang) = it.destructured - if (sub_url in sub_urls) { return@forEach } - sub_urls.add(sub_url) + if (subUrl in subUrls) { return@forEach } + subUrls.add(subUrl) subtitleCallback.invoke( SubtitleFile( - lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), - url = fixUrl(sub_url.replace("\\", "")) + lang = subLang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(subUrl.replace("\\", "")) ) ) } - val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + var extractedValue = Regex("""file": "(.*)",""").find(videoReq)?.groupValues?.get(1) + var decoded: String? = null - val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() - val decoded = String(bytes, Charsets.UTF_8) - Log.d("Kekik_${this.name}", "decoded » ${decoded}") + if (extractedValue != null) { + val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() + decoded = String(bytes, Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } else { + val evalJWSsetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val JWSsetup = getAndUnpack(getAndUnpack(evalJWSsetup)).replace("\\\\", "\\") + extractedValue = Regex("""file":"(.*)","label""").find(JWSsetup)?.groupValues?.get(1)?.replace("\\\\x", "") + + val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray() + decoded = bytes?.toString(Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } callback.invoke( ExtractorLink( source = this.name, name = this.name, url = decoded, - referer = ext_ref, + referer = extRef, quality = Qualities.Unknown.value, isM3u8 = true ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt index ebd57f9c..89f731f7 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt @@ -12,18 +12,17 @@ open class SibNet : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val i_source = app.get(url, referer=ext_ref).text - var m3u_link = Regex("""player.src\(\[\{src: \"([^\"]+)""").find(i_source)?.groupValues?.get(1) ?: throw ErrorLoadingException("m3u link not found") + val extRef = referer ?: "" + val iSource = app.get(url, referer=extRef).text + var m3uLink = Regex("""player.src\(\[\{src: \"([^\"]+)""").find(iSource)?.groupValues?.get(1) ?: throw ErrorLoadingException("m3u link not found") - m3u_link = "${mainUrl}${m3u_link}" - Log.d("Kekik_${this.name}", "m3u_link » ${m3u_link}") + m3uLink = "${mainUrl}${m3uLink}" callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = m3u_link, + url = m3uLink, referer = url, quality = Qualities.Unknown.value, type = INFER_TYPE diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt index de5ca9a2..f2a75b94 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt @@ -13,13 +13,13 @@ open class TRsTX : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" + val extRef = referer ?: "" - val video_req = app.get(url, referer=ext_ref).text + val videoReq = app.get(url, referer=extRef).text - val file = Regex("""file\":\"([^\"]+)""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val file = Regex("""file\":\"([^\"]+)""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") val postLink = "${mainUrl}/" + file.replace("\\", "") - val rawList = app.post(postLink, referer=ext_ref).parsedSafe>() ?: throw ErrorLoadingException("Post link not found") + val rawList = app.post(postLink, referer=extRef).parsedSafe>() ?: throw ErrorLoadingException("Post link not found") val postJson: List = rawList.drop(1).map { item -> val mapItem = item as Map<*, *> @@ -28,37 +28,35 @@ open class TRsTX : ExtractorApi() { file = mapItem["file"] as? String ) } - Log.d("Kekik_${this.name}", "postJson » ${postJson}") - val vid_links = mutableSetOf() - val vid_map = mutableListOf>() + val vidLinks = mutableSetOf() + val vidMap = mutableListOf>() for (item in postJson) { if (item.file == null || item.title == null) continue val fileUrl = "${mainUrl}/playlist/" + item.file.substring(1) + ".txt" - val videoData = app.post(fileUrl, referer=ext_ref).text + val videoData = app.post(fileUrl, referer=extRef).text - if (videoData in vid_links) { continue } - vid_links.add(videoData) + if (videoData in vidLinks) { continue } + vidLinks.add(videoData) - vid_map.add(mapOf( + vidMap.add(mapOf( "title" to item.title, "videoData" to videoData )) } - for (mapEntry in vid_map) { - Log.d("Kekik_${this.name}", "mapEntry » ${mapEntry}") + for (mapEntry in vidMap) { val title = mapEntry["title"] ?: continue - val m3u_link = mapEntry["videoData"] ?: continue + val m3uLink = mapEntry["videoData"] ?: continue callback.invoke( ExtractorLink( source = this.name, name = "${this.name} - ${title}", - url = m3u_link, - referer = ext_ref, + url = m3uLink, + referer = extRef, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt index 157374a3..0893b4de 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt @@ -13,12 +13,11 @@ open class TauVideo : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_key = url.split("/").last() - val video_url = "${mainUrl}/api/video/${video_key}" - Log.d("Kekik_${this.name}", "video_url » ${video_url}") + val extRef = referer ?: "" + val videoKey = url.split("/").last() + val videoUrl = "${mainUrl}/api/video/${videoKey}" - val api = app.get(video_url).parsedSafe() ?: throw ErrorLoadingException("TauVideo") + val api = app.get(videoUrl).parsedSafe() ?: throw ErrorLoadingException("TauVideo") for (video in api.urls) { callback.invoke( @@ -26,7 +25,7 @@ open class TauVideo : ExtractorApi() { source = this.name, name = this.name, url = video.url, - referer = ext_ref, + referer = extRef, quality = getQualityFromName(video.label), type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt index e57772ce..f7c3dd5e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt @@ -12,36 +12,45 @@ open class VidMoxy : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_req = app.get(url, referer=ext_ref).text + val extRef = referer ?: "" + val videoReq = app.get(url, referer=extRef).text - val sub_urls = mutableSetOf() - Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach { - val (sub_url, sub_lang) = it.destructured + val subUrls = mutableSetOf() + Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(videoReq).forEach { + val (subUrl, subLang) = it.destructured - if (sub_url in sub_urls) { return@forEach } - sub_urls.add(sub_url) + if (subUrl in subUrls) { return@forEach } + subUrls.add(subUrl) subtitleCallback.invoke( SubtitleFile( - lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), - url = fixUrl(sub_url.replace("\\", "")) + lang = subLang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(subUrl.replace("\\", "")) ) ) } - val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + var extractedValue = Regex("""file": "(.*)",""").find(videoReq)?.groupValues?.get(1) + var decoded: String? = null - val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() - val decoded = String(bytes, Charsets.UTF_8) - Log.d("Kekik_${this.name}", "decoded » ${decoded}") + if (extractedValue != null) { + val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() + decoded = String(bytes, Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } else { + val evaljwSetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val jwSetup = getAndUnpack(getAndUnpack(evaljwSetup)).replace("\\\\", "\\") + extractedValue = Regex("""file":"(.*)","label""").find(jwSetup)?.groupValues?.get(1)?.replace("\\\\x", "") + + val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray() + decoded = bytes?.toString(Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } callback.invoke( ExtractorLink( source = this.name, name = this.name, url = decoded, - referer = ext_ref, + referer = extRef, quality = Qualities.Unknown.value, isM3u8 = true ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt index 1161ff66..c85e6416 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt @@ -15,14 +15,13 @@ open class VideoSeyred : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_id = url.substringAfter("embed/").substringBefore("?") - val video_url = "${mainUrl}/playlist/${video_id}.json" - Log.d("Kekik_${this.name}", "video_url » ${video_url}") + val extRef = referer ?: "" + val videoId = url.substringAfter("embed/").substringBefore("?") + val videoUrl = "${mainUrl}/playlist/${videoId}.json" - val response_raw = app.get(video_url) - val response_list:List = jacksonObjectMapper().readValue(response_raw.text) ?: throw ErrorLoadingException("VideoSeyred") - val response = response_list[0] ?: throw ErrorLoadingException("VideoSeyred") + val responseRaw = app.get(videoUrl) + val responseList:List = jacksonObjectMapper().readValue(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred") + val response = responseList[0] ?: throw ErrorLoadingException("VideoSeyred") for (track in response.tracks) { if (track.label != null && track.kind == "captions") { From 8fcb3e3121de93017f7b3cbf959b4429040a2184 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 30 Jul 2024 12:45:25 -0600 Subject: [PATCH 552/570] Fix cast recycler scrolling (#1221) --- .../ui/result/ResultFragmentPhone.kt | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) 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 f1399e8d..97bc49ea 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 @@ -23,6 +23,7 @@ import androidx.core.widget.NestedScrollView import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider import com.discord.panels.OverlappingPanelsLayout +import com.discord.panels.PanelState import com.discord.panels.PanelsChildGestureRegionObserver import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastContext @@ -118,6 +119,14 @@ open class ResultFragmentPhone : FullScreenPlayer() { return root } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { register(it) } + } + } + var currentTrailers: List = emptyList() var currentTrailerIndex = 0 @@ -210,9 +219,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { } override fun onDestroyView() { - - //somehow this still leaks and I dont know why???? - // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt PanelsChildGestureRegionObserver.Provider.get().let { obs -> resultBinding?.resultCastItems?.let { obs.unregister(it) @@ -329,13 +335,18 @@ open class ResultFragmentPhone : FullScreenPlayer() { syncModel.addFromUrl(storedData.url) val api = APIHolder.getApiFromNameNull(storedData.apiName) - PanelsChildGestureRegionObserver.Provider.get().apply { - resultBinding?.resultCastItems?.let { - register(it) + // This may not be 100% reliable, and may delay for small period + // before resultCastItems will be scrollable again, but this does work + // most of the time. + binding?.resultOverlappingPanels?.registerEndPanelStateListeners( + object : OverlappingPanelsLayout.PanelStateListener { + override fun onPanelStateChange(panelState: PanelState) { + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { register(it) } + } + } } - addGestureRegionsUpdateListener(gestureRegionsListener) - } - + ) // ===== ===== ===== @@ -674,6 +685,9 @@ open class ResultFragmentPhone : FullScreenPlayer() { observe(viewModel.page) { data -> if (data == null) return@observe resultBinding?.apply { + PanelsChildGestureRegionObserver.Provider.get().apply { + register(resultCastItems) + } (data as? Resource.Success)?.value?.let { d -> resultVpn.setText(d.vpnText) resultInfo.setText(d.metaText) @@ -1167,4 +1181,4 @@ open class ResultFragmentPhone : FullScreenPlayer() { } } } -} +} \ No newline at end of file From ab379ab31c30069aac3afdeec76410e69ea6bc95 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 30 Jul 2024 12:54:54 -0600 Subject: [PATCH 553/570] Support for multi deleting downloads and other major improvements/fixes (#1177) --- .../lagradost/cloudstream3/MainActivity.kt | 51 +- .../ui/download/DownloadAdapter.kt | 386 +++++++++----- .../ui/download/DownloadButtonSetup.kt | 31 +- .../ui/download/DownloadChildFragment.kt | 174 +++++-- .../ui/download/DownloadFragment.kt | 182 +++++-- .../ui/download/DownloadViewModel.kt | 483 +++++++++++++++--- .../ui/download/button/BaseFetchButton.kt | 7 +- .../ui/download/button/PieFetchButton.kt | 6 +- .../ui/player/DownloadFileGenerator.kt | 32 +- .../ui/player/DownloadedPlayerActivity.kt | 11 +- .../ui/result/ResultTrailerPlayer.kt | 30 +- .../cloudstream3/utils/AppContextUtils.kt | 14 +- .../utils/BackPressedCallbackHelper.kt | 30 ++ .../cloudstream3/utils/SnackbarHelper.kt | 84 +++ .../cloudstream3/utils/SubtitleUtils.kt | 56 ++ .../utils/VideoDownloadManager.kt | 45 +- .../res/layout/download_child_episode.xml | 17 +- .../res/layout/download_header_episode.xml | 11 + .../res/layout/fragment_child_downloads.xml | 59 ++- .../main/res/layout/fragment_downloads.xml | 109 ++-- app/src/main/res/values/strings.xml | 12 +- 21 files changed, 1384 insertions(+), 446 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index b59265ee..5408d2a8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -133,6 +133,8 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback @@ -151,6 +153,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog +import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute @@ -1254,17 +1257,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa this.setKey(getString(R.string.jsdelivr_proxy_key), false) } else { this.setKey(getString(R.string.jsdelivr_proxy_key), true) - val parentView: View = findViewById(android.R.id.content) - Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG) - .let { snackbar -> - snackbar.setAction(R.string.revert) { - setKey(getString(R.string.jsdelivr_proxy_key), false) - } - snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground)) - snackbar.setTextColor(colorFromAttribute(R.attr.textColor)) - snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary)) - snackbar.show() - } + showSnackbar( + this@MainActivity, + R.string.jsdelivr_enabled, + Snackbar.LENGTH_LONG, + R.string.revert + ) { setKey(getString(R.string.jsdelivr_proxy_key), false) } } } } @@ -1603,7 +1601,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa if (isLayout(TV or EMULATOR)) { if (navDestination.matchDestination(R.id.navigation_home)) { - attachBackPressedCallback() + attachBackPressedCallback { + showConfirmExitDialog() + window?.navigationBarColor = + colorFromAttribute(R.attr.primaryGrayBackground) + updateLocale() + } } else detachBackPressedCallback() } } @@ -1848,28 +1851,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa finish() } - private var backPressedCallback: OnBackPressedCallback? = null - - private fun attachBackPressedCallback() { - if (backPressedCallback == null) { - backPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - showConfirmExitDialog() - window?.navigationBarColor = - colorFromAttribute(R.attr.primaryGrayBackground) - updateLocale() - } - } - } - - backPressedCallback?.isEnabled = true - onBackPressedDispatcher.addCallback(this, backPressedCallback ?: return) - } - - private fun detachBackPressedCallback() { - backPressedCallback?.isEnabled = false - } - suspend fun checkGithubConnectivity(): Boolean { return try { app.get( @@ -1880,4 +1861,4 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa false } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt index 9a026334..20458429 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -1,9 +1,9 @@ package com.lagradost.cloudstream3.ui.download -import android.annotation.SuppressLint import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.ViewGroup +import android.widget.CheckBox import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil @@ -31,47 +31,30 @@ const val DOWNLOAD_ACTION_LONG_CLICK = 5 const val DOWNLOAD_ACTION_GO_TO_CHILD = 0 const val DOWNLOAD_ACTION_LOAD_RESULT = 1 -abstract class VisualDownloadCached( - open val currentBytes: Long, - open val totalBytes: Long, - open val data: VideoDownloadHelper.DownloadCached -) { +sealed class VisualDownloadCached { + abstract val currentBytes: Long + abstract val totalBytes: Long + abstract val data: VideoDownloadHelper.DownloadCached + abstract var isSelected: Boolean - // Just to be extra-safe with areContentsTheSame - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is VisualDownloadCached) return false + data class Child( + override val currentBytes: Long, + override val totalBytes: Long, + override val data: VideoDownloadHelper.DownloadEpisodeCached, + override var isSelected: Boolean, + ) : VisualDownloadCached() - if (currentBytes != other.currentBytes) return false - if (totalBytes != other.totalBytes) return false - if (data != other.data) return false - - return true - } - - override fun hashCode(): Int { - var result = currentBytes.hashCode() - result = 31 * result + totalBytes.hashCode() - result = 31 * result + data.hashCode() - return result - } + data class Header( + override val currentBytes: Long, + override val totalBytes: Long, + override val data: VideoDownloadHelper.DownloadHeaderCached, + override var isSelected: Boolean, + val child: VideoDownloadHelper.DownloadEpisodeCached?, + val currentOngoingDownloads: Int, + val totalDownloads: Int, + ) : VisualDownloadCached() } -data class VisualDownloadChildCached( - override val currentBytes: Long, - override val totalBytes: Long, - override val data: VideoDownloadHelper.DownloadEpisodeCached, -): VisualDownloadCached(currentBytes, totalBytes, data) - -data class VisualDownloadHeaderCached( - override val currentBytes: Long, - override val totalBytes: Long, - override val data: VideoDownloadHelper.DownloadHeaderCached, - val child: VideoDownloadHelper.DownloadEpisodeCached?, - val currentOngoingDownloads: Int, - val totalDownloads: Int, -): VisualDownloadCached(currentBytes, totalBytes, data) - data class DownloadClickEvent( val action: Int, val data: VideoDownloadHelper.DownloadEpisodeCached @@ -83,108 +66,180 @@ data class DownloadHeaderClickEvent( ) class DownloadAdapter( - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val mediaClickCallback: (DownloadClickEvent) -> Unit, + private val onHeaderClickEvent: (DownloadHeaderClickEvent) -> Unit, + private val onItemClickEvent: (DownloadClickEvent) -> Unit, + private val onItemSelectionChanged: (Int, Boolean) -> Unit, ) : ListAdapter(DiffCallback()) { + private var isMultiDeleteState: Boolean = false + companion object { private const val VIEW_TYPE_HEADER = 0 private const val VIEW_TYPE_CHILD = 1 } inner class DownloadViewHolder( - private val binding: ViewBinding, - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val mediaClickCallback: (DownloadClickEvent) -> Unit, + private val binding: ViewBinding ) : RecyclerView.ViewHolder(binding.root) { fun bind(card: VisualDownloadCached?) { when (binding) { - is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadHeaderCached) - is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadChildCached) + is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadCached.Header) + is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadCached.Child) } } - @SuppressLint("SetTextI18n") - private fun bindHeader(card: VisualDownloadHeaderCached?) { - if (binding !is DownloadHeaderEpisodeBinding) return - card ?: return - val d = card.data + private fun bindHeader(card: VisualDownloadCached.Header?) { + if (binding !is DownloadHeaderEpisodeBinding || card == null) return + val data = card.data binding.apply { - downloadHeaderPoster.apply { - setImage(d.poster) - setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(DOWNLOAD_ACTION_LOAD_RESULT, d)) + episodeHolder.apply { + if (isMultiDeleteState) { + setOnClickListener { + toggleIsChecked(deleteCheckbox, data.id) + } + } + + setOnLongClickListener { + toggleIsChecked(deleteCheckbox, data.id) + true } } - downloadHeaderTitle.text = d.name - val formattedSizeString = formatShortFileSize(itemView.context, card.totalBytes) + downloadHeaderPoster.apply { + setImage(data.poster) + if (isMultiDeleteState) { + setOnClickListener { + toggleIsChecked(deleteCheckbox, data.id) + } + } else { + setOnClickListener { + onHeaderClickEvent.invoke( + DownloadHeaderClickEvent( + DOWNLOAD_ACTION_LOAD_RESULT, + data + ) + ) + } + } + + setOnLongClickListener { + toggleIsChecked(deleteCheckbox, data.id) + true + } + } + downloadHeaderTitle.text = data.name + val formattedSize = formatShortFileSize(itemView.context, card.totalBytes) if (card.child != null) { - downloadHeaderGotoChild.isVisible = false + handleChildDownload(card, formattedSize) + } else handleParentDownload(card, formattedSize) - val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes) - if (status == DownloadStatusTell.IsDone) { - // We do this here instead if we are finished downloading - // so that we can use the value from the view model - // rather than extra unneeded disk operations and to prevent a - // delay in updating download icon state. - downloadButton.setProgress(card.currentBytes, card.totalBytes) - downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes) - // We will let the view model handle this - downloadButton.doSetProgress = false - downloadButton.progressBar.progressDrawable = - downloadButton.getDrawableFromStatus(status) - ?.let { ContextCompat.getDrawable(downloadButton.context, it) } - downloadHeaderInfo.text = formattedSizeString - } else { - downloadButton.doSetProgress = true - downloadButton.progressBar.progressDrawable = - ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) + if (isMultiDeleteState) { + deleteCheckbox.setOnCheckedChangeListener { _, isChecked -> + onItemSelectionChanged.invoke(data.id, isChecked) } + } else deleteCheckbox.setOnCheckedChangeListener(null) - downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback) - downloadButton.isVisible = true - - episodeHolder.setOnClickListener { - mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child)) - } - } else { - downloadButton.isVisible = false - downloadHeaderGotoChild.isVisible = true - - try { - downloadHeaderInfo.text = downloadHeaderInfo.context.getString(R.string.extra_info_format) - .format( - card.totalDownloads, - downloadHeaderInfo.context.resources.getQuantityString( - R.plurals.episodes, - card.totalDownloads - ), - formattedSizeString - ) - } catch (e: Exception) { - // You probably formatted incorrectly - downloadHeaderInfo.text = "Error" - logError(e) - } - - episodeHolder.setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(DOWNLOAD_ACTION_GO_TO_CHILD, d)) - } + deleteCheckbox.apply { + isVisible = isMultiDeleteState + isChecked = card.isSelected } } } - private fun bindChild(card: VisualDownloadChildCached?) { - if (binding !is DownloadChildEpisodeBinding) return - card ?: return - val d = card.data + private fun DownloadHeaderEpisodeBinding.handleChildDownload( + card: VisualDownloadCached.Header, + formattedSize: String + ) { + card.child ?: return + downloadHeaderGotoChild.isVisible = false + val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes) + if (status == DownloadStatusTell.IsDone) { + // We do this here instead if we are finished downloading + // so that we can use the value from the view model + // rather than extra unneeded disk operations and to prevent a + // delay in updating download icon state. + downloadButton.setProgress(card.currentBytes, card.totalBytes) + downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes) + // We will let the view model handle this + downloadButton.doSetProgress = false + downloadButton.progressBar.progressDrawable = + downloadButton.getDrawableFromStatus(status) + ?.let { ContextCompat.getDrawable(downloadButton.context, it) } + downloadHeaderInfo.text = formattedSize + } else { + // We need to make sure we restore the correct progress + // when we refresh data in the adapter. + downloadButton.resetView() + val drawable = downloadButton.getDrawableFromStatus(status)?.let { + ContextCompat.getDrawable(downloadButton.context, it) + } + downloadButton.statusView.setImageDrawable(drawable) + downloadButton.progressBar.progressDrawable = + ContextCompat.getDrawable( + downloadButton.context, + downloadButton.progressDrawable + ) + } + + downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, onItemClickEvent) + downloadButton.isVisible = !isMultiDeleteState + + if (!isMultiDeleteState) { + episodeHolder.setOnClickListener { + onItemClickEvent.invoke( + DownloadClickEvent( + DOWNLOAD_ACTION_PLAY_FILE, + card.child + ) + ) + } + } + } + + private fun DownloadHeaderEpisodeBinding.handleParentDownload( + card: VisualDownloadCached.Header, + formattedSize: String + ) { + downloadButton.isVisible = false + downloadHeaderGotoChild.isVisible = !isMultiDeleteState + + try { + downloadHeaderInfo.text = + downloadHeaderInfo.context.getString(R.string.extra_info_format).format( + card.totalDownloads, + downloadHeaderInfo.context.resources.getQuantityString( + R.plurals.episodes, + card.totalDownloads + ), + formattedSize + ) + } catch (e: Exception) { + downloadHeaderInfo.text = "" + logError(e) + } + + if (!isMultiDeleteState) { + episodeHolder.setOnClickListener { + onHeaderClickEvent.invoke( + DownloadHeaderClickEvent( + DOWNLOAD_ACTION_GO_TO_CHILD, + card.data + ) + ) + } + } + } + + private fun bindChild(card: VisualDownloadCached.Child?) { + if (binding !is DownloadChildEpisodeBinding || card == null) return + + val data = card.data binding.apply { - val posDur = getViewPos(d.id) + val posDur = getViewPos(data.id) downloadChildEpisodeProgress.apply { isVisible = posDur != null posDur?.let { @@ -194,36 +249,87 @@ class DownloadAdapter( } } - val status = downloadButton.getStatus(d.id, card.currentBytes, card.totalBytes) + val status = downloadButton.getStatus(data.id, card.currentBytes, card.totalBytes) if (status == DownloadStatusTell.IsDone) { // We do this here instead if we are finished downloading // so that we can use the value from the view model // rather than extra unneeded disk operations and to prevent a // delay in updating download icon state. downloadButton.setProgress(card.currentBytes, card.totalBytes) - downloadButton.applyMetaData(d.id, card.currentBytes, card.totalBytes) + downloadButton.applyMetaData(data.id, card.currentBytes, card.totalBytes) // We will let the view model handle this downloadButton.doSetProgress = false downloadButton.progressBar.progressDrawable = downloadButton.getDrawableFromStatus(status) ?.let { ContextCompat.getDrawable(downloadButton.context, it) } - downloadChildEpisodeTextExtra.text = formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes) + downloadChildEpisodeTextExtra.text = + formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes) } else { - downloadButton.doSetProgress = true + // We need to make sure we restore the correct progress + // when we refresh data in the adapter. + downloadButton.resetView() + val drawable = downloadButton.getDrawableFromStatus(status)?.let { + ContextCompat.getDrawable(downloadButton.context, it) + } + downloadButton.statusView.setImageDrawable(drawable) downloadButton.progressBar.progressDrawable = - ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) + ContextCompat.getDrawable( + downloadButton.context, + downloadButton.progressDrawable + ) } - downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, mediaClickCallback) - downloadButton.isVisible = true + downloadButton.setDefaultClickListener( + data, + downloadChildEpisodeTextExtra, + onItemClickEvent + ) + downloadButton.isVisible = !isMultiDeleteState downloadChildEpisodeText.apply { - text = context.getNameFull(d.name, d.episode, d.season) + text = context.getNameFull(data.name, data.episode, data.season) isSelected = true // Needed for text repeating } downloadChildEpisodeHolder.setOnClickListener { - mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) + onItemClickEvent.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, data)) + } + + downloadChildEpisodeHolder.apply { + when { + isMultiDeleteState -> { + setOnClickListener { + toggleIsChecked(deleteCheckbox, data.id) + } + } + + else -> { + setOnClickListener { + onItemClickEvent.invoke( + DownloadClickEvent( + DOWNLOAD_ACTION_PLAY_FILE, + data + ) + ) + } + } + } + + setOnLongClickListener { + toggleIsChecked(deleteCheckbox, data.id) + true + } + } + + if (isMultiDeleteState) { + deleteCheckbox.setOnCheckedChangeListener { _, isChecked -> + onItemSelectionChanged.invoke(data.id, isChecked) + } + } else deleteCheckbox.setOnCheckedChangeListener(null) + + deleteCheckbox.apply { + isVisible = isMultiDeleteState + isChecked = card.isSelected } } } @@ -236,7 +342,7 @@ class DownloadAdapter( VIEW_TYPE_CHILD -> DownloadChildEpisodeBinding.inflate(inflater, parent, false) else -> throw IllegalArgumentException("Invalid view type") } - return DownloadViewHolder(binding, clickCallback, mediaClickCallback) + return DownloadViewHolder(binding) } override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) { @@ -245,18 +351,52 @@ class DownloadAdapter( override fun getItemViewType(position: Int): Int { return when (getItem(position)) { - is VisualDownloadChildCached -> VIEW_TYPE_CHILD - is VisualDownloadHeaderCached -> VIEW_TYPE_HEADER + is VisualDownloadCached.Child -> VIEW_TYPE_CHILD + is VisualDownloadCached.Header -> VIEW_TYPE_HEADER else -> throw IllegalArgumentException("Invalid data type at position $position") } } + fun setIsMultiDeleteState(value: Boolean) { + if (isMultiDeleteState == value) return + isMultiDeleteState = value + notifyItemRangeChanged(0, itemCount) + } + + fun notifyAllSelected() { + currentList.indices.forEach { index -> + if (!currentList[index].isSelected) { + notifyItemChanged(index) + } + } + } + + fun notifySelectionStates() { + currentList.indices.forEach { index -> + if (currentList[index].isSelected) { + notifyItemChanged(index) + } + } + } + + private fun toggleIsChecked(checkbox: CheckBox, itemId: Int) { + val isChecked = !checkbox.isChecked + checkbox.isChecked = isChecked + onItemSelectionChanged.invoke(itemId, isChecked) + } + class DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean { + override fun areItemsTheSame( + oldItem: VisualDownloadCached, + newItem: VisualDownloadCached + ): Boolean { return oldItem.data.id == newItem.data.id } - override fun areContentsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean { + override fun areContentsTheSame( + oldItem: VisualDownloadCached, + newItem: VisualDownloadCached + ): Boolean { return oldItem == newItem } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index c8c40e29..bf2c1b49 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -1,11 +1,10 @@ package com.lagradost.cloudstream3.ui.download import android.content.DialogInterface -import android.widget.Toast import androidx.appcompat.app.AlertDialog +import com.google.android.material.snackbar.Snackbar import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator @@ -14,9 +13,11 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE +import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager +import kotlinx.coroutines.MainScope object DownloadButtonSetup { fun handleDownloadClick(click: DownloadClickEvent) { @@ -29,9 +30,15 @@ object DownloadButtonSetup { DialogInterface.OnClickListener { _, which -> when (which) { DialogInterface.BUTTON_POSITIVE -> { - VideoDownloadManager.deleteFileAndUpdateSettings(ctx, id) + VideoDownloadManager.deleteFilesAndUpdateSettings( + ctx, + setOf(id), + MainScope() + ) } + DialogInterface.BUTTON_NEGATIVE -> { + // Do nothing on cancel } } } @@ -56,11 +63,13 @@ object DownloadButtonSetup { } } } + DOWNLOAD_ACTION_PAUSE_DOWNLOAD -> { VideoDownloadManager.downloadEvent.invoke( Pair(click.data.id, VideoDownloadManager.DownloadActionType.Pause) ) } + DOWNLOAD_ACTION_RESUME_DOWNLOAD -> { activity?.let { ctx -> if (VideoDownloadManager.downloadStatus.containsKey(id) && VideoDownloadManager.downloadStatus[id] == VideoDownloadManager.DownloadType.IsPaused) { @@ -79,6 +88,7 @@ object DownloadButtonSetup { } } } + DOWNLOAD_ACTION_LONG_CLICK -> { activity?.let { act -> val length = @@ -88,12 +98,15 @@ object DownloadButtonSetup { )?.fileLength ?: 0 if (length > 0) { - showToast(R.string.delete, Toast.LENGTH_LONG) - } else { - showToast(R.string.download, Toast.LENGTH_LONG) + showSnackbar( + act, + R.string.offline_file, + Snackbar.LENGTH_LONG + ) } } } + DOWNLOAD_ACTION_PLAY_FILE -> { activity?.let { act -> val info = @@ -119,7 +132,7 @@ object DownloadButtonSetup { id = click.data.id, parentId = click.data.parentId, - name = act.getString(R.string.downloaded_file), //click.data.name ?: keyInfo.displayName + name = act.getString(R.string.downloaded_file), // click.data.name ?: keyInfo.displayName season = click.data.season, episode = click.data.episode, headerName = parent.name, @@ -132,7 +145,7 @@ object DownloadButtonSetup { ) ) ) - //R.id.global_to_navigation_player, PlayerFragment.newInstance( + // R.id.global_to_navigation_player, PlayerFragment.newInstance( // UriData( // info.path.toString(), // keyInfo.basePath, @@ -145,7 +158,7 @@ object DownloadButtonSetup { // click.data.season // ), // getViewPos(click.data.id)?.position ?: 0 - //) + // ) ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index 03db948c..09c48a04 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -1,29 +1,33 @@ package com.lagradost.cloudstream3.ui.download import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding +import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.Coroutines.main -import com.lagradost.cloudstream3.utils.DataStore.getKey -import com.lagradost.cloudstream3.utils.DataStore.getKeys +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV -import com.lagradost.cloudstream3.utils.VideoDownloadHelper -import com.lagradost.cloudstream3.utils.VideoDownloadManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext class DownloadChildFragment : Fragment() { + private lateinit var downloadsViewModel: DownloadViewModel + private var binding: FragmentChildDownloadsBinding? = null + companion object { fun newInstance(headerName: String, folder: String): Bundle { return Bundle().apply { @@ -34,61 +38,54 @@ class DownloadChildFragment : Fragment() { } override fun onDestroyView() { - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it } - downloadDeleteEventListener = null + detachBackPressedCallback() binding = null super.onDestroyView() } - private var binding: FragmentChildDownloadsBinding? = null - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { + downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false) binding = localBinding return localBinding.root } - private fun updateList(folder: String) = main { - context?.let { ctx -> - val data = withContext(Dispatchers.IO) { ctx.getKeys(folder) } - val eps = withContext(Dispatchers.IO) { - data.mapNotNull { key -> - context?.getKey(key) - }.mapNotNull { - val info = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(ctx, it.id) - ?: return@mapNotNull null - VisualDownloadChildCached( - currentBytes = info.fileLength, - totalBytes = info.totalBytes, - data = it, - ) - } - }.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 } - if (eps.isEmpty()) { - activity?.onBackPressedDispatcher?.onBackPressed() - return@main - } - - (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(eps) - } - } - - private var downloadDeleteEventListener: ((Int) -> Unit)? = null - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + /** + * We never want to retain multi-delete state + * when navigating to downloads. Setting this state + * immediately can sometimes result in the observer + * not being notified in time to update the UI. + * + * By posting to the main looper, we ensure that this + * operation is executed after the view has been fully created + * and all initializations are completed, allowing the + * observer to properly receive and handle the state change. + */ + Handler(Looper.getMainLooper()).post { + downloadsViewModel.setIsMultiDeleteState(false) + } + + /** + * We have to make sure selected items are + * cleared here as well so we don't run in an + * inconsistent state where selected items do + * not match the multi delete state we are in. + */ + downloadsViewModel.clearSelectedItems() + val folder = arguments?.getString("folder") val name = arguments?.getString("name") if (folder == null) { - activity?.onBackPressedDispatcher?.onBackPressed() // TODO FIX + activity?.onBackPressedDispatcher?.onBackPressed() return } - fixPaddingStatusbar(binding?.downloadChildRoot) binding?.downloadChildToolbar?.apply { title = name @@ -101,13 +98,55 @@ class DownloadChildFragment : Fragment() { setAppBarNoScrollFlagsOnTV() } + binding?.downloadDeleteAppbar?.setAppBarNoScrollFlagsOnTV() + + observe(downloadsViewModel.childCards) { + if (it.isEmpty()) { + activity?.onBackPressedDispatcher?.onBackPressed() + return@observe + } + + (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(it) + } + observe(downloadsViewModel.isMultiDeleteState) { isMultiDeleteState -> + val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter + adapter?.setIsMultiDeleteState(isMultiDeleteState) + binding?.downloadDeleteAppbar?.isVisible = isMultiDeleteState + if (!isMultiDeleteState) { + detachBackPressedCallback() + downloadsViewModel.clearSelectedItems() + binding?.downloadChildToolbar?.isVisible = true + } + } + observe(downloadsViewModel.selectedBytes) { + updateDeleteButton(downloadsViewModel.selectedItemIds.value?.count() ?: 0, it) + } + observe(downloadsViewModel.selectedItemIds) { + handleSelectedChange(it) + updateDeleteButton(it.count(), downloadsViewModel.selectedBytes.value ?: 0L) + + binding?.btnDelete?.isVisible = it.isNotEmpty() + binding?.selectItemsText?.isVisible = it.isEmpty() + + val allSelected = downloadsViewModel.isAllSelected() + if (allSelected) { + binding?.btnToggleAll?.setText(R.string.deselect_all) + } else binding?.btnToggleAll?.setText(R.string.select_all) + } + val adapter = DownloadAdapter( {}, - { downloadClickEvent -> - handleDownloadClick(downloadClickEvent) - if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { - setUpDownloadDeleteListener(folder) - } + { click -> + if (click.action == DOWNLOAD_ACTION_DELETE_FILE) { + context?.let { ctx -> + downloadsViewModel.handleSingleDelete(ctx, click.data.id) + } + } else handleDownloadClick(click) + }, + { itemId, isChecked -> + if (isChecked) { + downloadsViewModel.addSelected(itemId) + } else downloadsViewModel.removeSelected(itemId) } ) @@ -122,18 +161,47 @@ class DownloadChildFragment : Fragment() { ) } - updateList(folder) + context?.let { downloadsViewModel.updateChildList(it, folder) } + fixPaddingStatusbar(binding?.downloadChildRoot) } - private fun setUpDownloadDeleteListener(folder: String) { - downloadDeleteEventListener = { id: Int -> - val list = (binding?.downloadChildList?.adapter as? DownloadAdapter)?.currentList - if (list != null) { - if (list.any { it.data.id == id }) { - updateList(folder) + private fun handleSelectedChange(selected: MutableSet) { + if (selected.isNotEmpty()) { + binding?.downloadDeleteAppbar?.isVisible = true + binding?.downloadChildToolbar?.isVisible = false + activity?.attachBackPressedCallback { + downloadsViewModel.setIsMultiDeleteState(false) + } + + binding?.btnDelete?.setOnClickListener { + context?.let { ctx -> + downloadsViewModel.handleMultiDelete(ctx) } } + + binding?.btnCancel?.setOnClickListener { + downloadsViewModel.setIsMultiDeleteState(false) + } + + binding?.btnToggleAll?.setOnClickListener { + val allSelected = downloadsViewModel.isAllSelected() + val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter + if (allSelected) { + adapter?.notifySelectionStates() + downloadsViewModel.clearSelectedItems() + } else { + adapter?.notifyAllSelected() + downloadsViewModel.selectAllItems() + } + } + + downloadsViewModel.setIsMultiDeleteState(true) } - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + } + + private fun updateDeleteButton(count: Int, selectedBytes: Long) { + val formattedSize = formatShortFileSize(context, selectedBytes) + binding?.btnDelete?.text = + getString(R.string.delete_format).format(count, formattedSize) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 23d546e1..447b4f13 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -8,6 +8,8 @@ import android.content.Intent import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION import android.os.Build import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.View @@ -17,7 +19,6 @@ import android.widget.TextView import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes -import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged @@ -27,7 +28,7 @@ import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding import com.lagradost.cloudstream3.databinding.StreamInputBinding -import com.lagradost.cloudstream3.isMovieType +import com.lagradost.cloudstream3.isEpisodeBased import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick @@ -40,20 +41,22 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE -import com.lagradost.cloudstream3.utils.DataStore +import com.lagradost.cloudstream3.utils.DataStore.getFolderName import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV -import com.lagradost.cloudstream3.utils.VideoDownloadManager import java.net.URI const val DOWNLOAD_NAVIGATE_TO = "downloadpage" class DownloadFragment : Fragment() { private lateinit var downloadsViewModel: DownloadViewModel + private var binding: FragmentDownloadsBinding? = null private fun View.setLayoutWidth(weight: Long) { val param = LinearLayout.LayoutParams( @@ -65,14 +68,11 @@ class DownloadFragment : Fragment() { } override fun onDestroyView() { - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it } - downloadDeleteEventListener = null + detachBackPressedCallback() binding = null super.onDestroyView() } - private var binding: FragmentDownloadsBinding? = null - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -84,12 +84,34 @@ class DownloadFragment : Fragment() { return localBinding.root } - private var downloadDeleteEventListener: ((Int) -> Unit)? = null - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) hideKeyboard() binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() + binding?.downloadDeleteAppbar?.setAppBarNoScrollFlagsOnTV() + + /** + * We never want to retain multi-delete state + * when navigating to downloads. Setting this state + * immediately can sometimes result in the observer + * not being notified in time to update the UI. + * + * By posting to the main looper, we ensure that this + * operation is executed after the view has been fully created + * and all initializations are completed, allowing the + * observer to properly receive and handle the state change. + */ + Handler(Looper.getMainLooper()).post { + downloadsViewModel.setIsMultiDeleteState(false) + } + + /** + * We have to make sure selected items are + * cleared here as well so we don't run in an + * inconsistent state where selected items do + * not match the multi delete state we are in. + */ + downloadsViewModel.clearSelectedItems() observe(downloadsViewModel.headerCards) { (binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(it) @@ -97,25 +119,82 @@ class DownloadFragment : Fragment() { binding?.textNoDownloads?.isVisible = it.isEmpty() } observe(downloadsViewModel.availableBytes) { - updateStorageInfo(view.context, it, R.string.free_storage, binding?.downloadFreeTxt, binding?.downloadFree) + updateStorageInfo( + view.context, + it, + R.string.free_storage, + binding?.downloadFreeTxt, + binding?.downloadFree + ) } observe(downloadsViewModel.usedBytes) { - updateStorageInfo(view.context, it, R.string.used_storage, binding?.downloadUsedTxt, binding?.downloadUsed) - binding?.downloadStorageAppbar?.isVisible = it > 0 + updateStorageInfo( + view.context, + it, + R.string.used_storage, + binding?.downloadUsedTxt, + binding?.downloadUsed + ) + + // Prevent race condition and make sure + // we don't display it early + if ( + downloadsViewModel.isMultiDeleteState.value == null || + downloadsViewModel.isMultiDeleteState.value == false + ) binding?.downloadStorageAppbar?.isVisible = it > 0 } observe(downloadsViewModel.downloadBytes) { - updateStorageInfo(view.context, it, R.string.app_storage, binding?.downloadAppTxt, binding?.downloadApp) + updateStorageInfo( + view.context, + it, + R.string.app_storage, + binding?.downloadAppTxt, + binding?.downloadApp + ) + } + observe(downloadsViewModel.selectedBytes) { + updateDeleteButton(downloadsViewModel.selectedItemIds.value?.count() ?: 0, it) + } + observe(downloadsViewModel.isMultiDeleteState) { isMultiDeleteState -> + val adapter = binding?.downloadList?.adapter as? DownloadAdapter + adapter?.setIsMultiDeleteState(isMultiDeleteState) + binding?.downloadDeleteAppbar?.isVisible = isMultiDeleteState + if (!isMultiDeleteState) { + detachBackPressedCallback() + downloadsViewModel.clearSelectedItems() + // Prevent race condition and make sure + // we don't display it early + if (downloadsViewModel.usedBytes.value?.let { it > 0 } == true) { + binding?.downloadStorageAppbar?.isVisible = true + } + } + } + observe(downloadsViewModel.selectedItemIds) { + handleSelectedChange(it) + updateDeleteButton(it.count(), downloadsViewModel.selectedBytes.value ?: 0L) + + binding?.btnDelete?.isVisible = it.isNotEmpty() + binding?.selectItemsText?.isVisible = it.isEmpty() + + val allSelected = downloadsViewModel.isAllSelected() + if (allSelected) { + binding?.btnToggleAll?.setText(R.string.deselect_all) + } else binding?.btnToggleAll?.setText(R.string.select_all) } val adapter = DownloadAdapter( + { click -> handleItemClick(click) }, { click -> - handleItemClick(click) + if (click.action == DOWNLOAD_ACTION_DELETE_FILE) { + context?.let { ctx -> + downloadsViewModel.handleSingleDelete(ctx, click.data.id) + } + } else handleDownloadClick(click) }, - { downloadClickEvent -> - handleDownloadClick(downloadClickEvent) - if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { - setUpDownloadDeleteListener() - } + { itemId, isChecked -> + if (isChecked) { + downloadsViewModel.addSelected(itemId) + } else downloadsViewModel.removeSelected(itemId) } ) @@ -126,7 +205,6 @@ class DownloadFragment : Fragment() { setLinearListLayout( isHorizontal = false, nextRight = FOCUS_SELF, - nextUp = FOCUS_SELF, nextDown = FOCUS_SELF, ) } @@ -147,35 +225,68 @@ class DownloadFragment : Fragment() { handleScroll(scrollY - oldScrollY) } } - downloadsViewModel.updateList(requireContext()) + + context?.let { downloadsViewModel.updateHeaderList(it) } fixPaddingStatusbar(binding?.downloadRoot) } private fun handleItemClick(click: DownloadHeaderClickEvent) { when (click.action) { DOWNLOAD_ACTION_GO_TO_CHILD -> { - if (!click.data.type.isMovieType()) { - val folder = DataStore.getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString()) + if (click.data.type.isEpisodeBased()) { + val folder = + getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString()) activity?.navigate( R.id.action_navigation_downloads_to_navigation_download_child, DownloadChildFragment.newInstance(click.data.name, folder) ) } } + DOWNLOAD_ACTION_LOAD_RESULT -> { - (activity as AppCompatActivity?)?.loadResult(click.data.url, click.data.apiName) + activity?.loadResult(click.data.url, click.data.apiName) } } } - private fun setUpDownloadDeleteListener() { - downloadDeleteEventListener = { id -> - val list = (binding?.downloadList?.adapter as? DownloadAdapter)?.currentList - if (list?.any { it.data.id == id } == true) { - context?.let { downloadsViewModel.updateList(it) } + private fun handleSelectedChange(selected: MutableSet) { + if (selected.isNotEmpty()) { + binding?.downloadDeleteAppbar?.isVisible = true + binding?.downloadStorageAppbar?.isVisible = false + activity?.attachBackPressedCallback { + downloadsViewModel.setIsMultiDeleteState(false) } + + binding?.btnDelete?.setOnClickListener { + context?.let { ctx -> + downloadsViewModel.handleMultiDelete(ctx) + } + } + + binding?.btnCancel?.setOnClickListener { + downloadsViewModel.setIsMultiDeleteState(false) + } + + binding?.btnToggleAll?.setOnClickListener { + val allSelected = downloadsViewModel.isAllSelected() + val adapter = binding?.downloadList?.adapter as? DownloadAdapter + if (allSelected) { + adapter?.notifySelectionStates() + downloadsViewModel.clearSelectedItems() + } else { + adapter?.notifyAllSelected() + downloadsViewModel.selectAllItems() + } + } + + downloadsViewModel.setIsMultiDeleteState(true) } - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + } + + private fun updateDeleteButton(count: Int, selectedBytes: Long) { + val formattedSize = formatShortFileSize(context, selectedBytes) + binding?.btnDelete?.text = + getString(R.string.delete_format).format(count, formattedSize) } private fun updateStorageInfo( @@ -185,7 +296,10 @@ class DownloadFragment : Fragment() { textView: TextView?, view: View? ) { - textView?.text = getString(R.string.storage_size_format).format(getString(stringRes), formatShortFileSize(context, bytes)) + textView?.text = getString(R.string.storage_size_format).format( + getString(stringRes), + formatShortFileSize(context, bytes) + ) view?.setLayoutWidth(bytes) } @@ -218,7 +332,9 @@ class DownloadFragment : Fragment() { if (!preventAutoSwitching) activateSwitchOnHls(text?.toString(), binding) } - (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt(0)?.text?.toString()?.let { copy -> + (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt( + 0 + )?.text?.toString()?.let { copy -> val fixedText = copy.trim() binding.streamUrl.setText(fixedText) activateSwitchOnHls(fixedText, binding) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt index 83d96592..137f1355 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt @@ -1,122 +1,439 @@ package com.lagradost.cloudstream3.ui.download import android.content.Context +import android.content.DialogInterface import android.os.Environment import android.os.StatFs +import androidx.appcompat.app.AlertDialog import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lagradost.cloudstream3.isMovieType +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.isEpisodeBased import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStore.getFolderName import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import com.lagradost.cloudstream3.utils.VideoDownloadManager.deleteFilesAndUpdateSettings import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadFileInfoAndUpdateSettings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class DownloadViewModel : ViewModel() { - private val _headerCards = - MutableLiveData>().apply { listOf() } - val headerCards: LiveData> = _headerCards + + private val _headerCards = MutableLiveData>() + val headerCards: LiveData> = _headerCards + + private val _childCards = MutableLiveData>() + val childCards: LiveData> = _childCards private val _usedBytes = MutableLiveData() - private val _availableBytes = MutableLiveData() - private val _downloadBytes = MutableLiveData() - val usedBytes: LiveData = _usedBytes + + private val _availableBytes = MutableLiveData() val availableBytes: LiveData = _availableBytes + + private val _downloadBytes = MutableLiveData() val downloadBytes: LiveData = _downloadBytes - private var previousVisual: List? = null + private val _selectedBytes = MutableLiveData(0) + val selectedBytes: LiveData = _selectedBytes - fun updateList(context: Context) = viewModelScope.launchSafe { - val children = withContext(Dispatchers.IO) { - context.getKeys(DOWNLOAD_EPISODE_CACHE) + private val _isMultiDeleteState = MutableLiveData(false) + val isMultiDeleteState: LiveData = _isMultiDeleteState + + private val _selectedItemIds = MutableLiveData>(mutableSetOf()) + val selectedItemIds: LiveData> = _selectedItemIds + + private var previousVisual: List? = null + + fun setIsMultiDeleteState(value: Boolean) { + _isMultiDeleteState.postValue(value) + } + + fun addSelected(itemId: Int) { + updateSelectedItems { it.add(itemId) } + } + + fun removeSelected(itemId: Int) { + updateSelectedItems { it.remove(itemId) } + } + + fun selectAllItems() { + val items = headerCards.value.orEmpty() + childCards.value.orEmpty() + updateSelectedItems { it.addAll(items.map { item -> item.data.id }) } + } + + fun clearSelectedItems() { + // We need this to be done immediately + // so we can't use postValue + _selectedItemIds.value = mutableSetOf() + updateSelectedItems { it.clear() } + } + + fun isAllSelected(): Boolean { + val currentSelected = selectedItemIds.value ?: return false + val items = headerCards.value.orEmpty() + childCards.value.orEmpty() + return items.count() == currentSelected.count() && items.all { it.data.id in currentSelected } + } + + private fun updateSelectedItems(action: (MutableSet) -> Unit) { + val currentSelected = selectedItemIds.value ?: mutableSetOf() + action(currentSelected) + _selectedItemIds.postValue(currentSelected) + updateSelectedBytes() + updateSelectedCards() + } + + private fun updateSelectedBytes() = viewModelScope.launchSafe { + val selectedItemsList = getSelectedItemsData() ?: return@launchSafe + val totalSelectedBytes = selectedItemsList.sumOf { it.totalBytes } + _selectedBytes.postValue(totalSelectedBytes) + } + + private fun updateSelectedCards() = viewModelScope.launchSafe { + val currentSelected = selectedItemIds.value ?: return@launchSafe + + headerCards.value?.let { headers -> + headers.forEach { header -> + header.isSelected = header.data.id in currentSelected + } + _headerCards.postValue(headers) + } + + childCards.value?.let { children -> + children.forEach { child -> + child.isSelected = child.data.id in currentSelected + } + _childCards.postValue(children) + } + } + + fun updateHeaderList(context: Context) = viewModelScope.launchSafe { + val visual = withContext(Dispatchers.IO) { + val children = context.getKeys(DOWNLOAD_EPISODE_CACHE) .mapNotNull { context.getKey(it) } .distinctBy { it.id } // Remove duplicates + + val (totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads) = + calculateDownloadStats(context, children) + + val cached = context.getKeys(DOWNLOAD_HEADER_CACHE) + .mapNotNull { context.getKey(it) } + + createVisualDownloadList( + context, cached, totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads + ) } - // parentId : bytes - val totalBytesUsedByChild = HashMap() - // parentId : bytes - val currentBytesUsedByChild = HashMap() - // parentId : downloadsCount - val totalDownloads = HashMap() - - // Gets all children downloads - withContext(Dispatchers.IO) { - children.forEach { c -> - val childFile = getDownloadFileInfoAndUpdateSettings(context, c.id) ?: return@forEach - - if (childFile.fileLength <= 1) return@forEach - val len = childFile.totalBytes - val flen = childFile.fileLength - - totalBytesUsedByChild[c.parentId] = totalBytesUsedByChild[c.parentId]?.plus(len) ?: len - currentBytesUsedByChild[c.parentId] = currentBytesUsedByChild[c.parentId]?.plus(flen) ?: flen - totalDownloads[c.parentId] = totalDownloads[c.parentId]?.plus(1) ?: 1 - } - } - - val cached = withContext(Dispatchers.IO) { // Won't fetch useless keys - totalDownloads.entries.filter { it.value > 0 }.mapNotNull { - context.getKey( - DOWNLOAD_HEADER_CACHE, - it.key.toString() - ) - } - } - - val visual = withContext(Dispatchers.IO) { - cached.mapNotNull { - val downloads = totalDownloads[it.id] ?: 0 - val bytes = totalBytesUsedByChild[it.id] ?: 0 - val currentBytes = currentBytesUsedByChild[it.id] ?: 0 - if (bytes <= 0 || downloads <= 0) return@mapNotNull null - val movieEpisode = - if (!it.type.isMovieType()) null - else context.getKey( - DOWNLOAD_EPISODE_CACHE, - getFolderName(it.id.toString(), it.id.toString()) - ) - VisualDownloadHeaderCached( - currentBytes = currentBytes, - totalBytes = bytes, - data = it, - child = movieEpisode, - currentOngoingDownloads = 0, - totalDownloads = downloads, - ) - }.sortedBy { - (it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0) - } // Episode sorting by episode, lowest to highest - } - - // Only update list if different from the previous one to prevent duplicate initialization if (visual != previousVisual) { previousVisual = visual - - try { - val stat = StatFs(Environment.getExternalStorageDirectory().path) - val localBytesAvailable = stat.availableBytes - val localTotalBytes = stat.blockSizeLong * stat.blockCountLong - val localDownloadedBytes = visual.sumOf { it.totalBytes } - - _usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes) - _availableBytes.postValue(localBytesAvailable) - _downloadBytes.postValue(localDownloadedBytes) - } catch (t: Throwable) { - _downloadBytes.postValue(0) - logError(t) - } - + updateStorageStats(visual) _headerCards.postValue(visual) } } + + private fun calculateDownloadStats( + context: Context, + children: List + ): Triple, Map, Map> { + // parentId : bytes + val totalBytesUsedByChild = mutableMapOf() + // parentId : bytes + val currentBytesUsedByChild = mutableMapOf() + // parentId : downloadsCount + val totalDownloads = mutableMapOf() + + children.forEach { child -> + val childFile = getDownloadFileInfoAndUpdateSettings(context, child.id) ?: return@forEach + if (childFile.fileLength <= 1) return@forEach + + val len = childFile.totalBytes + val flen = childFile.fileLength + + totalBytesUsedByChild.merge(child.parentId, len, Long::plus) + currentBytesUsedByChild.merge(child.parentId, flen, Long::plus) + totalDownloads.merge(child.parentId, 1, Int::plus) + } + return Triple(totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads) + } + + private fun createVisualDownloadList( + context: Context, + cached: List, + totalBytesUsedByChild: Map, + currentBytesUsedByChild: Map, + totalDownloads: Map + ): List { + return cached.mapNotNull { + val downloads = totalDownloads[it.id] ?: 0 + val bytes = totalBytesUsedByChild[it.id] ?: 0 + val currentBytes = currentBytesUsedByChild[it.id] ?: 0 + if (bytes <= 0 || downloads <= 0) return@mapNotNull null + + val isSelected = selectedItemIds.value?.contains(it.id) ?: false + val movieEpisode = if (it.type.isEpisodeBased()) null else context.getKey( + DOWNLOAD_EPISODE_CACHE, + getFolderName(it.id.toString(), it.id.toString()) + ) + + VisualDownloadCached.Header( + currentBytes = currentBytes, + totalBytes = bytes, + data = it, + child = movieEpisode, + currentOngoingDownloads = 0, + totalDownloads = downloads, + isSelected = isSelected, + ) + // Prevent order being almost completely random, + // making things difficult to find. + }.sortedWith(compareBy { + // Sort by isEpisodeBased() ascending. We put those that + // are episode based at the bottom for UI purposes and to + // make it easier to find by grouping them together. + it.data.type.isEpisodeBased() + }.thenBy { + // Then we sort alphabetically by name (case-insensitive). + // Again, we do this to make things easier to find. + it.data.name.lowercase() + }) + } + + fun updateChildList(context: Context, folder: String) = viewModelScope.launchSafe { + val visual = withContext(Dispatchers.IO) { + context.getKeys(folder).mapNotNull { key -> + context.getKey(key) + }.mapNotNull { + val isSelected = selectedItemIds.value?.contains(it.id) ?: false + val info = getDownloadFileInfoAndUpdateSettings(context, it.id) ?: return@mapNotNull null + VisualDownloadCached.Child( + currentBytes = info.fileLength, + totalBytes = info.totalBytes, + isSelected = isSelected, + data = it, + ) + } + }.sortedWith(compareBy( + // Sort by season first, and then by episode number, + // to ensure sorting is consistent. + { it.data.season ?: 0 }, + { it.data.episode } + )) + + if (previousVisual != visual) { + previousVisual = visual + _childCards.postValue(visual) + } + } + + private fun removeItems(idsToRemove: Set) = viewModelScope.launchSafe { + val updatedHeaders = headerCards.value.orEmpty().filter { it.data.id !in idsToRemove } + val updatedChildren = childCards.value.orEmpty().filter { it.data.id !in idsToRemove } + _headerCards.postValue(updatedHeaders) + _childCards.postValue(updatedChildren) + } + + private fun updateStorageStats(visual: List) { + try { + val stat = StatFs(Environment.getExternalStorageDirectory().path) + val localBytesAvailable = stat.availableBytes + val localTotalBytes = stat.blockSizeLong * stat.blockCountLong + val localDownloadedBytes = visual.sumOf { it.totalBytes } + val localUsedBytes = localTotalBytes - localBytesAvailable + _usedBytes.postValue(localUsedBytes) + _availableBytes.postValue(localBytesAvailable) + _downloadBytes.postValue(localDownloadedBytes) + } catch (t: Throwable) { + _downloadBytes.postValue(0) + logError(t) + } + } + + fun handleMultiDelete(context: Context) = viewModelScope.launchSafe { + val selectedItemsList = getSelectedItemsData().orEmpty() + val deleteData = processSelectedItems(context, selectedItemsList) + val message = buildDeleteMessage(context, deleteData) + showDeleteConfirmationDialog(context, message, deleteData.ids, deleteData.parentIds) + } + + fun handleSingleDelete( + context: Context, + itemId: Int + ) = viewModelScope.launchSafe { + val itemData = getItemDataFromId(itemId) + val deleteData = processSelectedItems(context, itemData) + val message = buildDeleteMessage(context, deleteData) + showDeleteConfirmationDialog(context, message, deleteData.ids, deleteData.parentIds) + } + + private fun processSelectedItems( + context: Context, + selectedItemsList: List + ): DeleteData { + val names = mutableListOf() + val seriesNames = mutableListOf() + + val ids = mutableSetOf() + val parentIds = mutableSetOf() + + var parentName: String? = null + + selectedItemsList.forEach { item -> + when (item) { + is VisualDownloadCached.Header -> { + if (item.data.type.isEpisodeBased()) { + val episodes = context.getKeys(DOWNLOAD_EPISODE_CACHE) + .mapNotNull { + context.getKey( + it + ) + } + .filter { it.parentId == item.data.id } + .map { it.id } + ids.addAll(episodes) + parentIds.add(item.data.id) + + val episodeInfo = "${item.data.name} (${item.totalDownloads} ${ + context.resources.getQuantityString( + R.plurals.episodes, + item.totalDownloads + ).lowercase() + })" + seriesNames.add(episodeInfo) + } else { + ids.add(item.data.id) + names.add(item.data.name) + } + } + + is VisualDownloadCached.Child -> { + ids.add(item.data.id) + val parent = context.getKey( + DOWNLOAD_HEADER_CACHE, + item.data.parentId.toString() + ) + parentName = parent?.name + names.add( + context.getNameFull( + item.data.name, + item.data.episode, + item.data.season + ) + ) + } + } + } + + return DeleteData(ids, parentIds, seriesNames, names, parentName) + } + + private fun buildDeleteMessage( + context: Context, + data: DeleteData + ): String { + val formattedNames = data.names.sortedBy { it.lowercase() } + .joinToString(separator = "\n") { "• $it" } + val formattedSeriesNames = data.seriesNames.sortedBy { it.lowercase() } + .joinToString(separator = "\n") { "• $it" } + + return when { + data.ids.count() == 1 -> { + context.getString(R.string.delete_message).format( + data.names.firstOrNull() + ) + } + + data.seriesNames.isNotEmpty() && data.names.isEmpty() -> { + context.getString(R.string.delete_message_series_only).format(formattedSeriesNames) + } + + data.parentName != null && data.names.isNotEmpty() -> { + context.getString(R.string.delete_message_series_episodes) + .format(data.parentName, formattedNames) + } + + data.seriesNames.isNotEmpty() -> { + val seriesSection = context.getString(R.string.delete_message_series_section) + .format(formattedSeriesNames) + context.getString(R.string.delete_message_multiple) + .format(formattedNames) + "\n\n" + seriesSection + } + + else -> context.getString(R.string.delete_message_multiple).format(formattedNames) + } + } + + private fun showDeleteConfirmationDialog( + context: Context, + message: String, + ids: Set, + parentIds: Set + ) { + val builder = AlertDialog.Builder(context) + val dialogClickListener = + DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + viewModelScope.launchSafe { + setIsMultiDeleteState(false) + deleteFilesAndUpdateSettings(context, ids, this) { successfulIds -> + // We always remove parent because if we are deleting from here + // and we have it as non-empty, it was triggered on + // parent header card + removeItems(successfulIds + parentIds) + } + } + } + + DialogInterface.BUTTON_NEGATIVE -> { + // Do nothing on cancel + } + } + } + + try { + val title = if (ids.count() == 1) { + R.string.delete_file + } else R.string.delete_files + builder.setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.delete, dialogClickListener) + .setNegativeButton(R.string.cancel, dialogClickListener) + .show().setDefaultFocus() + } catch (e: Exception) { + logError(e) + } + } + + private fun getSelectedItemsData(): List? { + val headers = headerCards.value.orEmpty() + val children = childCards.value.orEmpty() + + return selectedItemIds.value?.mapNotNull { id -> + headers.find { it.data.id == id } ?: children.find { it.data.id == id } + } + } + + private fun getItemDataFromId(itemId: Int): List { + val headers = headerCards.value.orEmpty() + val children = childCards.value.orEmpty() + + return (headers + children).filter { it.data.id == itemId } + } + + private data class DeleteData( + val ids: Set, + val parentIds: Set, + val seriesNames: List, + val names: List, + val parentName: String? + ) } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index 45132131..908e3a80 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -93,7 +93,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : abstract fun setStatus(status: VideoDownloadManager.DownloadType?) - fun getStatus(id:Int, downloadedBytes: Long, totalBytes: Long): DownloadStatusTell { + fun getStatus(id: Int, downloadedBytes: Long, totalBytes: Long): DownloadStatusTell { // some extra padding for just in case return VideoDownloadManager.downloadStatus[id] ?: if (downloadedBytes > 1024L && downloadedBytes + 1024L >= totalBytes) { @@ -101,7 +101,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } else DownloadStatusTell.IsPaused } - fun applyMetaData(id:Int, downloadedBytes: Long, totalBytes: Long) { + fun applyMetaData(id: Int, downloadedBytes: Long, totalBytes: Long) { val status = getStatus(id, downloadedBytes, totalBytes) currentMetaData.apply { @@ -140,7 +140,8 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } else { if (doSetProgress) { progressText?.apply { - val currentFormattedSizeString = formatShortFileSize(context, downloadedBytes) + val currentFormattedSizeString = + formatShortFileSize(context, downloadedBytes) val totalFormattedSizeString = formatShortFileSize(context, totalBytes) text = // if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index abc159d0..29c2daa2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -58,7 +58,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } private var progressBarBackground: View - private var statusView: ImageView + var statusView: ImageView open fun onInflate() {} @@ -248,7 +248,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } */ @MainThread - private fun setStatusInternal(status : DownloadStatusTell?) { + private fun setStatusInternal(status: DownloadStatusTell?) { val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { val animation = AnimationUtils.loadAnimation(context, waitingAnimation) @@ -286,7 +286,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : if (Looper.myLooper() == Looper.getMainLooper()) { try { setStatusInternal(status) - } catch (t : Throwable) { + } catch (t: Throwable) { logError(t) // Just in case setStatusInternal throws because thread progressBarBackground.post { setStatusInternal(status) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index a8a3106a..a0668abc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -4,7 +4,9 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.VideoDownloadManager +import com.lagradost.cloudstream3.utils.SubtitleUtils.cleanDisplayName +import com.lagradost.cloudstream3.utils.SubtitleUtils.isMatchingSubtitle +import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFolder import kotlin.math.max import kotlin.math.min @@ -49,10 +51,6 @@ class DownloadFileGenerator( return null } - private fun cleanDisplayName(name: String): String { - return name.substringBeforeLast('.').trim() - } - override suspend fun generateLinks( clearCache: Boolean, type: LoadType, @@ -69,28 +67,9 @@ class DownloadFileGenerator( val cleanDisplay = cleanDisplayName(display) - VideoDownloadManager.getFolder(ctx, relative, meta.basePath) - ?.forEach { (name, uri) -> - // only these files are allowed, so no videos as subtitles - if (listOf( - ".vtt", - ".srt", - ".txt", - ".ass", - ".ttml", - ".sbv", - ".dfxp" - ).none { name.contains(it, true) } - ) return@forEach - - // cant have the exact same file as a subtitle - if (name.equals(display, true)) return@forEach - + getFolder(ctx, relative, meta.basePath)?.forEach { (name, uri) -> + if (isMatchingSubtitle(name, display, cleanDisplay)) { val cleanName = cleanDisplayName(name) - - // we only want files with the approx same name - if (!cleanName.startsWith(cleanDisplay, true)) return@forEach - val realName = cleanName.removePrefix(cleanDisplay) subtitleCallback( @@ -104,6 +83,7 @@ class DownloadFileGenerator( ) ) } + } return true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 4279b542..c38160c2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -4,13 +4,13 @@ import android.content.Intent import android.os.Bundle import android.util.Log import android.view.KeyEvent -import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback class DownloadedPlayerActivity : AppCompatActivity() { private val dTAG = "DownloadedPlayerAct" @@ -70,14 +70,7 @@ class DownloadedPlayerActivity : AppCompatActivity() { return } - onBackPressedDispatcher.addCallback( - this, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - finish() - } - } - ) + attachBackPressedCallback { finish() } } override fun onResume() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index 135dc530..2ab60c2f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -7,7 +7,6 @@ import android.os.Bundle import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import androidx.activity.OnBackPressedCallback import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.CommonActivity.screenHeight @@ -17,6 +16,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.PlayerEventSource import com.lagradost.cloudstream3.ui.player.SubtitleData +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback open class ResultTrailerPlayer : ResultFragmentPhone() { @@ -156,7 +157,9 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { uiReset() if (isFullScreenPlayer) { - attachBackPressedCallback() + activity?.attachBackPressedCallback { + updateFullscreen(false) + } } else detachBackPressedCallback() } @@ -175,27 +178,4 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { fixPlayerSize() } } - - private var backPressedCallback: OnBackPressedCallback? = null - - private fun attachBackPressedCallback() { - if (backPressedCallback == null) { - backPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - updateFullscreen(false) - } - } - } - - backPressedCallback?.isEnabled = true - - activity?.onBackPressedDispatcher?.addCallback( - activity ?: return, - backPressedCallback ?: return - ) - } - - private fun detachBackPressedCallback() { - backPressedCallback?.isEnabled = false - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index b13de062..8d65acf7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -677,9 +677,15 @@ object AppContextUtils { } fun Context.isNetworkAvailable(): Boolean { - val manager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val activeNetworkInfo = manager.activeNetworkInfo - return activeNetworkInfo != null && activeNetworkInfo.isConnected || manager.allNetworkInfo?.any { it.isConnected } ?: false + val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val network = connectivityManager.activeNetwork ?: return false + val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } else { + @Suppress("DEPRECATION") + connectivityManager.activeNetworkInfo?.isConnected == true + } } fun splitQuery(url: URL): Map { @@ -1018,4 +1024,4 @@ object AppContextUtils { } return currentAudioFocusRequest } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt new file mode 100644 index 00000000..1326ab27 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt @@ -0,0 +1,30 @@ +package com.lagradost.cloudstream3.utils + +import androidx.activity.ComponentActivity +import androidx.activity.OnBackPressedCallback + +object BackPressedCallbackHelper { + private var backPressedCallback: OnBackPressedCallback? = null + + fun ComponentActivity.attachBackPressedCallback(callback: () -> Unit) { + if (backPressedCallback == null) { + backPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + callback.invoke() + } + } + } + + backPressedCallback?.isEnabled = true + + onBackPressedDispatcher.addCallback( + this@attachBackPressedCallback, + backPressedCallback ?: return + ) + } + + fun detachBackPressedCallback() { + backPressedCallback?.isEnabled = false + backPressedCallback = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt new file mode 100644 index 00000000..e6a77795 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt @@ -0,0 +1,84 @@ +package com.lagradost.cloudstream3.utils + +import android.app.Activity +import android.view.View +import androidx.annotation.MainThread +import androidx.annotation.StringRes +import com.google.android.material.snackbar.Snackbar +import com.lagradost.api.Log +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.ui.result.UiText +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute + +object SnackbarHelper { + + private const val TAG = "COMPACT" + private var currentSnackbar: Snackbar? = null + + @MainThread + fun showSnackbar( + act: Activity?, + message: UiText, + duration: Int = Snackbar.LENGTH_SHORT, + actionText: UiText? = null, + actionCallback: (() -> Unit)? = null + ) { + if (act == null) return + showSnackbar(act, message.asString(act), duration, + actionText?.asString(act), actionCallback) + } + + @MainThread + fun showSnackbar( + act: Activity?, + @StringRes message: Int, + duration: Int = Snackbar.LENGTH_SHORT, + @StringRes actionText: Int? = null, + actionCallback: (() -> Unit)? = null + ) { + if (act == null) return + showSnackbar(act, act.getString(message), duration, + actionText?.let { act.getString(it) }, actionCallback) + } + + @MainThread + fun showSnackbar( + act: Activity?, + message: String?, + duration: Int = Snackbar.LENGTH_SHORT, + actionText: String? = null, + actionCallback: (() -> Unit)? = null + ) { + if (act == null || message == null) { + Log.w(TAG, "Invalid showSnackbar: act = $act, message = $message") + return + } + Log.i(TAG, "showSnackbar: $message") + + try { + currentSnackbar?.dismiss() + } catch (e: Exception) { + logError(e) + } + + try { + val parentView = act.findViewById(android.R.id.content) + val snackbar = Snackbar.make(parentView, message, duration) + + actionCallback?.let { + snackbar.setAction(actionText) { actionCallback.invoke() } + } + + snackbar.show() + currentSnackbar = snackbar + + snackbar.setBackgroundTint(act.colorFromAttribute(R.attr.primaryBlackBackground)) + snackbar.setTextColor(act.colorFromAttribute(R.attr.textColor)) + snackbar.setActionTextColor(act.colorFromAttribute(R.attr.colorPrimary)) + + } catch (e: Exception) { + logError(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt new file mode 100644 index 00000000..93a53395 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt @@ -0,0 +1,56 @@ +package com.lagradost.cloudstream3.utils + +import android.content.Context +import com.lagradost.api.Log +import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFolder +import com.lagradost.safefile.SafeFile + +object SubtitleUtils { + + // Only these files are allowed, so no videos as subtitles + private val allowedExtensions = listOf( + ".vtt", ".srt", ".txt", ".ass", + ".ttml", ".sbv", ".dfxp" + ) + + fun deleteMatchingSubtitles(context: Context, info: VideoDownloadManager.DownloadedFileInfo) { + val relative = info.relativePath + val display = info.displayName + val cleanDisplay = cleanDisplayName(display) + + getFolder(context, relative, info.basePath)?.forEach { (name, uri) -> + if (isMatchingSubtitle(name, display, cleanDisplay)) { + val subtitleFile = SafeFile.fromUri(context, uri) + if (subtitleFile == null || !subtitleFile.delete()) { + Log.e("SubtitleDeletion", "Failed to delete subtitle file: ${subtitleFile?.name()}") + } + } + } + } + + /** + * @param name the file name of the subtitle + * @param display the file name of the video + * @param cleanDisplay the cleanDisplayName of the video file name + */ + fun isMatchingSubtitle( + name: String, + display: String, + cleanDisplay: String + ): Boolean { + // Check if the file has a valid subtitle extension + val hasValidExtension = allowedExtensions.any { name.contains(it, ignoreCase = true) } + + // We can't have the exact same file as a subtitle + val isNotDisplayName = !name.equals(display, ignoreCase = true) + + // Check if the file name starts with a cleaned version of the display name + val startsWithCleanDisplay = cleanDisplayName(name).startsWith(cleanDisplay, ignoreCase = true) + + return hasValidExtension && isNotDisplayName && startsWithCleanDisplay + } + + fun cleanDisplayName(name: String): String { + return name.substringBeforeLast('.').trim() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index a3f6d789..2190e03f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -20,6 +20,7 @@ import androidx.work.WorkManager import com.bumptech.glide.Glide import com.bumptech.glide.load.model.GlideUrl import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.api.Log import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -29,12 +30,14 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey +import com.lagradost.cloudstream3.utils.SubtitleUtils.deleteMatchingSubtitles import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.safefile.MediaFileContentType import com.lagradost.safefile.SafeFile @@ -42,6 +45,8 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -1733,7 +1738,37 @@ object VideoDownloadManager { } } - fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { + fun deleteFilesAndUpdateSettings( + context: Context, + ids: Set, + scope: CoroutineScope, + onComplete: (Set) -> Unit = {} + ) { + scope.launchSafe(Dispatchers.IO) { + val deleteJobs = ids.map { id -> + async { + id to deleteFileAndUpdateSettings(context, id) + } + } + val results = deleteJobs.awaitAll() + + val (successfulResults, failedResults) = results.partition { it.second } + val successfulIds = successfulResults.map { it.first }.toSet() + + if (failedResults.isNotEmpty()) { + failedResults.forEach { (id, _) -> + // TODO show a toast if some failed? + Log.e("FileDeletion", "Failed to delete file with ID: $id") + } + } else { + Log.i("FileDeletion", "All files deleted successfully") + } + + onComplete.invoke(successfulIds) + } + } + + private fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { val success = deleteFile(context, id) if (success) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) return success @@ -1759,11 +1794,17 @@ object VideoDownloadManager { private fun deleteFile(context: Context, id: Int): Boolean { val info = context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return false + val file = info.toFile(context) + downloadEvent.invoke(id to DownloadActionType.Stop) downloadProgressEvent.invoke(Triple(id, 0, 0)) downloadStatusEvent.invoke(id to DownloadType.IsStopped) downloadDeleteEvent.invoke(id) - return info.toFile(context)?.delete() ?: false + + val isFileDeleted = file?.delete() == true || file?.exists() == false + if (isFileDeleted) deleteMatchingSubtitles(context, info) + + return isFileDeleted } fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? { diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml index 4974a027..e53e63d3 100644 --- a/app/src/main/res/layout/download_child_episode.xml +++ b/app/src/main/res/layout/download_child_episode.xml @@ -2,10 +2,8 @@ @@ -78,7 +73,6 @@ tools:text="128MB / 237MB" /> - + + \ No newline at end of file diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index a0b64ce3..957869d4 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -77,5 +77,16 @@ android:focusable="true" android:nextFocusLeft="@id/episode_holder" android:padding="10dp" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_child_downloads.xml b/app/src/main/res/layout/fragment_child_downloads.xml index 9afaea0b..64ed1d70 100644 --- a/app/src/main/res/layout/fragment_child_downloads.xml +++ b/app/src/main/res/layout/fragment_child_downloads.xml @@ -7,13 +7,69 @@ android:layout_height="match_parent" android:background="?attr/primaryGrayBackground" android:orientation="vertical" - tools:context=".ui.download.DownloadFragment"> + tools:context=".ui.download.DownloadChildFragment"> + + + + +

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⪚gf)t9sd9jc>Z+kR{=B9h_0g_ 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 diff --git a/app/src/main/res/drawable/profile_bg_pink.jpg b/app/src/main/res/drawable/profile_bg_pink.jpg index 63473fe0cfd6e3b2f8f5e56d3401047f3f089b09..9d4940f0d9ff412ccaa11fd3c8e30736273cb1d9 100644 GIT binary patch literal 112946 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

Eq2tl-2|T%gQ(ot06(7;`ow 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 B_jCXN literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/profile_bg_teal.jpg b/app/src/main/res/drawable/profile_bg_teal.jpg new file mode 100644 index 0000000000000000000000000000000000000000..75ef777bfebf000ff741ad7dab8e0aacc69a6937 GIT binary patch literal 128982 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/player_prioritize_item.xml b/app/src/main/res/layout/player_prioritize_item.xml new file mode 100644 index 00000000..b78863f8 --- /dev/null +++ b/app/src/main/res/layout/player_prioritize_item.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/player_quality_profile_dialog.xml b/app/src/main/res/layout/player_quality_profile_dialog.xml new file mode 100644 index 00000000..7bd7a680 --- /dev/null +++ b/app/src/main/res/layout/player_quality_profile_dialog.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_quality_profile_item.xml b/app/src/main/res/layout/player_quality_profile_item.xml new file mode 100644 index 00000000..3fad69ac --- /dev/null +++ b/app/src/main/res/layout/player_quality_profile_item.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/player_select_source_and_subs.xml b/app/src/main/res/layout/player_select_source_and_subs.xml index 067e4ad5..550b08d5 100644 --- a/app/src/main/res/layout/player_select_source_and_subs.xml +++ b/app/src/main/res/layout/player_select_source_and_subs.xml @@ -1,5 +1,6 @@ - + android:background="@drawable/outline_drawable_less" + android:foreground="?attr/selectableItemBackgroundBorderless" + android:gravity="center_vertical" + android:orientation="horizontal"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8f67739d..fbaecd2e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -658,4 +658,23 @@ Subscribed to %s Unsubscribed from %s Episode %d released! - \ No newline at end of file + Profile %d + Wi-Fi + Mobile data + Set default + Use + Edit + Profiles + Help + + Here you can change how the sources are ordered. If a video has a higher priority it will appear higher in the source selection. + The sum of the source priority and the quality priority is the video priority. + \n\nSource A: 3 + \nQuality B: 7 + \nWill have a combined video priority of 10. + + \n\nNOTE: If the sum is 10 or more the player will automatically skip loading when that link is loaded! + + Qualities + Profile background + diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index 2d2905ea..ad33e036 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -11,14 +11,14 @@ - - + + + + + + + + Date: Wed, 14 Jun 2023 22:42:42 +0000 Subject: [PATCH 086/570] Fixed skip loading (#484) * Added quality profiles * Better quality selection * Added profile bg and fixed some sources * Properly fixed skip loading * Extra safety --------- Co-authored-by: Lag <> --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 14 +++++++++++--- .../ui/player/PlayerGeneratorViewModel.kt | 14 ++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) 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 e20a07fa..fd29d998 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 @@ -238,6 +238,7 @@ class GeneratorPlayer : FullScreenPlayer() { } meta.name = newMeta.headerName } + is ExtractorUri -> { if (newMeta.tvType?.isMovieType() == false) { meta.episode = newMeta.episode @@ -980,6 +981,7 @@ class GeneratorPlayer : FullScreenPlayer() { is ResultEpisode -> { DataStoreHelper.removeLastWatched(newMeta.parentId) } + is ExtractorUri -> { DataStoreHelper.removeLastWatched(newMeta.parentId) } @@ -996,6 +998,7 @@ class GeneratorPlayer : FullScreenPlayer() { isFromDownload = false ) } + is ExtractorUri -> { DataStoreHelper.setLastWatched( resumeMeta.parentId, @@ -1127,6 +1130,7 @@ class GeneratorPlayer : FullScreenPlayer() { season = meta.season tvType = meta.tvType } + is ExtractorUri -> { headerName = meta.headerName subName = meta.name @@ -1343,6 +1347,7 @@ class GeneratorPlayer : FullScreenPlayer() { is Resource.Loading -> { startLoading() } + is Resource.Success -> { // provider returned false //if (it.value != true) { @@ -1350,6 +1355,7 @@ class GeneratorPlayer : FullScreenPlayer() { //} startPlayer() } + is Resource.Failure -> { showToast(activity, it.errorString, Toast.LENGTH_LONG) startPlayer() @@ -1364,10 +1370,12 @@ class GeneratorPlayer : FullScreenPlayer() { overlay_loading_skip_button?.isVisible = turnVisible normalSafeApiCall { - currentLinks.lastOrNull()?.let { last -> - if (getLinkPriority(currentQualityProfile, last) >= QualityDataHelper.AUTO_SKIP_PRIORITY) { - startPlayer() + if (currentLinks.any { link -> + getLinkPriority(currentQualityProfile, link) >= + QualityDataHelper.AUTO_SKIP_PRIORITY } + ) { + startPlayer() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 7faf0cf5..1b13b519 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -156,18 +156,24 @@ class PlayerGeneratorViewModel : ViewModel() { val currentSubs = mutableSetOf() // clear old data - _currentSubs.postValue(currentSubs) - _currentLinks.postValue(currentLinks) + _currentSubs.postValue(emptySet()) + _currentLinks.postValue(emptySet()) // load more data _loadingLinks.postValue(Resource.Loading()) val loadingState = safeApiCall { generator?.generateLinks(clearCache = clearCache, isCasting = isCasting, { currentLinks.add(it) - _currentLinks.postValue(currentLinks) + // Clone to prevent ConcurrentModificationException + normalSafeApiCall { + // Extra normalSafeApiCall since .toSet() iterates. + _currentLinks.postValue(currentLinks.toSet()) + } }, { currentSubs.add(it) - // _currentSubs.postValue(currentSubs) // this causes ConcurrentModificationException, so fuck it + normalSafeApiCall { + _currentSubs.postValue(currentSubs.toSet()) + } }) } From 40a963588f0048bb690d37e151d95ebc4f866e49 Mon Sep 17 00:00:00 2001 From: "imgbot[bot]" <31301654+imgbot[bot]@users.noreply.github.com> Date: Thu, 15 Jun 2023 21:45:03 +0200 Subject: [PATCH 087/570] [ImgBot] Optimize images (#485) *Total -- 351.66kb -> 337.87kb (3.92%) Signed-off-by: ImgBotApp Co-authored-by: ImgBotApp --- .../res/drawable/profile_bg_dark_blue.jpg | Bin 42704 -> 42568 bytes .../main/res/drawable/profile_bg_orange.jpg | Bin 70427 -> 69262 bytes app/src/main/res/drawable/profile_bg_pink.jpg | Bin 117989 -> 112946 bytes app/src/main/res/drawable/profile_bg_teal.jpg | Bin 128982 -> 121206 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/app/src/main/res/drawable/profile_bg_dark_blue.jpg b/app/src/main/res/drawable/profile_bg_dark_blue.jpg index c6482bc78592f57686761aa1b87e787fe745f684..d59e4888c64d22fec4fbb4b6772f467315dcddd5 100644 GIT binary patch literal 42568 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⪚gf)t9sd9jc>Z+kR{=B9h_0g_ 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/profile_bg_pink.jpg b/app/src/main/res/drawable/profile_bg_pink.jpg new file mode 100644 index 0000000000000000000000000000000000000000..63473fe0cfd6e3b2f8f5e56d3401047f3f089b09 GIT binary patch literal 117989 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/profile_bg_purple.jpg b/app/src/main/res/drawable/profile_bg_purple.jpg new file mode 100644 index 0000000000000000000000000000000000000000..15723dba35e110e570ab502a63d583c4b13bf8c8 GIT binary patch literal 8564 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

g!fC#;0+fen{k3i*HT`@Uz5%PpHG z!Cm^L3(OC+m%K|#bEPy-lJhBM~Nhg5ex48|bhgu~Jm{m@;L+cIq0UAwAc;aHLIwomF`!k@2Q^ugeMy&f_ zSc31DsJVrLY4A)4TZaQ{h#6An-F;==kvWO^ZZmP+N=1%o*ha+CLfQ1=T^X2wx{H6w)U8^Z5N0pAp7j5l9wy3OOan2%J6d} zl&_->J=#&70snd4wW``5qWzLV#728($1=`Xv+UT{$+|DKhr?Bw^o ziyqac0<8MrlZ`W7sYB!4;4yUB12uQzun$b+$y99s7~VP+H&oro~fG@pt7`nHdZ7LZ*ie zJ$h!QC~YEqbX2!?ElvKqY7N2J3pevcj>0B_%{xQSr6HdCXfibHKaW94gS@PHm9>3h z!+mV{v)$Q3A8iM;1a!Va=1ZQu1Ma%Oc^hBxXDUja_cB~~JV?T{OEt-K&mM7Dx)Ua4 zlYeP$b=9XiKAkr8MR^8aQeS%ID^{i3p(nQ^_aoUIj}p^7mRa=~CkkN(5=j(%=!yyt zmAyR(oG0!+%TAG$MkdzM-X9MrZ@on@RbW#~obSkI-oBzL>xe5R35q7|4^d><3Mtx;=x0*<9KreEZ4I#e#E=E@tWQp| z!o-!AjKMrW@iLLlPuME-0$fqGRKI@vej@Jx*}(ovkhgKjzY_7@$*aNg;IeW1F~w9xi-ZXDsAa%y zusdGbGh&f#w(onos@&|`!at21S&oOgga`#djFxy-Cg|-d$>ZH`YF!EUCnT$TLod0$ zpT%8K9W12#^09$Cc`ifu-s7IoXga zUp`v%@Q#T}cG#J3rmZ`ZODfdT+Zba$ob#Bw%+m8&%Jdx@NTS0lN3^@m?(9#*7BKl@ zN>I&7o8g*~g>9!l3z1zKkX{<(FYM+cKfV_dzp!NL@TF56(c;ELiNILo4E16!&-fU=wyAYk^R}=pQoQXPN diff --git a/.github/player.jpg b/.github/player.jpg deleted file mode 100644 index f6959cf31d27e5debc98e7d69b54688c589803a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49418 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�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