From cc3eba51f3c4e1dcbdd1e72597eea48ed5f71244 Mon Sep 17 00:00:00 2001 From: LagradOst Date: Mon, 13 Dec 2021 19:41:33 +0100 Subject: [PATCH] staging for sync --- .../com/lagradost/cloudstream3/MainAPI.kt | 142 ++++-- .../lagradost/cloudstream3/MainActivity.kt | 6 +- .../animeproviders/ZoroProvider.kt | 23 + .../metaproviders/TmdbProvider.kt | 39 +- .../movieproviders/AllMoviesForYouProvider.kt | 20 +- .../movieproviders/SflixProvider.kt | 51 +- .../movieproviders/ThenosProvider.kt | 482 ------------------ .../movieproviders/TrailersToProvider.kt | 305 ----------- .../movieproviders/VfSerieProvider.kt | 4 +- .../syncproviders/AccountManager.kt | 6 - .../cloudstream3/syncproviders/OAuth2API.kt | 3 + .../syncproviders/providers/DropboxApi.kt | 7 +- .../cloudstream3/ui/home/HomeFragment.kt | 14 +- .../ui/quicksearch/QuickSearchFragment.kt | 28 +- .../cloudstream3/ui/result/ResultFragment.kt | 137 ++++- .../cloudstream3/ui/result/ResultViewModel.kt | 17 +- .../cloudstream3/ui/search/SearchFragment.kt | 29 +- .../cloudstream3/utils/DataStoreHelper.kt | 14 +- .../lagradost/cloudstream3/utils/UIHelper.kt | 14 + .../cloudstream3/utils/VideoDownloadHelper.kt | 2 +- .../main/res/drawable/ic_baseline_add_24.xml | 5 + app/src/main/res/layout/fragment_result.xml | 207 +++++--- app/src/main/res/layout/fragment_search.xml | 6 +- app/src/main/res/layout/quick_search.xml | 2 +- app/src/main/res/values/strings.xml | 13 +- 25 files changed, 504 insertions(+), 1072 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/movieproviders/ThenosProvider.kt delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/movieproviders/TrailersToProvider.kt create mode 100644 app/src/main/res/drawable/ic_baseline_add_24.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 153ea028..b56e0ef7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -367,7 +367,7 @@ data class AnimeSearchResponse( override val posterUrl: String?, val year: Int? = null, - val dubStatus: EnumSet?, + val dubStatus: EnumSet? = null, val otherName: String? = null, val dubEpisodes: Int? = null, @@ -418,7 +418,7 @@ interface LoadResponse { val plot: String? val rating: Int? // 0-100 val tags: List? - val duration: String? + var duration: Int? // in minutes val trailerUrl: String? val recommendations: List? } @@ -444,29 +444,29 @@ data class AnimeEpisode( ) data class TorrentLoadResponse( - override val name: String, - override val url: String, - override val apiName: String, - val magnet: String?, - val torrent: String?, - override val plot: String?, - override val type: TvType = TvType.Torrent, - override val posterUrl: String? = null, - override val year: Int? = null, - override val rating: Int? = null, - override val tags: List? = null, - override val duration: String? = null, - override val trailerUrl: String? = null, - override val recommendations: List? = null, + override var name: String, + override var url: String, + override var apiName: String, + var magnet: String?, + var torrent: String?, + override var plot: String?, + override var type: TvType = TvType.Torrent, + override var posterUrl: String? = null, + override var year: Int? = null, + override var rating: Int? = null, + override var tags: List? = null, + override var duration: Int? = null, + override var trailerUrl: String? = null, + override var recommendations: List? = null, ) : LoadResponse data class AnimeLoadResponse( var engName: String? = null, var japName: String? = null, - override val name: String, - override val url: String, - override val apiName: String, - override val type: TvType, + override var name: String, + override var url: String, + override var apiName: String, + override var type: TvType, override var posterUrl: String? = null, override var year: Int? = null, @@ -481,7 +481,7 @@ data class AnimeLoadResponse( var malId: Int? = null, var anilistId: Int? = null, override var rating: Int? = null, - override var duration: String? = null, + override var duration: Int? = null, override var trailerUrl: String? = null, override var recommendations: List? = null, ) : LoadResponse @@ -503,24 +503,52 @@ fun MainAPI.newAnimeLoadResponse( } data class MovieLoadResponse( - override val name: String, - override val url: String, - override val apiName: String, - override val type: TvType, - val dataUrl: String, + override var name: String, + override var url: String, + override var apiName: String, + override var type: TvType, + var dataUrl: String, - override val posterUrl: String? = null, - override val year: Int? = null, - override val plot: String? = null, + override var posterUrl: String? = null, + override var year: Int? = null, + override var plot: String? = null, - val imdbId: String? = null, - override val rating: Int? = null, - override val tags: List? = null, - override val duration: String? = null, - override val trailerUrl: String? = null, - override val recommendations: List? = null, + var imdbId: String? = null, + override var rating: Int? = null, + override var tags: List? = null, + override var duration: Int? = null, + override var trailerUrl: String? = null, + override var recommendations: List? = null, ) : LoadResponse +fun MainAPI.newMovieLoadResponse( + name: String, + url: String, + type: TvType, + dataUrl : String, + initializer: MovieLoadResponse.() -> Unit = { } +): MovieLoadResponse { + val builder = MovieLoadResponse(name = name, url = url, apiName = this.name, type = type,dataUrl = dataUrl) + builder.initializer() + return builder +} + +fun LoadResponse.setDuration(input : String?) { + if (input == null) return + Regex("([0-9]*)h.*?([0-9]*)m").matchEntire(input)?.groupValues?.let { values -> + if(values.size == 3) { + val hours = values[1].toIntOrNull() + val minutes = values[2].toIntOrNull() + this.duration = if(minutes != null && hours != null) { hours * 60 + minutes } else null + } + } + Regex("([0-9]*)m").matchEntire(input)?.groupValues?.let { values -> + if(values.size == 2) { + this.duration = values[1].toIntOrNull() + } + } +} + data class TvSeriesEpisode( val name: String? = null, val season: Int? = null, @@ -529,25 +557,37 @@ data class TvSeriesEpisode( val posterUrl: String? = null, val date: String? = null, val rating: Int? = null, - val descript: String? = null, + val description: String? = null, ) data class TvSeriesLoadResponse( - override val name: String, - override val url: String, - override val apiName: String, - override val type: TvType, - val episodes: List, + override var name: String, + override var url: String, + override var apiName: String, + override var type: TvType, + var episodes: List, - override val posterUrl: String? = null, - override val year: Int? = null, - override val plot: String? = null, + override var posterUrl: String? = null, + override var year: Int? = null, + override var plot: String? = null, - val showStatus: ShowStatus? = null, - val imdbId: String? = null, - override val rating: Int? = null, - override val tags: List? = null, - override val duration: String? = null, - override val trailerUrl: String? = null, - override val recommendations: List? = null, + var showStatus: ShowStatus? = null, + var imdbId: String? = null, + override var rating: Int? = null, + override var tags: List? = null, + override var duration: Int? = null, + override var trailerUrl: String? = null, + override var recommendations: List? = null, ) : LoadResponse + +fun MainAPI.newTvSeriesLoadResponse( + name: String, + url: String, + type: TvType, + episodes : List, + initializer: TvSeriesLoadResponse.() -> Unit = { } +): TvSeriesLoadResponse { + val builder = TvSeriesLoadResponse(name = name, url = url, apiName = this.name, type = type, episodes = episodes) + builder.initializer() + return builder +} \ 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 b43177f2..914bbe16 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -370,8 +370,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { showToast(act, act.getString(message), duration) } - fun showToast(act: Activity?, message: String, duration: Int) { - if (act == null) return + fun showToast(act: Activity?, message: String?, duration: Int? = null) { + if (act == null || message == null) return try { currentToast?.cancel() } catch (e: Exception) { @@ -390,7 +390,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val toast = Toast(act) toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) - toast.duration = duration + toast.duration = duration ?: Toast.LENGTH_SHORT toast.view = layout toast.show() currentToast = toast diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ZoroProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ZoroProvider.kt index e441c366..89016484 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ZoroProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ZoroProvider.kt @@ -222,6 +222,28 @@ class ZoroProvider : MainAPI() { ) } + val recommendations = + document.select("#main-content > section > .tab-content > div > .film_list-wrap > .flw-item") + .mapNotNull { head -> + val filmPoster = head?.selectFirst(".film-poster") + val epPoster = filmPoster?.selectFirst("img")?.attr("data-src") + val a = head?.selectFirst(".film-detail > .film-name > a") + val epHref = a?.attr("href") + val epTitle = a?.attr("title") + if (epHref == null || epTitle == null || epPoster == null) { + null + } else { + AnimeSearchResponse( + epTitle, + fixUrl(epHref), + this.name, + TvType.Anime, + epPoster, + dubStatus = null + ) + } + } + return newAnimeLoadResponse(title, url, TvType.Anime) { japName = japaneseTitle engName = title @@ -231,6 +253,7 @@ class ZoroProvider : MainAPI() { showStatus = status plot = description this.tags = tags + this.recommendations = recommendations } } 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 5b45e852..937361b4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt @@ -2,12 +2,10 @@ package com.lagradost.cloudstream3.metaproviders import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.secondsToReadable import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.uwetrottmann.tmdb2.Tmdb import com.uwetrottmann.tmdb2.entities.* import java.util.* -import kotlin.math.roundToInt /** * episode and season starting from 1 @@ -34,7 +32,7 @@ open class TmdbProvider : MainAPI() { // Fuck it, public private api key because github actions won't co-operate. // Please no stealy. - val tmdb = Tmdb("e6333b32409e02a4a6eba6fb7ff866bb") + private val tmdb = Tmdb("e6333b32409e02a4a6eba6fb7ff866bb") private fun getImageUrl(link: String?): String? { if (link == null) return null @@ -81,38 +79,37 @@ open class TmdbProvider : MainAPI() { private fun TvShow.toLoadResponse(): TvSeriesLoadResponse { val episodes = this.seasons?.filter { !disableSeasonZero || (it.season_number ?: 0) != 0 } - ?.mapNotNull { - it.episodes?.map { + ?.mapNotNull { season -> + season.episodes?.map { episode -> TvSeriesEpisode( - it.name, - it.season_number, - it.episode_number, + episode.name, + episode.season_number, + episode.episode_number, TmdbLink( - it.external_ids?.imdb_id ?: this.external_ids?.imdb_id, + episode.external_ids?.imdb_id ?: this.external_ids?.imdb_id, this.id, - it.episode_number, - it.season_number, + episode.episode_number, + episode.season_number, ).toJson(), - getImageUrl(it.still_path), - it.air_date?.toString(), - it.rating, - it.overview, + getImageUrl(episode.still_path), + episode.air_date?.toString(), + episode.rating, + episode.overview, ) - } ?: (1..(it.episode_count ?: 1)).map { episodeNum -> + } ?: (1..(season.episode_count ?: 1)).map { episodeNum -> TvSeriesEpisode( episode = episodeNum, data = TmdbLink( this.external_ids?.imdb_id, this.id, episodeNum, - it.season_number, + season.season_number, ).toJson(), - season = it.season_number + season = season.season_number ) } }?.flatten() ?: listOf() -// println("STATUS ${this.status}") return TvSeriesLoadResponse( this.name ?: this.original_name, getUrl(id, true), @@ -130,7 +127,7 @@ open class TmdbProvider : MainAPI() { this.external_ids?.imdb_id, this.rating, this.genres?.mapNotNull { it.name }, - this.episode_run_time?.average()?.times(60)?.toInt()?.let { secondsToReadable(it, "") }, + this.episode_run_time?.average()?.toInt(), null, this.recommendations?.results?.map { it.toSearchResponse() } ) @@ -158,7 +155,7 @@ open class TmdbProvider : MainAPI() { null,//this.status this.rating, this.genres?.mapNotNull { it.name }, - this.runtime?.times(60)?.let { secondsToReadable(it, "") }, + this.runtime, null, this.recommendations?.results?.map { it.toSearchResponse() } ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/AllMoviesForYouProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/AllMoviesForYouProvider.kt index d55cb030..8845c50a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/AllMoviesForYouProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/AllMoviesForYouProvider.kt @@ -136,19 +136,13 @@ class AllMoviesForYouProvider : MainAPI() { val data = getLink(document) ?: throw ErrorLoadingException("No Links Found") - return MovieLoadResponse( - title, - url, - this.name, - type, - mapper.writeValueAsString(data.filter { it != "about:blank" }), - backgroundPoster, - year?.toIntOrNull(), - descipt, - null, - rating, - duration = duration - ) + return newMovieLoadResponse(title,url,type,mapper.writeValueAsString(data.filter { it != "about:blank" })) { + posterUrl = backgroundPoster + this.year = year?.toIntOrNull() + this.plot = descipt + this.rating = rating + setDuration(duration) + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SflixProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SflixProvider.kt index a73242a3..5c65490d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SflixProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SflixProvider.kt @@ -21,9 +21,9 @@ class SflixProvider(providerUrl: String, providerName: String) : MainAPI() { override val hasDownloadSupport = true override val usesWebView = true override val supportedTypes = setOf( - TvType.Movie, - TvType.TvSeries, - ) + TvType.Movie, + TvType.TvSeries, + ) private fun Element.toSearchResult(): SearchResponse { val img = this.select("img") @@ -162,22 +162,12 @@ class SflixProvider(providerUrl: String, providerName: String) : MainAPI() { val webViewUrl = "$url${sourceId?.let { ".$it" } ?: ""}".replace("/movie/", "/watch-movie/") - return MovieLoadResponse( - title, - url, - this.name, - TvType.Movie, - webViewUrl, - posterUrl, - year, - plot, - null, - null, - null, - duration, - null, - null - ) + return newMovieLoadResponse(title, url, TvType.Movie, webViewUrl) { + this.year = year + this.posterUrl = posterUrl + this.plot = plot + setDuration(duration) + } } else { val seasonsHtml = app.get("$mainUrl/ajax/v2/tv/seasons/$id").text val seasonsDocument = Jsoup.parse(seasonsHtml) @@ -212,23 +202,12 @@ class SflixProvider(providerUrl: String, providerName: String) : MainAPI() { } } - return TvSeriesLoadResponse( - title, - url, - this.name, - TvType.TvSeries, - episodes, - posterUrl, - year, - plot, - null, - null, - null, - null, - duration, - null, - null - ) + return newTvSeriesLoadResponse(title,url,TvType.TvSeries,episodes) { + this.posterUrl = posterUrl + this.year = year + this.plot = plot + setDuration(duration) + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/ThenosProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/ThenosProvider.kt deleted file mode 100644 index 38f2ed52..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/ThenosProvider.kt +++ /dev/null @@ -1,482 +0,0 @@ -package com.lagradost.cloudstream3.movieproviders - -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.module.kotlin.readValue -import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.getQualityFromName -import java.util.concurrent.TimeUnit - -class ThenosProvider : MainAPI() { - override val mainUrl = "https://www.thenos.org" - override val name = "Thenos" - override val hasQuickSearch = true - override val hasMainPage = true - override val hasChromecastSupport = false - - override val supportedTypes = setOf( - TvType.Movie, - TvType.TvSeries, - ) - - override fun getMainPage(): HomePageResponse { - val map = mapOf( - "New Releases" to "released", - "Recently Added in Movies" to "recent", - "Recently Added in Shows" to "recent/shows", - "Top Rated" to "rating" - ) - val list = ArrayList() - map.entries.forEach { - val url = "$apiUrl/library/${it.value}" - val response = app.get(url).text - val mapped = mapper.readValue(response) - - mapped.Metadata?.mapNotNull { meta -> - meta?.toSearchResponse() - }?.let { searchResponses -> - list.add( - HomePageList( - it.key, - searchResponses - ) - ) - } - } - - return HomePageResponse( - list - ) - } - - private fun secondsToReadable(seconds: Int, completedValue: String): String { - var secondsLong = seconds.toLong() - val days = TimeUnit.SECONDS - .toDays(secondsLong) - secondsLong -= TimeUnit.DAYS.toSeconds(days) - - val hours = TimeUnit.SECONDS - .toHours(secondsLong) - secondsLong -= TimeUnit.HOURS.toSeconds(hours) - - val minutes = TimeUnit.SECONDS - .toMinutes(secondsLong) - secondsLong -= TimeUnit.MINUTES.toSeconds(minutes) - if (minutes < 0) { - return completedValue - } - //println("$days $hours $minutes") - return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m" - } - - private val apiUrl = "https://api.thenos.org" - - override fun quickSearch(query: String): List { - val url = "$apiUrl/library/search?query=$query" - return searchFromUrl(url) - } - - data class ThenosSearchResponse( - @JsonProperty("size") val size: Int?, - @JsonProperty("Hub") val Hub: List? - ) - - data class Part( - @JsonProperty("id") val id: Long?, - @JsonProperty("key") val key: String?, - @JsonProperty("duration") val duration: Long?, - @JsonProperty("file") val file: String?, - @JsonProperty("size") val size: Long?, - @JsonProperty("audioProfile") val audioProfile: String?, - @JsonProperty("container") val container: String?, - @JsonProperty("has64bitOffsets") val has64bitOffsets: Boolean?, - @JsonProperty("optimizedForStreaming") val optimizedForStreaming: Boolean?, - @JsonProperty("videoProfile") val videoProfile: String? - ) - - data class Media( - @JsonProperty("id") val id: Long?, - @JsonProperty("duration") val duration: Long?, - @JsonProperty("bitrate") val bitrate: Long?, - @JsonProperty("width") val width: Long?, - @JsonProperty("height") val height: Long?, - @JsonProperty("aspectRatio") val aspectRatio: Double?, - @JsonProperty("audioChannels") val audioChannels: Long?, - @JsonProperty("audioCodec") val audioCodec: String?, - @JsonProperty("videoCodec") val videoCodec: String?, - @JsonProperty("videoResolution") val videoResolution: String?, - @JsonProperty("container") val container: String?, - @JsonProperty("videoFrameRate") val videoFrameRate: String?, - @JsonProperty("optimizedForStreaming") val optimizedForStreaming: Long?, - @JsonProperty("audioProfile") val audioProfile: String?, - @JsonProperty("has64bitOffsets") val has64bitOffsets: Boolean?, - @JsonProperty("videoProfile") val videoProfile: String?, - @JsonProperty("Part") val Part: List? - ) - - data class Genre( - @JsonProperty("tag") val tag: String? - ) - - - data class Country( - @JsonProperty("tag") val tag: String? - ) - - - data class Role( - @JsonProperty("tag") val tag: String? - ) - - data class Hub( - @JsonProperty("title") val title: String?, - @JsonProperty("type") val type: String?, - @JsonProperty("hubIdentifier") val hubIdentifier: String?, - @JsonProperty("context") val context: String?, - @JsonProperty("size") val size: Int?, - @JsonProperty("more") val more: Boolean?, - @JsonProperty("style") val style: String?, - @JsonProperty("Metadata") val Metadata: List? - ) - - data class Metadata( - @JsonProperty("librarySectionTitle") val librarySectionTitle: String?, - @JsonProperty("ratingKey") val ratingKey: String?, - @JsonProperty("key") val key: String?, - @JsonProperty("guid") val guid: String?, - @JsonProperty("studio") val studio: String?, - @JsonProperty("type") val type: String?, - @JsonProperty("title") val title: String?, - @JsonProperty("librarySectionID") val librarySectionID: Int?, - @JsonProperty("librarySectionKey") val librarySectionKey: String?, - @JsonProperty("contentRating") val contentRating: String?, - @JsonProperty("summary") val summary: String?, - @JsonProperty("audienceRating") val audienceRating: Int?, - @JsonProperty("year") val year: Int?, - @JsonProperty("thumb") val thumb: String?, - @JsonProperty("art") val art: String?, - @JsonProperty("duration") val duration: Int?, - @JsonProperty("originallyAvailableAt") val originallyAvailableAt: String?, - @JsonProperty("addedAt") val addedAt: Int?, - @JsonProperty("updatedAt") val updatedAt: Int?, - @JsonProperty("audienceRatingImage") val audienceRatingImage: String?, - @JsonProperty("Media") val Media: List?, - @JsonProperty("Genre") val Genre: List?, - @JsonProperty("Director") val Director: List?, - @JsonProperty("Country") val Country: List?, - @JsonProperty("Role") val Role: List? - ) - - data class Director( - @JsonProperty("tag") val tag: String - ) - - private fun Metadata.toSearchResponse(): SearchResponse? { - if (type == "movie") { - return MovieSearchResponse( - title ?: "", - ratingKey ?: return null, - this@ThenosProvider.name, - TvType.Movie, - art?.let { "$apiUrl$it" }, - year - - ) - } else if (type == "show") { - return TvSeriesSearchResponse( - title ?: "", - ratingKey ?: return null, - this@ThenosProvider.name, - TvType.TvSeries, - art?.let { "$apiUrl$it" }, - year, - null - ) - } - return null - } - - private fun searchFromUrl(url: String): List { - val response = app.get(url).text - val test = mapper.readValue(response) - val returnValue = ArrayList() - - test.Hub?.forEach { - it.Metadata?.forEach metadata@{ meta -> - if (meta.ratingKey == null || meta.title == null) return@metadata - meta.toSearchResponse()?.let { response -> returnValue.add(response) } - } - } - - return returnValue - } - - override fun search(query: String): List { - val url = "$apiUrl/library/search/advance?query=$query" - return searchFromUrl(url) - } - - data class ThenosSource( - @JsonProperty("title") val title: String?, - @JsonProperty("image") val image: String?, - @JsonProperty("sources") val sources: List?, - @JsonProperty("tracks") val tracks: List - ) - - data class Sources( - @JsonProperty("file") val file: String?, - @JsonProperty("label") val label: String?, - @JsonProperty("default") val default: Boolean?, - @JsonProperty("type") val type: String? - ) - - data class Tracks( - @JsonProperty("file") val file: String?, - @JsonProperty("label") val label: String?, - @JsonProperty("kind") val kind: String? - ) - - override fun loadLinks( - data: String, - isCasting: Boolean, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ): Boolean { - val url = "$apiUrl/library/watch/$data" - val response = app.get(url).text - val mapped = mapper.readValue(response) - - mapped.sources?.forEach { source -> - val isM3u8 = source.type != "video/mp4" - val token = app.get("https://token.noss.workers.dev/").text - val authorization = - base64Decode(token) - - callback.invoke( - ExtractorLink( - this.name, - "${this.name} ${source.label ?: ""}", - (source.file)?.split("/")?.lastOrNull()?.let { - "https://www.googleapis.com/drive/v3/files/$it?alt=media" - } ?: return@forEach, - "https://www.thenos.org/", - getQualityFromName(source.label ?: ""), - isM3u8, - mapOf("authorization" to "Bearer $authorization") - ) - ) - } - - mapped.tracks.forEach { - subtitleCallback.invoke( - SubtitleFile( - it.label ?: "English", - it.file ?: return@forEach - ) - ) - } - - return true - } - - data class ThenosLoadResponse( - @JsonProperty("size") val size: Long?, - @JsonProperty("allowSync") val allowSync: Boolean?, - @JsonProperty("augmentationKey") val augmentationKey: String?, - @JsonProperty("identifier") val identifier: String?, - @JsonProperty("librarySectionID") val librarySectionID: Long?, - @JsonProperty("librarySectionTitle") val librarySectionTitle: String?, - @JsonProperty("librarySectionUUID") val librarySectionUUID: String?, - @JsonProperty("mediaTagPrefix") val mediaTagPrefix: String?, - @JsonProperty("mediaTagVersion") val mediaTagVersion: Long?, - @JsonProperty("Metadata") val Metadata: List? - ) - - - data class ThenosSeriesResponse( - @JsonProperty("size") val size: Long?, - @JsonProperty("allowSync") val allowSync: Boolean?, - @JsonProperty("art") val art: String?, - @JsonProperty("identifier") val identifier: String?, - @JsonProperty("key") val key: String?, - @JsonProperty("librarySectionID") val librarySectionID: Long?, - @JsonProperty("librarySectionTitle") val librarySectionTitle: String?, - @JsonProperty("librarySectionUUID") val librarySectionUUID: String?, - @JsonProperty("mediaTagPrefix") val mediaTagPrefix: String?, - @JsonProperty("mediaTagVersion") val mediaTagVersion: Long?, - @JsonProperty("nocache") val nocache: Boolean?, - @JsonProperty("parentIndex") val parentIndex: Long?, - @JsonProperty("parentTitle") val parentTitle: String?, - @JsonProperty("parentYear") val parentYear: Long?, - @JsonProperty("summary") val summary: String?, - @JsonProperty("theme") val theme: String?, - @JsonProperty("thumb") val thumb: String?, - @JsonProperty("title1") val title1: String?, - @JsonProperty("title2") val title2: String?, - @JsonProperty("viewGroup") val viewGroup: String?, - @JsonProperty("viewMode") val viewMode: Long?, - @JsonProperty("Metadata") val Metadata: List? - ) - - data class SeriesMetadata( - @JsonProperty("ratingKey") val ratingKey: String?, - @JsonProperty("key") val key: String?, - @JsonProperty("parentRatingKey") val parentRatingKey: String?, - @JsonProperty("guid") val guid: String?, - @JsonProperty("parentGuid") val parentGuid: String?, - @JsonProperty("parentStudio") val parentStudio: String?, - @JsonProperty("type") val type: String?, - @JsonProperty("title") val title: String?, - @JsonProperty("parentKey") val parentKey: String?, - @JsonProperty("parentTitle") val parentTitle: String?, - @JsonProperty("summary") val summary: String?, - @JsonProperty("index") val index: Long?, - @JsonProperty("parentIndex") val parentIndex: Long?, - @JsonProperty("parentYear") val parentYear: Long?, - @JsonProperty("thumb") val thumb: String?, - @JsonProperty("art") val art: String?, - @JsonProperty("parentThumb") val parentThumb: String?, - @JsonProperty("parentTheme") val parentTheme: String?, - @JsonProperty("leafCount") val leafCount: Long?, - @JsonProperty("viewedLeafCount") val viewedLeafCount: Long?, - @JsonProperty("addedAt") val addedAt: Long?, - @JsonProperty("updatedAt") val updatedAt: Int? - ) - - data class SeasonResponse( - @JsonProperty("size") val size: Long?, - @JsonProperty("allowSync") val allowSync: Boolean?, - @JsonProperty("art") val art: String?, - @JsonProperty("grandparentContentRating") val grandparentContentRating: String?, - @JsonProperty("grandparentRatingKey") val grandparentRatingKey: Long?, - @JsonProperty("grandparentStudio") val grandparentStudio: String?, - @JsonProperty("grandparentTheme") val grandparentTheme: String?, - @JsonProperty("grandparentThumb") val grandparentThumb: String?, - @JsonProperty("grandparentTitle") val grandparentTitle: String?, - @JsonProperty("identifier") val identifier: String?, - @JsonProperty("key") val key: String?, - @JsonProperty("librarySectionID") val librarySectionID: Long?, - @JsonProperty("librarySectionTitle") val librarySectionTitle: String?, - @JsonProperty("librarySectionUUID") val librarySectionUUID: String?, - @JsonProperty("mediaTagPrefix") val mediaTagPrefix: String?, - @JsonProperty("mediaTagVersion") val mediaTagVersion: Long?, - @JsonProperty("nocache") val nocache: Boolean?, - @JsonProperty("parentIndex") val parentIndex: Long?, - @JsonProperty("parentTitle") val parentTitle: String?, - @JsonProperty("summary") val summary: String?, - @JsonProperty("theme") val theme: String?, - @JsonProperty("thumb") val thumb: String?, - @JsonProperty("title1") val title1: String?, - @JsonProperty("title2") val title2: String?, - @JsonProperty("viewGroup") val viewGroup: String?, - @JsonProperty("viewMode") val viewMode: Long?, - @JsonProperty("Metadata") val Metadata: List? - ) - - data class SeasonMetadata( - @JsonProperty("ratingKey") val ratingKey: String?, - @JsonProperty("key") val key: String?, - @JsonProperty("parentRatingKey") val parentRatingKey: String?, - @JsonProperty("grandparentRatingKey") val grandparentRatingKey: String?, - @JsonProperty("guid") val guid: String?, - @JsonProperty("parentGuid") val parentGuid: String?, - @JsonProperty("grandparentGuid") val grandparentGuid: String?, - @JsonProperty("type") val type: String?, - @JsonProperty("title") val title: String?, - @JsonProperty("grandparentKey") val grandparentKey: String?, - @JsonProperty("parentKey") val parentKey: String?, - @JsonProperty("grandparentTitle") val grandparentTitle: String?, - @JsonProperty("parentTitle") val parentTitle: String?, - @JsonProperty("contentRating") val contentRating: String?, - @JsonProperty("summary") val summary: String?, - @JsonProperty("index") val index: Int?, - @JsonProperty("parentIndex") val parentIndex: Int?, - @JsonProperty("audienceRating") val audienceRating: Double?, - @JsonProperty("thumb") val thumb: String?, - @JsonProperty("art") val art: String?, - @JsonProperty("parentThumb") val parentThumb: String?, - @JsonProperty("grandparentThumb") val grandparentThumb: String?, - @JsonProperty("grandparentArt") val grandparentArt: String?, - @JsonProperty("grandparentTheme") val grandparentTheme: String?, - @JsonProperty("duration") val duration: Long?, - @JsonProperty("originallyAvailableAt") val originallyAvailableAt: String?, - @JsonProperty("addedAt") val addedAt: Long?, - @JsonProperty("updatedAt") val updatedAt: Long?, - @JsonProperty("audienceRatingImage") val audienceRatingImage: String?, - @JsonProperty("Media") val Media: List?, - @JsonProperty("Director") val Director: List?, - @JsonProperty("Role") val Role: List? - ) - - private fun getAllEpisodes(id: String): List { - val episodes = ArrayList() - val url = "$apiUrl/library/metadata/$id/children" - val response = app.get(url).text - val mapped = mapper.readValue(response) - mapped.Metadata?.forEach { series_meta -> - val fixedUrl = apiUrl + series_meta.key - val child = app.get(fixedUrl).text - val mappedSeason = mapper.readValue(child) - mappedSeason.Metadata?.forEach mappedSeason@{ meta -> - episodes.add( - TvSeriesEpisode( - meta.title, - meta.parentIndex, - meta.index, - meta.ratingKey ?: return@mappedSeason, - meta.thumb?.let { "$apiUrl$it" }, - meta.originallyAvailableAt, - (meta.audienceRating?.times(10))?.toInt(), - meta.summary - ) - ) - } - - } - return episodes - } - - override fun load(url: String): LoadResponse? { - val fixedUrl = "$apiUrl/library/metadata/${url.split("/").last()}" - val response = app.get(fixedUrl).text - val mapped = mapper.readValue(response) - - val isShow = mapped.Metadata?.any { it?.type == "show" } == true - val metadata = mapped.Metadata?.getOrNull(0) ?: return null - - return if (!isShow) { - MovieLoadResponse( - metadata.title ?: "No title found", - "$mainUrl/movie/${metadata.ratingKey}", - this.name, - TvType.Movie, - metadata.ratingKey ?: return null, - metadata.art?.let { "$apiUrl$it" }, - metadata.year, - metadata.summary, - null, // with Guid this is possible - metadata.audienceRating?.times(10), - metadata.Genre?.mapNotNull { it.tag }, - metadata.duration?.let { secondsToReadable(it / 1000, "") }, - null - ) - } else { - TvSeriesLoadResponse( - metadata.title ?: "No title found", - "$mainUrl/show/${metadata.ratingKey}", - this.name, - TvType.TvSeries, - metadata.ratingKey?.let { getAllEpisodes(it) } ?: return null, - metadata.art?.let { "$apiUrl$it" }, - metadata.year, - metadata.summary, - null, // with Guid this is possible - null,// with Guid this is possible - metadata.audienceRating?.times(10), - metadata.Genre?.mapNotNull { it.tag }, - metadata.duration?.let { secondsToReadable(it / 1000, "") }, - null - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/TrailersToProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/TrailersToProvider.kt deleted file mode 100644 index fec3dafa..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/TrailersToProvider.kt +++ /dev/null @@ -1,305 +0,0 @@ -package com.lagradost.cloudstream3.movieproviders - -import com.fasterxml.jackson.module.kotlin.readValue -import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.Qualities -import com.lagradost.cloudstream3.utils.SubtitleHelper -import org.jsoup.Jsoup - -// referer = https://trailers.to, USERAGENT ALSO REQUIRED -class TrailersToProvider : MainAPI() { - override val mainUrl = "https://trailers.to" - override val name = "Trailers.to" - override val hasQuickSearch = true - override val hasMainPage = true - override val hasChromecastSupport = false - override val supportedTypes = setOf( - TvType.Movie, - TvType.TvSeries, - ) - - override val vpnStatus = VPNStatus.MightBeNeeded - - override fun getMainPage(): HomePageResponse? { - val response = app.get(mainUrl).text - val document = Jsoup.parse(response) - val returnList = ArrayList() - val docs = document.select("section.section > div.container") - for (doc in docs) { - val epList = doc.selectFirst("> div.owl-carousel") ?: continue - val title = doc.selectFirst("> div.text-center > h2").text() - val list = epList.select("> div.item > div.box-nina") - val isMovieType = title.contains("Movie") - val currentList = list.mapNotNull { head -> - val hrefItem = head.selectFirst("> div.box-nina-media > a") - val href = fixUrl(hrefItem.attr("href")) - val img = hrefItem.selectFirst("> img") - val posterUrl = img.attr("src") - val name = img.attr("alt") - return@mapNotNull if (isMovieType) MovieSearchResponse( - name, - href, - this.name, - TvType.Movie, - posterUrl, - null - ) else TvSeriesSearchResponse( - name, - href, - this.name, - TvType.TvSeries, - posterUrl, - null, null - ) - } - if (currentList.isNotEmpty()) { - returnList.add(HomePageList(title, currentList)) - } - } - if (returnList.size <= 0) return null - - return HomePageResponse(returnList) - //section.section > div.container > div.owl-carousel - } - - override fun quickSearch(query: String): List { - val url = "$mainUrl/en/quick-search?q=$query" - val response = app.get(url).text - val document = Jsoup.parse(response) - val items = document.select("div.group-post-minimal > a.post-minimal") - if (items.isNullOrEmpty()) return ArrayList() - - val returnValue = ArrayList() - for (item in items) { - val href = fixUrl(item.attr("href")) - val poster = item.selectFirst("> div.post-minimal-media > img").attr("src") - val header = item.selectFirst("> div.post-minimal-main") - val name = header.selectFirst("> span.link-black").text() - val info = header.select("> p") - val year = info?.get(1)?.text()?.toIntOrNull() - val isTvShow = href.contains("/tvshow/") - - returnValue.add( - if (isTvShow) { - TvSeriesSearchResponse(name, href, this.name, TvType.TvSeries, poster, year, null) - } else { - MovieSearchResponse(name, href, this.name, TvType.Movie, poster, year) - } - ) - } - return returnValue - } - - override fun search(query: String): List { - val url = "$mainUrl/en/popular/movies-tvshows-collections?q=$query" - val response = app.get(url).text - val document = Jsoup.parse(response) - val items = document.select("div.col-lg-8 > article.list-item") - if (items.isNullOrEmpty()) return ArrayList() - val returnValue = ArrayList() - for (item in items) { - val poster = item.selectFirst("> div.tour-modern-media > a.tour-modern-figure > img").attr("src") - val infoDiv = item.selectFirst("> div.tour-modern-main") - val nameHeader = infoDiv.select("> h5.tour-modern-title > a").last() - val name = nameHeader.text() - val href = fixUrl(nameHeader.attr("href")) - val year = infoDiv.selectFirst("> div > span.small-text")?.text()?.takeLast(4)?.toIntOrNull() - val isTvShow = href.contains("/tvshow/") - - returnValue.add( - if (isTvShow) { - TvSeriesSearchResponse(name, href, this.name, TvType.TvSeries, poster, year, null) - } else { - MovieSearchResponse(name, href, this.name, TvType.Movie, poster, year) - } - ) - } - return returnValue - } - - private fun loadLink( - data: String, - callback: (ExtractorLink) -> Unit, - ): Boolean { - val response = app.get(data).text - val url = " Unit) { - if (url.isEmpty()) return - - val response = app.get(fixUrl(url)).text - val document = Jsoup.parse(response) - - val items = document.select("div.list-group > a.list-group-item") - for (item in items) { - val hash = item.attr("hash") ?: continue - val languageCode = item.attr("languagecode") ?: continue - if (hash.isEmpty()) continue - if (languageCode.isEmpty()) continue - - subtitleCallback.invoke( - SubtitleFile( - SubtitleHelper.fromTwoLettersToLanguage(languageCode) ?: languageCode, - "$mainUrl/subtitles/$hash" - ) - ) - } - } - - override fun loadLinks( - data: String, - isCasting: Boolean, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ): Boolean { - if (isCasting) return false - val pairData = mapper.readValue>(data) - val url = pairData.second - - val isMovie = url.contains("/web-sources/") - if (isMovie) { - val isSucc = loadLink(url, callback) - val subUrl = pairData.first - loadSubs(subUrl, subtitleCallback) - - return isSucc - } else if (url.contains("/episode/")) { - val response = app.get(url, params = mapOf("preview" to "1")).text - val document = Jsoup.parse(response) - // val qSub = document.select("subtitle-content") - val subUrl = document.select("subtitle-content")?.attr("data-url") ?: "" - - val subData = fixUrl(document.selectFirst("content").attr("data-url") ?: return false) - val isSucc = if (subData.contains("/web-sources/")) { - loadLink(subData, callback) - } else false - - loadSubs(subUrl, subtitleCallback) - return isSucc - } - return false - } - - override fun load(url: String): LoadResponse { - val response = app.get(if (url.endsWith("?preview=1")) url else "$url?preview=1").text - val document = Jsoup.parse(response) - var title = document?.selectFirst("h2.breadcrumbs-custom-title > a")?.text() - ?: throw ErrorLoadingException("Service might be unavailable") - - val metaInfo = document.select("div.post-info-meta > ul.post-info-meta-list > li") - val year = metaInfo?.get(0)?.selectFirst("> span.small-text")?.text()?.takeLast(4)?.toIntOrNull() - val rating = parseRating(metaInfo?.get(1)?.selectFirst("> span.small-text")?.text()?.replace("/ 10", "")) - val duration = metaInfo?.get(2)?.selectFirst("> span.small-text")?.text() - val imdbUrl = metaInfo?.get(3)?.selectFirst("> a")?.attr("href") - val trailer = metaInfo?.get(4)?.selectFirst("> a")?.attr("href") - val poster = document.selectFirst("div.slider-image > a > img").attr("src") - val descriptHeader = document.selectFirst("article.post-info") - title = title.substring(0, title.length - 6) // REMOVE YEAR - - val descript = descriptHeader.select("> div > p").text() - val table = descriptHeader.select("> table.post-info-table > tbody > tr > td") - var generes: List? = null - for (i in 0 until table.size / 2) { - val header = table[i * 2].text() - val info = table[i * 2 + 1] - when (header) { - "Genre" -> generes = info.text().split(",") - } - } - val tags = if (generes == null) null else ArrayList(generes) - - val isTvShow = url.contains("/tvshow/") - if (isTvShow) { - val episodes = document.select("#seasons-accordion .card-body > .tour-modern") - ?: throw ErrorLoadingException("No Episodes found") - val parsedEpisodes = episodes.withIndex().map { (index, item) -> - val epPoster = item.selectFirst("img").attr("src") - val main = item.selectFirst(".tour-modern-main") - val titleHeader = main.selectFirst("a") - val titleName = titleHeader.text() - val href = fixUrl(titleHeader.attr("href")) - val gValues = - Regex(""".*?[\w\s]+ ([0-9]+)(?::[\w\s]+)?\s-\s(?:Episode )?([0-9]+)?(?:: )?(.*)""").find(titleName)?.destructured - val season = gValues?.component1()?.toIntOrNull() - var episode = gValues?.component2()?.toIntOrNull() - if (episode == null) { - episode = index + 1 - } - val epName = - if (gValues?.component3()?.isNotEmpty() == true) gValues.component3() else "Episode $episode" - val infoHeaders = main.select("span.small-text") - val date = infoHeaders?.get(0)?.text() - val ratingText = infoHeaders?.get(1)?.text()?.replace("/ 10", "") - val epRating = if (ratingText == null) null else parseRating(ratingText) - val epDescript = main.selectFirst("p")?.text() - - TvSeriesEpisode( - epName, - season, - episode, - mapper.writeValueAsString(Pair("", href)), - epPoster, - date, - epRating, - epDescript - ) - } - return TvSeriesLoadResponse( - title, - url, - this.name, - TvType.TvSeries, - ArrayList(parsedEpisodes), - poster, - year, - descript, - null, - imdbUrlToIdNullable(imdbUrl), - rating, - tags, - duration, - trailer - ) - } else { - - //https://trailers.to/en/subtitle-details/2086212/jungle-cruise-2021?imdbId=tt0870154&season=0&episode=0 - //https://trailers.to/en/movie/2086212/jungle-cruise-2021 - - val subUrl = if (imdbUrl != null) { - val imdbId = imdbUrlToId(imdbUrl) - url.replace("/movie/", "/subtitle-details/") + "?imdbId=$imdbId&season=0&episode=0" - } else "" - - val data = mapper.writeValueAsString( - Pair( - subUrl, - fixUrl( - document.selectFirst("content")?.attr("data-url") - ?: throw ErrorLoadingException("Link not found") - ) - ) - ) - return MovieLoadResponse( - title, - url, - this.name, - TvType.Movie, - data, - poster, - year, - descript, - imdbUrlToIdNullable(imdbUrl), - rating, - tags, - duration, - trailer - ) - } - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VfSerieProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VfSerieProvider.kt index 9979dd7d..2d357f17 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VfSerieProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VfSerieProvider.kt @@ -44,11 +44,11 @@ class VfSerieProvider : MainAPI() { private fun getDirect(original: String): String { // original data, https://vf-serie.org/?trembed=1&trid=80467&trtype=2 for example val response = app.get(original).text - val url = "iframe .*src=\\\"(.*?)\\\"".toRegex().find(response)?.groupValues?.get(1) + val url = "iframe .*src=\"(.*?)\"".toRegex().find(response)?.groupValues?.get(1) .toString() // https://vudeo.net/embed-7jdb1t5b2mvo.html for example val vudoResponse = app.get(url).text val document = Jsoup.parse(vudoResponse) - return Regex("sources: \\[\"(.*?)\"\\]").find(document.html())?.groupValues?.get(1) + return Regex("sources: \\[\"(.*?)\"]").find(document.html())?.groupValues?.get(1) .toString() // direct mp4 link, https://m5.vudeo.net/2vp3xgpw2avjdohilpfbtyuxzzrqzuh4z5yxvztral5k3rjnba6f4byj3saa/v.mp4 for exemple } 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 9c55859e..ca81485e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -6,12 +6,6 @@ import com.lagradost.cloudstream3.utils.DataStore.removeKeys import com.lagradost.cloudstream3.utils.DataStore.setKey abstract class AccountManager(private val defIndex: Int) : OAuth2API { - // don't change this as all keys depend on it - open val idPrefix: String - get() { - throw(NotImplementedError()) - } - var accountIndex = defIndex protected val accountId get() = "${idPrefix}_account_$accountIndex" private val accountActiveKey get() = "${idPrefix}_active" 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 43cfafce..504b07f1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt @@ -10,6 +10,9 @@ interface OAuth2API { val name: String val redirectUrl: String + // don't change this as all keys depend on it + val idPrefix : String + fun handleRedirect(context: Context, url: String) fun authenticate(context: Context) 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 f5277d81..c38bbf78 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 @@ -5,11 +5,10 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API //TODO dropbox sync class Dropbox : OAuth2API { + override val idPrefix = "dropbox" override val name = "Dropbox" - override val key: String - get() = "zlqsamadlwydvb2" - override val redirectUrl: String - get() = "dropboxlogin" + override val key = "zlqsamadlwydvb2" + override val redirectUrl = "dropboxlogin" override fun authenticate(context: Context) { TODO("Not yet implemented") 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 dd5a7605..ce475f14 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 @@ -48,12 +48,11 @@ import com.lagradost.cloudstream3.utils.HOMEPAGE_API import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView -import com.lagradost.cloudstream3.utils.UIHelper.getGridIsCompact +import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.widget.CenterZoomLayoutManager -import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.fragment_home.* import java.util.* @@ -124,15 +123,8 @@ class HomeFragment : Fragment() { } private fun fixGrid() { - val compactView = activity?.getGridIsCompact() ?: false - val spanCountLandscape = if (compactView) 2 else 6 - val spanCountPortrait = if (compactView) 1 else 3 - val orientation = resources.configuration.orientation - - currentSpan = if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - spanCountLandscape - } else { - spanCountPortrait + activity?.getSpanCount()?.let { + currentSpan = it } configEvent.invoke(currentSpan) } 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 2d9d1822..03ebec87 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 @@ -19,11 +19,8 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList import com.lagradost.cloudstream3.ui.home.ParentItemAdapter -import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD -import com.lagradost.cloudstream3.ui.search.SearchAdapter +import com.lagradost.cloudstream3.ui.search.* import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse -import com.lagradost.cloudstream3.ui.search.SearchHelper -import com.lagradost.cloudstream3.ui.search.SearchViewModel import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.navigate @@ -34,12 +31,22 @@ import java.util.concurrent.locks.ReentrantLock class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { companion object { - fun push(activity: Activity?, mainApi: Boolean = true, autoSearch: String? = null) { + fun pushSearch(activity: Activity?, autoSearch: String? = null) { activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply { - putBoolean("mainapi", mainApi) + putBoolean("mainapi", true) putString("autosearch", autoSearch) }) } + + fun pushSync(activity: Activity?, autoSearch: String? = null, callback : (SearchClickCallback) -> Unit) { + clickCallback = callback + activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply { + putBoolean("mainapi", false) + putString("autosearch", autoSearch) + }) + } + + var clickCallback : ((SearchClickCallback) -> Unit)? = null } private val searchViewModel: SearchViewModel by activityViewModels() @@ -56,6 +63,11 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { return inflater.inflate(R.layout.quick_search, container, false) } + override fun onDestroy() { + super.onDestroy() + clickCallback = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) context?.fixPaddingStatusbar(quick_search_root) @@ -96,7 +108,7 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { SearchHelper.handleSearchClickCallback(activity, callback) } else { - //TODO MAL RESPONSE + clickCallback?.invoke(callback) } } else -> SearchHelper.handleSearchClickCallback(activity, callback) @@ -135,7 +147,7 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { is Resource.Success -> { it.value.let { data -> if (data.isNotEmpty()) { - (cardSpace?.adapter as SearchAdapter?)?.apply { + (search_autofit_results?.adapter as SearchAdapter?)?.apply { cardList = data.toList() notifyDataSetChanged() } 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 cf1adba0..a48a8bdd 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 @@ -8,6 +8,7 @@ import android.content.Context.CLIPBOARD_SERVICE import android.content.Intent import android.content.Intent.* import android.content.res.ColorStateList +import android.content.res.Configuration import android.net.Uri import android.os.Bundle import android.view.LayoutInflater @@ -15,7 +16,9 @@ import android.view.View import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup +import android.widget.TextView import android.widget.Toast +import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.content.FileProvider import androidx.core.view.isGone @@ -39,6 +42,8 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis +import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO @@ -47,6 +52,8 @@ import com.lagradost.cloudstream3.ui.download.EasyDownloadButton import com.lagradost.cloudstream3.ui.player.PlayerData import com.lagradost.cloudstream3.ui.player.PlayerFragment import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment +import com.lagradost.cloudstream3.ui.search.SearchAdapter +import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1 import com.lagradost.cloudstream3.utils.* @@ -57,12 +64,15 @@ import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getFolderName import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper.addSync +import com.lagradost.cloudstream3.utils.DataStoreHelper.getSync import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar +import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage @@ -256,7 +266,65 @@ class ResultFragment : Fragment() { } var startAction: Int? = null - var startValue: Int? = null + private var startValue: Int? = null + + private fun updateSync(id: Int) { + val syncList = context?.getSync(id, SyncApis.map { it.idPrefix }) ?: return + val list = ArrayList>() + for (i in 0 until SyncApis.count()) { + val res = syncList[i] ?: continue + list.add(Pair(SyncApis[i], res)) + } + viewModel.updateSync(context, list) + } + + private fun setFormatText(textView: TextView?, @StringRes format: Int, arg: Any?) { + if (arg == null) { + textView?.isVisible = false + } else { + val text = context?.getString(format)?.format(arg) + if (text == null) { + textView?.isVisible = false + } else { + textView?.isVisible = true + textView?.text = text + } + } + } + + private fun setDuration(duration: Int?) { + setFormatText(result_meta_duration, R.string.duration_format, duration) + } + + private fun setYear(year: Int?) { + setFormatText(result_meta_year, R.string.year_format, year) + } + + private fun setRating(rating: Int?) { + setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000)) + } + + private fun setRecommendations(rec: List?) { + return + result_recommendations?.isGone = rec.isNullOrEmpty() + rec?.let { list -> + (result_recommendations?.adapter as SearchAdapter?)?.apply { + cardList = list + notifyDataSetChanged() + } + } + } + + private fun fixGrid() { + activity?.getSpanCount()?.let { count -> + result_recommendations?.spanCount = count + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + fixGrid() + } private fun lateFixDownloadButton(show: Boolean) { if (!show || currentType?.isMovieType() == false) { @@ -273,6 +341,7 @@ class ResultFragment : Fragment() { @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + fixGrid() val restart = arguments?.getBoolean("restart") ?: false if (restart) { @@ -949,6 +1018,20 @@ class ResultFragment : Fragment() { currentId = it } + observe(viewModel.sync) { sync -> + for (s in sync) { + when (s) { + is Resource.Success -> { + val d = s.value ?: continue + setDuration(d.duration) + setRating(d.publicScore) + } + else -> { + } + } + } + } + observe(viewModel.resultResponse) { data -> when (data) { is Resource.Success -> { @@ -965,7 +1048,7 @@ class ResultFragment : Fragment() { VPNStatus.Torrent -> getString(R.string.vpn_torrent) else -> "" } - result_vpn?.visibility = if (api.vpnStatus == VPNStatus.None) GONE else VISIBLE + result_vpn?.isGone = api.vpnStatus == VPNStatus.None result_info?.text = when (api.providerType) { ProviderType.MetaProvider -> getString(R.string.provider_info_meta) @@ -973,8 +1056,6 @@ class ResultFragment : Fragment() { } result_info?.isVisible = api.providerType == ProviderType.MetaProvider - //result_bookmark_button.text = getString(R.string.type_watching) - currentHeaderName = d.name currentType = d.type @@ -992,7 +1073,7 @@ class ResultFragment : Fragment() { } result_search?.setOnClickListener { - QuickSearchFragment.push(activity, true, d.name) + QuickSearchFragment.pushSearch(activity, d.name) } result_share?.setOnClickListener { @@ -1003,6 +1084,20 @@ class ResultFragment : Fragment() { startActivity(createChooser(i, d.name)) } + updateSync(d.getId()) + result_add_sync?.setOnClickListener { + QuickSearchFragment.pushSync(activity, d.name) { click -> + context?.addSync(d.getId(), click.card.apiName, click.card.url)?.let { + showToast( + activity, + context?.getString(R.string.added_sync_format)?.format(click.card.name), + Toast.LENGTH_SHORT + ) + } + updateSync(d.getId()) + } + } + val metadataInfoArray = ArrayList>() if (d is AnimeLoadResponse) { val status = when (d.showStatus) { @@ -1015,23 +1110,10 @@ class ResultFragment : Fragment() { } } - result_meta_year?.isGone = d.year == null - result_meta_year?.text = d.year?.toString() ?: "" - if (d.rating == null) { - result_meta_rating?.isVisible = false - } else { - result_meta_rating?.isVisible = true - result_meta_rating?.text = "%.1f/10.0".format(d.rating!!.toFloat() / 10f).replace(",", ".") - } - - val duration = d.duration - if (duration.isNullOrEmpty()) { - result_meta_duration?.isVisible = false - } else { - result_meta_duration?.isVisible = true - result_meta_duration?.text = - if (duration.endsWith("min") || duration.endsWith("h")) duration else "${duration}min" - } + setDuration(d.duration) + setYear(d.year) + setRating(d.rating) + setRecommendations(d.recommendations) result_meta_site?.text = d.apiName @@ -1188,6 +1270,17 @@ class ResultFragment : Fragment() { } } + val recAdapter: RecyclerView.Adapter? = activity?.let { + SearchAdapter( + ArrayList(), + result_recommendations, + ) { callback -> + SearchHelper.handleSearchClickCallback(activity, callback) + } + } + + result_recommendations.adapter = recAdapter + context?.let { ctx -> result_bookmark_button?.isVisible = ctx.isTvSettings() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt index 75ebc5bb..7fa13561 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt @@ -10,6 +10,7 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.utils.* @@ -85,6 +86,9 @@ class ResultViewModel : ViewModel() { private val _watchStatus: MutableLiveData = MutableLiveData() val watchStatus: LiveData get() = _watchStatus + private val _sync: MutableLiveData>> = MutableLiveData() + val sync: LiveData>> get() = _sync + fun updateWatchStatus(context: Context, status: WatchType) = viewModelScope.launch { val currentId = id.value ?: return@launch _watchStatus.postValue(status) @@ -198,6 +202,17 @@ class ResultViewModel : ViewModel() { } } + fun updateSync(context: Context?, sync: List>) = viewModelScope.launch { + if(context == null) return@launch + + val list = ArrayList>() + for (s in sync) { + val result = safeApiCall { s.first.getResult(context, s.second) } + list.add(result) + _sync.postValue(list) + } + } + private fun updateEpisodes(context: Context, localId: Int?, list: List, selection: Int?) { _episodes.postValue(list) val set = HashMap() @@ -363,7 +378,7 @@ class ResultViewModel : ViewModel() { (mainId + index + 1).hashCode(), index, i.rating, - i.descript, + i.description, 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 279954f0..29fb6d7c 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 @@ -9,6 +9,7 @@ import android.view.WindowManager import android.widget.* import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.preference.PreferenceManager @@ -27,6 +28,7 @@ import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository.Companion.providersActive import com.lagradost.cloudstream3.ui.APIRepository.Companion.typesActive import com.lagradost.cloudstream3.ui.home.HomeFragment +import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.currentSpan import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList import com.lagradost.cloudstream3.ui.home.ParentItemAdapter import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings @@ -35,7 +37,7 @@ import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.SEARCH_PROVIDER_TOGGLE import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import com.lagradost.cloudstream3.utils.UIHelper.getGridIsCompact +import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import kotlinx.android.synthetic.main.fragment_search.* import java.util.concurrent.locks.ReentrantLock @@ -68,18 +70,11 @@ class SearchFragment : Fragment() { } private fun fixGrid() { - val compactView = activity?.getGridIsCompact() ?: false - val spanCountLandscape = if (compactView) 2 else 6 - val spanCountPortrait = if (compactView) 1 else 3 - val orientation = resources.configuration.orientation - - val currentSpan = if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - spanCountLandscape - } else { - spanCountPortrait + activity?.getSpanCount()?.let { + currentSpan = it } - cardSpace.spanCount = currentSpan - HomeFragment.currentSpan = currentSpan + search_autofit_results.spanCount = currentSpan + currentSpan = currentSpan HomeFragment.configEvent.invoke(currentSpan) } @@ -102,13 +97,13 @@ class SearchFragment : Fragment() { val adapter: RecyclerView.Adapter? = activity?.let { SearchAdapter( ArrayList(), - cardSpace, + search_autofit_results, ) { callback -> SearchHelper.handleSearchClickCallback(activity, callback) } } - cardSpace.adapter = adapter + search_autofit_results.adapter = adapter search_loading_bar.alpha = 0f val searchExitIcon = main_search.findViewById(androidx.appcompat.R.id.search_close_btn) @@ -325,7 +320,7 @@ class SearchFragment : Fragment() { is Resource.Success -> { it.value.let { data -> if (data.isNotEmpty()) { - (cardSpace?.adapter as SearchAdapter?)?.apply { + (search_autofit_results?.adapter as SearchAdapter?)?.apply { cardList = data.toList() notifyDataSetChanged() } @@ -393,8 +388,8 @@ class SearchFragment : Fragment() { val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) val isAdvancedSearch = settingsManager.getBoolean("advanced_search", true) - search_master_recycler.visibility = if (isAdvancedSearch) View.VISIBLE else View.GONE - cardSpace.visibility = if (!isAdvancedSearch) View.VISIBLE else View.GONE + search_master_recycler.isVisible = isAdvancedSearch + search_autofit_results.isVisible = !isAdvancedSearch // SubtitlesFragment.push(activity) //searchViewModel.search("iron man") 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 70057f01..e6e62850 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -118,7 +118,7 @@ object DataStoreHelper { fun Context.setViewPos(id: Int?, pos: Long, dur: Long) { if (id == null) return - if(dur < 10_000) return // too short + if (dur < 10_000) return // too short setKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), PosDur(pos, dur)) } @@ -146,6 +146,16 @@ object DataStoreHelper { } fun Context.setResultSeason(id: Int, value: Int?) { - return setKey("$currentAccount/$RESULT_SEASON", id.toString(), value) + setKey("$currentAccount/$RESULT_SEASON", id.toString(), value) + } + + fun Context.addSync(id: Int, idPrefix: String, url: String) { + setKey("${idPrefix}_sync", id.toString(), url) + } + + fun Context.getSync(id : Int, idPrefixes : List) : List { + return idPrefixes.map { idPrefix -> + getKey("${idPrefix}_sync", id.toString()) + } } } \ No newline at end of file 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 50d3de29..bbfa328e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -7,6 +7,7 @@ import android.app.AppOpsManager import android.app.Dialog import android.content.Context import android.content.pm.PackageManager +import android.content.res.Configuration import android.content.res.Resources import android.graphics.Color import android.os.Build @@ -68,6 +69,19 @@ object UIHelper { ) } + fun Activity?.getSpanCount() : Int? { + val compactView = this?.getGridIsCompact() ?: return null + val spanCountLandscape = if (compactView) 2 else 6 + val spanCountPortrait = if (compactView) 1 else 3 + val orientation = this.resources?.configuration?.orientation ?: return null + + return if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + spanCountLandscape + } else { + spanCountPortrait + } + } + fun Fragment.hideKeyboard() { activity?.window?.decorView?.clearFocus() view?.let { 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 7cfcbc8a..e4a0deb7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt @@ -12,7 +12,7 @@ object VideoDownloadHelper { override val id: Int, val parentId: Int, val rating: Int?, - val descript: String?, + val description: String?, val cacheTime: Long, ) : EasyDownloadButton.IMinimumData diff --git a/app/src/main/res/drawable/ic_baseline_add_24.xml b/app/src/main/res/drawable/ic_baseline_add_24.xml new file mode 100644 index 00000000..70046c48 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_add_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 609f1936..72c64ff9 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -187,9 +187,30 @@ app:mediaRouteButtonTint="?attr/textColor" /> + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 3aeb0c02..0903ec54 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -26,7 +26,7 @@ android:nextFocusUp="@id/nav_rail_view" android:nextFocusRight="@id/search_filter" android:nextFocusLeft="@id/nav_rail_view" - android:nextFocusDown="@id/cardSpace" + android:nextFocusDown="@id/search_autofit_results" android:imeOptions="actionSearch" android:inputType="text" @@ -65,7 +65,7 @@ android:nextFocusUp="@id/nav_rail_view" android:nextFocusRight="@id/main_search" android:nextFocusLeft="@id/main_search" - android:nextFocusDown="@id/cardSpace" + android:nextFocusDown="@id/search_autofit_results" android:id="@+id/search_filter" android:background="?selectableItemBackgroundBorderless" @@ -91,7 +91,7 @@ android:paddingTop="5dp" app:spanCount="3" android:paddingEnd="8dp" - android:id="@+id/cardSpace" + android:id="@+id/search_autofit_results" tools:listitem="@layout/search_result_grid" android:orientation="vertical" /> diff --git a/app/src/main/res/layout/quick_search.xml b/app/src/main/res/layout/quick_search.xml index 2906758c..fe4b845a 100644 --- a/app/src/main/res/layout/quick_search.xml +++ b/app/src/main/res/layout/quick_search.xml @@ -34,7 +34,7 @@ -%d %d %d - %s Ep %d + %.1f/10.0 + %d + %s Ep %d Poster @@ -60,6 +62,7 @@ Rated: %.1f New update found!\n%s -> %s (Filler) %s + %d min CloudStream Home @@ -223,7 +226,7 @@ @string/sort_cancel Pause Resume - This will permanently delete %s\nAre you sure? + This will permanently delete %s\nAre you sure? Ongoing Completed @@ -337,12 +340,14 @@ Kitsu Trakt --> - %s %s + %s %s account Logout Login Switch account Add account + Add tracking + Added %s None Normal @@ -361,4 +366,6 @@ https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog --> The quick brown fox jumps over the lazy dog + + Recommended